Browse Source

p-token: Add `unwrap_lamports` instruction (#87)

* Add processor

* Add tests

* Add instruction

* Fix formatting

* Fix discriminator value

* Add check for unwrap lamports

* Update discriminator on tests

* Make amount optional

* Address review comments

* Review comments

* Update import
Fernando Otero 3 weeks ago
parent
commit
ae4630eade

+ 17 - 0
p-interface/src/instruction.rs

@@ -498,6 +498,23 @@ pub enum TokenInstruction {
     ///   3. `..+M` `[signer]` M signer accounts.
     WithdrawExcessLamports = 38,
 
+    /// Transfer lamports from a native SOL account to a destination account.
+    ///
+    /// This is useful to unwrap lamports from a wrapped SOL account.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   0. `[writable]` The source account.
+    ///   1. `[writable]` The destination account.
+    ///   2. `[signer]` The source account's owner/delegate.
+    ///
+    /// Data expected by this instruction:
+    ///
+    ///   - `Option<u64>` The amount of lamports to transfer. When an amount is
+    ///     not specified, the entire balance of the source account will be
+    ///     transferred.
+    UnwrapLamports = 45,
+
     /// Executes a batch of instructions. The instructions to be executed are
     /// specified in sequence on the instruction data. Each instruction
     /// provides:

+ 7 - 0
p-token/src/entrypoint.rs

@@ -486,6 +486,13 @@ fn inner_process_remaining_instruction(
 
             process_withdraw_excess_lamports(accounts)
         }
+        // 45 - UnwrapLamports
+        45 => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: UnwrapLamports");
+
+            process_unwrap_lamports(accounts, instruction_data)
+        }
         _ => Err(TokenError::InvalidInstruction.into()),
     }
 }

+ 2 - 1
p-token/src/processor/batch.rs

@@ -84,7 +84,8 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8])
                 // 13 - ApproveChecked
                 // 22 - InitializeImmutableOwner
                 // 38 - WithdrawExcessLamports
