Browse Source

p-token: Add `withdraw_excess_lamports` instruction (#36)

* Update workspace

* Rename interface crate

* Fix spelling

* [wip]: Fix review comments

* A few more fixes

* Fix merge

* Update workspace

* Rename interface crate

* Fix spelling

* [wip]: Fix review comments

* Add withdraw_excess_lamports instruction

* Add test

* Add missing safety comments

* Add more tests

* Fix merge

* Use assert_matches
Fernando Otero 7 months ago
parent
commit
90152b12d0

+ 301 - 0
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"

+ 13 - 1
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<u8> for TokenInstruction {
     fn try_from(value: u8) -> Result<Self, Self::Error> {
         match value {
             // SAFETY: `value` is guaranteed to be in the range of the enum variants.
-            0..=24 | 255 => Ok(unsafe { core::mem::transmute::<u8, TokenInstruction>(value) }),
+            0..=24 | 38 | 255 => Ok(unsafe { core::mem::transmute::<u8, TokenInstruction>(value) }),
             _ => Err(ProgramError::InvalidInstructionData),
         }
     }

+ 2 - 0
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"

+ 7 - 0
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),
     }
 }

+ 2 - 0
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`.
 ///

+ 80 - 0
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::<Account>(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::<Mint>(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(())
+}

+ 756 - 0
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::<Mint>();
+    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::<Account>();
+    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::<Multisig>();
+
+    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::<Mint>();
+    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::<Account>();
+    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::<Multisig>();
+
+    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::<Multisig>();
+
+    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
+        ))
+    );
+}