diff --git a/Cargo.lock b/Cargo.lock index f30c2e0f..bcdc9550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2840,11 +2840,13 @@ name = "pinocchio-token-program" version = "0.0.0" dependencies = [ "assert_matches", + "num-traits", "pinocchio", "pinocchio-log", "solana-program-test", "solana-sdk", "spl-token 4.0.2", + "spl-token-2022", "spl-token-interface", "test-case", ] @@ -5861,6 +5863,12 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + [[package]] name = "solana-seed-derivable" version = "2.2.1" @@ -6736,6 +6744,136 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spl-discriminator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" +dependencies = [ + "bytemuck", + "solana-program-error", + "solana-sha256-hasher", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.96", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-elgamal-registry" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" +dependencies = [ + "bytemuck", + "solana-program", + "solana-zk-sdk", + "spl-pod", + "spl-token-confidential-transfer-proof-extraction", +] + +[[package]] +name = "spl-memo" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" +dependencies = [ + "solana-account-info", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "spl-pod" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a7d5950993e1ff2680bd989df298eeb169367fb2f9deeef1f132de6e4e8016" +dependencies = [ + "borsh 1.5.5", + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "solana-program-option", + "solana-pubkey", + "solana-zk-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" +dependencies = [ + "num-derive", + "num-traits", + "solana-program", + "spl-program-error-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", + "thiserror 1.0.69", +] + [[package]] name = "spl-token" version = "4.0.2" @@ -6784,6 +6922,105 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "spl-token" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-2022" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-security-txt", + "solana-zk-sdk", + "spl-elgamal-registry", + "spl-memo", + "spl-pod", + "spl-token 7.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface", + "spl-type-length-value", + "thiserror 2.0.11", +] + +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" +dependencies = [ + "bytemuck", + "solana-curve25519", + "solana-program", + "solana-zk-sdk", + "spl-pod", + "thiserror 2.0.11", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-generation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3597628b0d2fe94e7900fd17cdb4cfbb31ee35c66f82809d27d86e44b2848b" +dependencies = [ + "curve25519-dalek 4.1.3", + "solana-zk-sdk", + "thiserror 2.0.11", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator", + "spl-pod", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-interface" version = "0.0.0" @@ -6794,6 +7031,70 @@ dependencies = [ "strum_macros 0.27.1", ] +[[package]] +name = "spl-token-metadata-interface" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" +dependencies = [ + "borsh 1.5.5", + "num-derive", + "num-traits", + "solana-borsh", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator", + "spl-pod", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-type-length-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "spl-discriminator", + "spl-pod", + "thiserror 1.0.69", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 6de462c9..5b7b0ea5 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -478,6 +478,18 @@ pub enum TokenInstruction { /// - `&str` The `ui_amount` of tokens to reformat. UiAmountToAmount, + /// This instruction is to be used to rescue SOL sent to any `TokenProgram` + /// owned account by sending them to any other account, leaving behind only + /// lamports for rent exemption. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source Account owned by the token program + /// 1. `[writable]` Destination account + /// 2. `[signer]` Authority + /// 3. `..+M` `[signer]` M signer accounts. + WithdrawExcessLamports = 38, + /// Executes a batch of instructions. The instructions to be executed are specified /// in sequence on the instruction data. Each instruction provides: /// - `u8`: number of accounts @@ -506,7 +518,7 @@ impl TryFrom for TokenInstruction { fn try_from(value: u8) -> Result { match value { // SAFETY: `value` is guaranteed to be in the range of the enum variants. - 0..=24 | 255 => Ok(unsafe { core::mem::transmute::(value) }), + 0..=24 | 38 | 255 => Ok(unsafe { core::mem::transmute::(value) }), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/p-token/Cargo.toml b/p-token/Cargo.toml index d8769984..03d61893 100644 --- a/p-token/Cargo.toml +++ b/p-token/Cargo.toml @@ -21,7 +21,9 @@ spl-token-interface = { version = "^0", path = "../interface" } [dev-dependencies] assert_matches = "1.5.0" +num-traits = "0.2" solana-program-test = "2.1" solana-sdk = "2.1" spl-token = { version="^4", features=["no-entrypoint"] } +spl-token-2022 = { version="^7", features=["no-entrypoint"] } test-case = "3.3.1" diff --git a/p-token/src/entrypoint.rs b/p-token/src/entrypoint.rs index 4483f023..0296dd9d 100644 --- a/p-token/src/entrypoint.rs +++ b/p-token/src/entrypoint.rs @@ -257,6 +257,13 @@ fn inner_process_remaining_instruction( process_ui_amount_to_amount(accounts, instruction_data) } + // 38 - WithdrawExcessLamports + 38 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: WithdrawExcessLamports"); + + process_withdraw_excess_lamports(accounts) + } _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/p-token/src/processor/mod.rs b/p-token/src/processor/mod.rs index 0002c0f9..5182e83e 100644 --- a/p-token/src/processor/mod.rs +++ b/p-token/src/processor/mod.rs @@ -39,6 +39,7 @@ pub mod thaw_account; pub mod transfer; pub mod transfer_checked; pub mod ui_amount_to_amount; +pub mod withdraw_excess_lamports; // Shared processors. pub mod shared; @@ -68,6 +69,7 @@ pub use thaw_account::process_thaw_account; pub use transfer::process_transfer; pub use transfer_checked::process_transfer_checked; pub use ui_amount_to_amount::process_ui_amount_to_amount; +pub use withdraw_excess_lamports::process_withdraw_excess_lamports; /// Maximum number of digits in a formatted `u64`. /// diff --git a/p-token/src/processor/withdraw_excess_lamports.rs b/p-token/src/processor/withdraw_excess_lamports.rs new file mode 100644 index 00000000..44e9015a --- /dev/null +++ b/p-token/src/processor/withdraw_excess_lamports.rs @@ -0,0 +1,80 @@ +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use spl_token_interface::{ + error::TokenError, + state::{account::Account, load, mint::Mint, multisig::Multisig, Transmutable}, +}; + +use super::validate_owner; + +#[inline(always)] +pub fn process_withdraw_excess_lamports(accounts: &[AccountInfo]) -> ProgramResult { + let [source_account_info, destination_info, authority_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // SAFETY: single mutable borrow to `source_account_info` account data + let source_data = unsafe { source_account_info.borrow_data_unchecked() }; + + match source_data.len() { + Account::LEN => { + // SAFETY: `source_data` has the same length as `Account`. + let account = unsafe { load::(source_data)? }; + + if account.is_native() { + return Err(TokenError::NativeNotSupported.into()); + } + + validate_owner(&account.owner, authority_info, remaining)?; + } + Mint::LEN => { + // SAFETY: `source_data` has the same length as `Mint`. + let mint = unsafe { load::(source_data)? }; + + if let Some(mint_authority) = mint.mint_authority() { + validate_owner(mint_authority, authority_info, remaining)?; + } else { + return Err(TokenError::AuthorityTypeNotSupported.into()); + } + } + Multisig::LEN => { + validate_owner(source_account_info.key(), authority_info, remaining)?; + } + _ => return Err(TokenError::InvalidState.into()), + } + + // Withdraws the excess lamports from the source account. + + let source_rent_exempt_reserve = Rent::get()?.minimum_balance(source_data.len()); + + let transfer_amount = source_account_info + .lamports() + .checked_sub(source_rent_exempt_reserve) + .ok_or(TokenError::NotRentExempt)?; + + let source_starting_lamports = source_account_info.lamports(); + // SAFETY: single mutable borrow to `source_account_info` lamports. + unsafe { + // Moves the lamports out of the source account. + // + // Note: The `transfer_amount` is guaranteed to be less than the source account's + // lamports. + *source_account_info.borrow_mut_lamports_unchecked() = + source_starting_lamports - transfer_amount; + } + + let destination_starting_lamports = destination_info.lamports(); + // SAFETY: single mutable borrow to `destination_info` lamports. + unsafe { + // Moves the lamports to the destination account. + *destination_info.borrow_mut_lamports_unchecked() = destination_starting_lamports + .checked_add(transfer_amount) + .ok_or(TokenError::Overflow)?; + } + + Ok(()) +} diff --git a/p-token/tests/withdraw_excess_lamports.rs b/p-token/tests/withdraw_excess_lamports.rs new file mode 100644 index 00000000..6e56c0c4 --- /dev/null +++ b/p-token/tests/withdraw_excess_lamports.rs @@ -0,0 +1,756 @@ +mod setup; + +use assert_matches::assert_matches; +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, BanksClientError, ProgramTest}; +use solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::{Transaction, TransactionError}, +}; +use spl_token_interface::state::{account::Account, mint::Mint, multisig::Multisig}; +use std::mem::size_of; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn withdraw_excess_lamports_from_mint(token_program: Pubkey) { + let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given a mint authority, freeze authority and an account keypair. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + let account = Keypair::new(); + let account_pubkey = account.pubkey(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &account.pubkey(), + &mint_authority.pubkey(), + Some(&freeze_authority), + 0, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // And we initialize a mint account with excess lamports. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we withdraw the excess lamports. + + let destination = Pubkey::new_unique(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &account_pubkey, + &destination, + &mint_authority.pubkey(), + &[], + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the destination account has the excess lamports. + + let destination = context.banks_client.get_account(destination).await.unwrap(); + + assert!(destination.is_some()); + + let destination = destination.unwrap(); + assert_eq!(destination.lamports, excess_lamports); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn withdraw_excess_lamports_from_account(token_program: Pubkey) { + let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // Given a mint authority, freeze authority and an account keypair. + + let owner = Keypair::new(); + let account = Keypair::new(); + let account_pubkey = account.pubkey(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_account( + &spl_token::ID, + &account.pubkey(), + &mint, + &owner.pubkey(), + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we withdraw the excess lamports. + + let destination = Pubkey::new_unique(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &account_pubkey, + &destination, + &owner.pubkey(), + &[], + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the destination account has the excess lamports. + + let destination = context.banks_client.get_account(destination).await.unwrap(); + + assert!(destination.is_some()); + + let destination = destination.unwrap(); + assert_eq!(destination.lamports, excess_lamports); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn withdraw_excess_lamports_from_multisig(token_program: Pubkey) { + let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given an account + + let multisig = Keypair::new(); + let signer1 = Keypair::new(); + let signer1_pubkey = signer1.pubkey(); + let signer2 = Keypair::new(); + let signer2_pubkey = signer2.pubkey(); + let signer3 = Keypair::new(); + let signer3_pubkey = signer3.pubkey(); + let signers = vec![&signer1_pubkey, &signer2_pubkey, &signer3_pubkey]; + + let rent = context.banks_client.get_rent().await.unwrap(); + let account_size = size_of::(); + + let mut initialize_ix = spl_token::instruction::initialize_multisig( + &spl_token::ID, + &multisig.pubkey(), + &signers, + 3, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // And we initialize the multisig account. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &multisig.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &multisig], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(multisig.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we withdraw the excess lamports. + + let destination = Pubkey::new_unique(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &multisig.pubkey(), + &destination, + &multisig.pubkey(), + &signers, + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &signer1, &signer2, &signer3], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the destination account has the excess lamports. + + let destination = context.banks_client.get_account(destination).await.unwrap(); + + assert!(destination.is_some()); + + let destination = destination.unwrap(); + assert_eq!(destination.lamports, excess_lamports); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn fail_withdraw_excess_lamports_from_mint_wrong_authority(token_program: Pubkey) { + let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given a mint authority, freeze authority and an account keypair. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + let account = Keypair::new(); + let account_pubkey = account.pubkey(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &account.pubkey(), + &mint_authority.pubkey(), + Some(&freeze_authority), + 0, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // And we initialize a mint account with excess lamports. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we try to withdraw the excess lamports with the wrong authority. + + let destination = Pubkey::new_unique(); + let wrong_authority = Keypair::new(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &account_pubkey, + &destination, + &wrong_authority.pubkey(), + &[], + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + // The we expect an error. + + assert_matches!( + error, + BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(4) // TokenError::OwnerMismatch + )) + ); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn fail_withdraw_excess_lamports_from_account_wrong_authority(token_program: Pubkey) { + let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // Given a mint authority, freeze authority and an account keypair. + + let owner = Keypair::new(); + let account = Keypair::new(); + let account_pubkey = account.pubkey(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_account( + &spl_token::ID, + &account.pubkey(), + &mint, + &owner.pubkey(), + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we try to withdraw the excess lamports with the wrong owner. + + let destination = Pubkey::new_unique(); + let wrong_owner = Keypair::new(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &account_pubkey, + &destination, + &wrong_owner.pubkey(), + &[], + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_owner], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + // The we expect an error. + + assert_matches!( + error, + BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(4) // TokenError::OwnerMismatch + )) + ); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn fail_withdraw_excess_lamports_from_multisig_wrong_authority(token_program: Pubkey) { + let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given an account + + let multisig = Keypair::new(); + let signer1 = Keypair::new(); + let signer1_pubkey = signer1.pubkey(); + let signer2 = Keypair::new(); + let signer2_pubkey = signer2.pubkey(); + let signer3 = Keypair::new(); + let signer3_pubkey = signer3.pubkey(); + let signers = vec![&signer1_pubkey, &signer2_pubkey, &signer3_pubkey]; + + let rent = context.banks_client.get_rent().await.unwrap(); + let account_size = size_of::(); + + let mut initialize_ix = spl_token::instruction::initialize_multisig( + &spl_token::ID, + &multisig.pubkey(), + &signers, + 3, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // And we initialize the multisig account. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &multisig.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &multisig], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(multisig.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we try to withdraw the excess lamports with the wrong authority. + + let destination = Pubkey::new_unique(); + let wrong_authority = Keypair::new(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &multisig.pubkey(), + &destination, + &wrong_authority.pubkey(), + &signers, + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &signer1, &signer2, &signer3], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + // The we expect an error. + + assert_matches!( + error, + BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(4) // TokenError::OwnerMismatch + )) + ); +} + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn fail_withdraw_excess_lamports_from_multisig_missing_signer(token_program: Pubkey) { + let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + let excess_lamports = 4_000_000_000_000; + + // Given an account + + let multisig = Keypair::new(); + let signer1 = Keypair::new(); + let signer1_pubkey = signer1.pubkey(); + let signer2 = Keypair::new(); + let signer2_pubkey = signer2.pubkey(); + let signer3 = Keypair::new(); + let signer3_pubkey = signer3.pubkey(); + let signers = vec![&signer1_pubkey, &signer2_pubkey, &signer3_pubkey]; + + let rent = context.banks_client.get_rent().await.unwrap(); + let account_size = size_of::(); + + let mut initialize_ix = spl_token::instruction::initialize_multisig( + &spl_token::ID, + &multisig.pubkey(), + &signers, + 3, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // And we initialize the multisig account. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &multisig.pubkey(), + rent.minimum_balance(account_size) + excess_lamports, + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &multisig], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let account = context + .banks_client + .get_account(multisig.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + assert_eq!( + account.lamports, + rent.minimum_balance(account_size) + excess_lamports + ); + + // When we try to withdraw the excess lamports with the wrong authority. + + let destination = Pubkey::new_unique(); + + let mut withdraw_ix = spl_token_2022::instruction::withdraw_excess_lamports( + &spl_token_2022::ID, + &multisig.pubkey(), + &destination, + &multisig.pubkey(), + &[&signer1_pubkey, &signer2_pubkey], + ) + .unwrap(); + // Switches the program id to the token program. + withdraw_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[withdraw_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &signer1, &signer2], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + // The we expect an error. + + assert_matches!( + error, + BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::MissingRequiredSignature + )) + ); +}