-                4..=13 | 22 | 38 => {
+                // 45 - UnwrapLamports
+                4..=13 | 22 | 38 | 45 => {
                     let [a0, ..] = ix_accounts else {
                         return Err(ProgramError::NotEnoughAccountKeys);
                     };

+ 2 - 0
p-token/src/processor/mod.rs

@@ -41,6 +41,7 @@ pub mod thaw_account;
 pub mod transfer;
 pub mod transfer_checked;
 pub mod ui_amount_to_amount;
+pub mod unwrap_lamports;
 pub mod withdraw_excess_lamports;
 // Shared processors.
 pub mod shared;
@@ -61,6 +62,7 @@ pub use {
     set_authority::process_set_authority, sync_native::process_sync_native,
     thaw_account::process_thaw_account, transfer::process_transfer,
     transfer_checked::process_transfer_checked, ui_amount_to_amount::process_ui_amount_to_amount,
+    unwrap_lamports::process_unwrap_lamports,
     withdraw_excess_lamports::process_withdraw_excess_lamports,
 };
 

+ 87 - 0
p-token/src/processor/unwrap_lamports.rs

@@ -0,0 +1,87 @@
+use {
+    super::validate_owner,
+    crate::processor::{check_account_owner, unpack_amount},
+    pinocchio::{
+        account_info::AccountInfo, hint::likely, program_error::ProgramError, ProgramResult,
+    },
+    pinocchio_token_interface::{
+        error::TokenError,
+        state::{account::Account, load_mut},
+    },
+};
+
+#[allow(clippy::arithmetic_side_effects)]
+pub fn process_unwrap_lamports(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
+    // instruction data: expected u8 (1) + optional u64 (8)
+    let [has_amount, maybe_amount @ ..] = instruction_data else {
+        return Err(TokenError::InvalidInstruction.into());
+    };
+
+    let maybe_amount = if likely(*has_amount == 0) {
+        None
+    } else if *has_amount == 1 {
+        Some(unpack_amount(maybe_amount)?)
+    } else {
+        return Err(TokenError::InvalidInstruction.into());
+    };
+
+    let [source_account_info, destination_account_info, authority_info, remaining @ ..] = accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    // SAFETY: single immutable borrow to `source_account_info` account data
+    let source_account =
+        unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
+
+    if !source_account.is_native() {
+        return Err(TokenError::NonNativeNotSupported.into());
+    }
+
+    // SAFETY: `authority_info` is not currently borrowed; in the case
+    // `authority_info` is the same as `source_account_info`, then it cannot be
+    // a multisig.
+    unsafe { validate_owner(&source_account.owner, authority_info, remaining)? };
+
+    // If we have an amount, we need to validate whether there are enough lamports
+    // to unwrap or not; otherwise we just use the full amount.
+    let (amount, remaining_amount) = if let Some(amount) = maybe_amount {
+        (
+            amount,
+            source_account
+                .amount()
+                .checked_sub(amount)
+                .ok_or(TokenError::InsufficientFunds)?,
+        )
+    } else {
+        (source_account.amount(), 0)
+    };
+
+    // Comparing whether the AccountInfo's "point" to the same account or
+    // not - this is a faster comparison since it just checks the internal
+    // raw pointer.
+    let self_transfer = source_account_info == destination_account_info;
+
+    if self_transfer || amount == 0 {
+        // Validates the token account owner since we are not writing
+        // to the account.
+        check_account_owner(source_account_info)
+    } else {
+        source_account.set_amount(remaining_amount);
+
+        // SAFETY: single mutable borrow to `source_account_info` lamports.
+        let source_lamports = unsafe { source_account_info.borrow_mut_lamports_unchecked() };
+        // Note: The amount of a source token account is already validated and the
+        // `lamports` on the account is always greater than `amount`.
+        *source_lamports -= amount;
+
+        // SAFETY: single mutable borrow to `destination_account_info` lamports; the
+        // account is already validated to be different from `source_account_info`.
+        let destination_lamports =
+            unsafe { destination_account_info.borrow_mut_lamports_unchecked() };
+        // Note: The total lamports supply is bound to `u64::MAX`.
+        *destination_lamports += amount;
+
+        Ok(())
+    }
+}

+ 629 - 0
p-token/tests/unwrap_lamports.rs

@@ -0,0 +1,629 @@
+mod setup;
+
+use {
+    crate::setup::TOKEN_PROGRAM_ID,
+    mollusk_svm::{result::Check, Mollusk},
+    pinocchio_token_interface::{
+        error::TokenError,
+        instruction::TokenInstruction,
+        native_mint,
+        state::{
+            account::Account as TokenAccount, account_state::AccountState, load_mut_unchecked,
+        },
+    },
+    solana_account::Account,
+    solana_instruction::{error::InstructionError, AccountMeta, Instruction},
+    solana_program_error::ProgramError,
+    solana_program_pack::Pack,
+    solana_pubkey::Pubkey,
+    solana_rent::Rent,
+    solana_sdk_ids::bpf_loader_upgradeable,
+};
+
+fn create_token_account(
+    mint: &Pubkey,
+    owner: &Pubkey,
+    is_native: bool,
+    amount: u64,
+    program_owner: &Pubkey,
+) -> Account {
+    let space = size_of::<TokenAccount>();
+    let mut lamports = Rent::default().minimum_balance(space);
+
+    let mut data: Vec<u8> = vec![0u8; space];
+    let token = unsafe { load_mut_unchecked::<TokenAccount>(data.as_mut_slice()).unwrap() };
+    token.set_account_state(AccountState::Initialized);
+    token.mint = *mint.as_array();
+    token.owner = *owner.as_array();
+    token.set_amount(amount);
+    token.set_native(is_native);
+
+    if is_native {
+        token.set_native_amount(lamports);
+        lamports = lamports.saturating_add(amount);
+    }
+
+    Account {
+        lamports,
+        data,
+        owner: *program_owner,
+        executable: false,
+        ..Default::default()
+    }
+}
+
+/// Creates a Mollusk instance with the default feature set.
+fn mollusk() -> Mollusk {
+    let mut mollusk = Mollusk::default();
+    mollusk.add_program(
+        &TOKEN_PROGRAM_ID,
+        "pinocchio_token_program",
+        &bpf_loader_upgradeable::id(),
+    );
+    mollusk
+}
+
+fn unwrap_lamports_instruction(
+    source: &Pubkey,
+    destination: &Pubkey,
+    authority: &Pubkey,
+    amount: Option<u64>,
+) -> Result<Instruction, ProgramError> {
+    let accounts = vec![
+        AccountMeta::new(*source, false),
+        AccountMeta::new(*destination, false),
+        AccountMeta::new_readonly(*authority, true),
+    ];
+
+    // Start with the batch discriminator
+    let mut data: Vec<u8> = vec![TokenInstruction::UnwrapLamports as u8];
+
+    if let Some(amount) = amount {
+        data.push(1);
+        data.extend_from_slice(&amount.to_le_bytes());
+    } else {
+        data.push(0);
+    }
+
+    Ok(Instruction {
+        program_id: spl_token::ID,
+        data,
+        accounts,
+    })
+}
+
+#[test]
+fn unwrap_lamports() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        None,
+    )
+    .unwrap();
+
+    // It should succeed to unwrap 2_000_000_000 lamports.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&destination_account_key)
+                .lamports(2_000_000_000)
+                .build(),
+            Check::account(&source_account_key)
+                .lamports(Rent::default().minimum_balance(size_of::<TokenAccount>()))
+                .build(),
+        ],
+    );
+
+    // And the remaining amount must be 0.
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+}
+
+#[test]
+fn unwrap_lamports_with_amount() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        Some(2_000_000_000),
+    )
+    .unwrap();
+
+    // It should succeed to unwrap 2_000_000_000 lamports.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&destination_account_key)
+                .lamports(2_000_000_000)
+                .build(),
+            Check::account(&source_account_key)
+                .lamports(Rent::default().minimum_balance(size_of::<TokenAccount>()))
+                .build(),
+        ],
+    );
+
+    // And the remaining amount must be 0.
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+}
+
+#[test]
+fn fail_unwrap_lamports_with_insufficient_funds() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 1_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        1_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        Some(2_000_000_000),
+    )
+    .unwrap();
+
+    // When we try to unwrap 2_000_000_000 lamports, we expect a
+    // `TokenError::InsufficientFunds` error.
+
+    mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[Check::err(ProgramError::Custom(
+            TokenError::InsufficientFunds as u32,
+        ))],
+    );
+}
+
+#[test]
+fn unwrap_lamports_with_parial_amount() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        Some(1_000_000_000),
+    )
+    .unwrap();
+
+    // It should succeed to unwrap 1_000_000_000 lamports.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&destination_account_key)
+                .lamports(1_000_000_000)
+                .build(),
+            Check::account(&source_account_key)
+                .lamports(
+                    Rent::default().minimum_balance(size_of::<TokenAccount>()) + 1_000_000_000,
+                )
+                .build(),
+        ],
+    );
+
+    // And the remaining amount must be 1_000_000_000.
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 1_000_000_000);
+}
+
+#[test]
+fn fail_unwrap_lamports_with_invalid_authority() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+    let fake_authority_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 1_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        1_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &fake_authority_key, // <-- wrong authority
+        Some(2_000_000_000),
+    )
+    .unwrap();
+
+    // When we try to unwrap lamports with an invalid authority, we expect a
+    // `TokenError::OwnerMismatch` error.
+
+    mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (fake_authority_key, Account::default()),
+        ],
+        &[Check::err(ProgramError::Custom(
+            TokenError::OwnerMismatch as u32,
+        ))],
+    );
+}
+
+#[test]
+fn fail_unwrap_lamports_with_non_native_account() {
+    let mint = Pubkey::new_unique();
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+
+    // non-native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let mut source_account = create_token_account(
+        &mint,
+        &authority_key,
+        false, // <-- non-native account
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+    source_account.lamports += 2_000_000_000;
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        Some(1_000_000_000),
+    )
+    .unwrap();
+
+    // When we try to unwrap lamports from a non-native account, we expect a
+    // `TokenError::NonNativeNotSupported` error.
+
+    mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[Check::err(ProgramError::Custom(
+            TokenError::NonNativeNotSupported as u32,
+        ))],
+    );
+}
+
+#[test]
+fn unwrap_lamports_with_self_transfer() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &source_account_key, // <-- destination same as source
+        &authority_key,
+        Some(1_000_000_000),
+    )
+    .unwrap();
+
+    // It should succeed to unwrap lamports with the same source and destination
+    // accounts.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&source_account_key)
+                .lamports(
+                    Rent::default().minimum_balance(size_of::<TokenAccount>()) + 2_000_000_000,
+                )
+                .build(),
+        ],
+    );
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 2_000_000_000);
+}
+
+#[test]
+fn fail_unwrap_lamports_with_invalid_native_account() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let destination_account_key = Pubkey::new_unique();
+    let invalid_program_owner = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let mut source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &invalid_program_owner, // <-- invalid program owner
+    );
+    source_account.lamports += 2_000_000_000;
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        Some(1_000_000_000),
+    )
+    .unwrap();
+
+    // When we try to unwrap lamports with an invalid native account, we expect
+    // a `InstructionError::ExternalAccountDataModified` error.
+
+    mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, Account::default()),
+            (authority_key, Account::default()),
+        ],
+        &[Check::instruction_err(
+            InstructionError::ExternalAccountDataModified,
+        )],
+    );
+}
+
+#[test]
+fn unwrap_lamports_to_native_account() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    // destination native account:
+    //   - amount: 0
+    let destination_account_key = Pubkey::new_unique();
+    let destination_account =
+        create_token_account(&native_mint, &authority_key, true, 0, &TOKEN_PROGRAM_ID);
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        None,
+    )
+    .unwrap();
+
+    // It should succeed to unwrap 2_000_000_000 lamports.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, destination_account),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&destination_account_key)
+                .lamports(
+                    Rent::default().minimum_balance(size_of::<TokenAccount>()) + 2_000_000_000,
+                )
+                .build(),
+            Check::account(&source_account_key)
+                .lamports(Rent::default().minimum_balance(size_of::<TokenAccount>()))
+                .build(),
+        ],
+    );
+
+    // And the remaining amount on the source account must be 0.
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+
+    // And the amount on the destination account must be 0 since we transferred
+    // lamports directly to the account.
+
+    let account = result.get_account(&destination_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+}
+
+#[test]
+fn unwrap_lamports_to_token_account() {
+    let native_mint = Pubkey::new_from_array(native_mint::ID);
+    let authority_key = Pubkey::new_unique();
+    let non_native_mint = Pubkey::new_unique();
+
+    // native account:
+    //   - amount: 2_000_000_000
+    let source_account_key = Pubkey::new_unique();
+    let source_account = create_token_account(
+        &native_mint,
+        &authority_key,
+        true,
+        2_000_000_000,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    // destination non-native account:
+    //   - amount: 0
+    let destination_account_key = Pubkey::new_unique();
+    let destination_account = create_token_account(
+        &non_native_mint,
+        &authority_key,
+        false,
+        0,
+        &TOKEN_PROGRAM_ID,
+    );
+
+    let instruction = unwrap_lamports_instruction(
+        &source_account_key,
+        &destination_account_key,
+        &authority_key,
+        None,
+    )
+    .unwrap();
+
+    // It should succeed to unwrap 2_000_000_000 lamports.
+
+    let result = mollusk().process_and_validate_instruction(
+        &instruction,
+        &[
+            (source_account_key, source_account),
+            (destination_account_key, destination_account),
+            (authority_key, Account::default()),
+        ],
+        &[
+            Check::success(),
+            Check::account(&destination_account_key)
+                .lamports(
+                    Rent::default().minimum_balance(size_of::<TokenAccount>()) + 2_000_000_000,
+                )
+                .build(),
+            Check::account(&source_account_key)
+                .lamports(Rent::default().minimum_balance(size_of::<TokenAccount>()))
+                .build(),
+        ],
+    );
+
+    // And the remaining amount on the source account must be 0.
+
+    let account = result.get_account(&source_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+
+    // And the amount on the destination account must be 0 since we transferred
+    // lamports directly to the account.
+
+    let account = result.get_account(&destination_account_key);
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&account.data).unwrap();
+    assert_eq!(token_account.amount, 0);
+}