diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs index e13ce549c09..1f8f41e1c6d 100644 --- a/token/cli/tests/command.rs +++ b/token/cli/tests/command.rs @@ -139,6 +139,7 @@ async fn main() { async_trial!(group_pointer, test_validator, payer), async_trial!(group_member_pointer, test_validator, payer), async_trial!(transfer_hook, test_validator, payer), + async_trial!(transfer_hook_with_transfer_fee, test_validator, payer), async_trial!(metadata, test_validator, payer), async_trial!(group, test_validator, payer), async_trial!(confidential_transfer_with_fee, test_validator, payer), @@ -3872,6 +3873,107 @@ async fn transfer_hook(test_validator: &TestValidator, payer: &Keypair) { assert_eq!(extension.program_id, None.try_into().unwrap()); } +async fn transfer_hook_with_transfer_fee(test_validator: &TestValidator, payer: &Keypair) { + let program_id = spl_token_2022::id(); + let mut config = test_config_with_default_signer(test_validator, payer, &program_id); + let transfer_hook_program_id = Pubkey::new_unique(); + + let transfer_fee_basis_points = 100; + let maximum_fee: u64 = 10_000_000_000; + + let result = process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--program-id", + &program_id.to_string(), + "--transfer-hook", + &transfer_hook_program_id.to_string(), + "--transfer-fee", + &transfer_fee_basis_points.to_string(), + &maximum_fee.to_string(), + ], + ) + .await; + + // Check that the transfer-hook extension is correctly configured + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + assert_eq!( + extension.program_id, + Some(transfer_hook_program_id).try_into().unwrap() + ); + + // Check that the transfer-fee extension is correctly configured + let extension = mint_state.get_extension::().unwrap(); + assert_eq!( + u16::from(extension.older_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.older_transfer_fee.maximum_fee), + maximum_fee + ); + assert_eq!( + u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.newer_transfer_fee.maximum_fee), + maximum_fee + ); + + // Make sure that parsing transfer hook accounts and expected-fee works + let blockhash = Hash::default(); + let program_client: Arc> = Arc::new( + ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), + ); + config.program_client = program_client; + + let _result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &mint.to_string(), + "100", + &Pubkey::new_unique().to_string(), + "--blockhash", + &blockhash.to_string(), + "--nonce", + &Pubkey::new_unique().to_string(), + "--nonce-authority", + &Pubkey::new_unique().to_string(), + "--sign-only", + "--mint-decimals", + &format!("{}", TEST_DECIMALS), + "--from", + &Pubkey::new_unique().to_string(), + "--owner", + &Pubkey::new_unique().to_string(), + "--transfer-hook-account", + &format!("{}:readonly", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:readonly-signer", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable-signer", Pubkey::new_unique()), + "--expected-fee", + "1", + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); +} + async fn metadata(test_validator: &TestValidator, payer: &Keypair) { let program_id = spl_token_2022::id(); let config = test_config_with_default_signer(test_validator, payer, &program_id); diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 33352f62c27..165616a645f 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1166,21 +1166,44 @@ where let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?; - self.process_ixs( - &[transfer_fee::instruction::transfer_checked_with_fee( + let fetch_account_data_fn = |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }; + + let instruction = if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { + let mut instruction = transfer_fee::instruction::transfer_checked_with_fee( &self.program_id, source, - &self.pubkey, + self.get_address(), destination, authority, &multisig_signers, amount, decimals, fee, - )?], - signing_keypairs, - ) - .await + )?; + instruction.accounts.extend(transfer_hook_accounts.clone()); + instruction + } else { + offchain::create_transfer_checked_with_fee_instruction_with_extra_metas( + &self.program_id, + source, + self.get_address(), + destination, + authority, + &multisig_signers, + amount, + decimals, + fee, + fetch_account_data_fn, + ) + .await + .map_err(|_| TokenError::AccountNotFound)? + }; + + self.process_ixs(&[instruction], signing_keypairs).await } /// Burn tokens from account diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index d26511bd9fd..584b88e133d 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -13,6 +13,7 @@ use { entrypoint::ProgramResult, instruction::{AccountMeta, Instruction, InstructionError}, program_error::ProgramError, + program_option::COption, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, @@ -23,6 +24,7 @@ use { spl_token_2022::{ error::TokenError, extension::{ + transfer_fee::{TransferFee, TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, BaseStateWithExtensions, }, @@ -36,6 +38,9 @@ use { std::{convert::TryInto, sync::Arc}, }; +const TEST_MAXIMUM_FEE: u64 = 10_000_000; +const TEST_FEE_BASIS_POINTS: u16 = 100; + /// Test program to fail transfer hook, conforms to transfer-hook-interface pub fn process_instruction_fail( _program_id: &Pubkey, @@ -139,6 +144,58 @@ pub fn process_instruction_swap( Ok(()) } +// Test program to transfer two types of tokens with transfer hooks at once with +// fees +pub fn process_instruction_swap_with_fee( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _input: &[u8], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let source_a_account_info = next_account_info(account_info_iter)?; + let mint_a_info = next_account_info(account_info_iter)?; + let destination_a_account_info = next_account_info(account_info_iter)?; + let authority_a_info = next_account_info(account_info_iter)?; + let token_program_a_info = next_account_info(account_info_iter)?; + + let source_b_account_info = next_account_info(account_info_iter)?; + let mint_b_info = next_account_info(account_info_iter)?; + let destination_b_account_info = next_account_info(account_info_iter)?; + let authority_b_info = next_account_info(account_info_iter)?; + let token_program_b_info = next_account_info(account_info_iter)?; + + let remaining_accounts = account_info_iter.as_slice(); + + onchain::invoke_transfer_checked_with_fee( + token_program_a_info.key, + source_a_account_info.clone(), + mint_a_info.clone(), + destination_a_account_info.clone(), + authority_a_info.clone(), + remaining_accounts, + 1_000_000_000, + 9, + 10_000_000, + &[], + )?; + + onchain::invoke_transfer_checked_with_fee( + token_program_b_info.key, + source_b_account_info.clone(), + mint_b_info.clone(), + destination_b_account_info.clone(), + authority_b_info.clone(), + remaining_accounts, + 1_000_000_000, + 9, + 10_000_000, + &[], + )?; + + Ok(()) +} + async fn setup_accounts( token_context: &TokenContext, alice_account: Keypair, @@ -261,6 +318,44 @@ async fn setup(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestCo context } +async fn setup_with_fee(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestContext { + let mut program_test = setup_program_test(program_id); + + let transfer_fee_config_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; + let maximum_fee = TEST_MAXIMUM_FEE; + add_validation_account(&mut program_test, &mint.pubkey(), program_id); + + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let mut context = TestContext { + context, + token_context: None, + }; + context + .init_token_with_mint_keypair_and_freeze_authority( + mint, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(*authority), + program_id: Some(*program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + context +} + async fn setup_with_confidential_transfers( mint: Keypair, program_id: &Pubkey, @@ -549,6 +644,99 @@ async fn success_transfer() { ); } +#[tokio::test] +async fn success_transfer_with_fee() { + let authority = Keypair::new(); + let program_id = Pubkey::new_unique(); + let mint_keypair = Keypair::new(); + + let maximum_fee = TEST_MAXIMUM_FEE; + let alice_amount = maximum_fee * 100; + let transfer_amount = maximum_fee; + + let token_context = setup_with_fee(mint_keypair, &program_id, &authority.pubkey()) + .await + .token_context + .take() + .unwrap(); + + let (alice_account, bob_account) = + setup_accounts(&token_context, Keypair::new(), Keypair::new(), alice_amount).await; + + let transfer_fee = TransferFee { + epoch: 0.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + }; + let transfer_fee_config = TransferFeeConfig { + transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), + withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), + withheld_amount: 0.into(), + older_transfer_fee: transfer_fee, + newer_transfer_fee: transfer_fee, + }; + let fee = transfer_fee_config + .calculate_epoch_fee(0, transfer_amount) + .unwrap(); + + token_context + .token + .transfer_with_fee( + &alice_account, + &bob_account, + &token_context.alice.pubkey(), + transfer_amount, + fee, + &[&token_context.alice], + ) + .await + .unwrap(); + + // Get the accounts' state after the transfer + let alice_state = token_context + .token + .get_account_info(&alice_account) + .await + .unwrap(); + let bob_state = token_context + .token + .get_account_info(&bob_account) + .await + .unwrap(); + + // Check that the correct amount was deducted from Alice's account + assert_eq!(alice_state.base.amount, alice_amount - transfer_amount); + + // Check the there are no tokens withheld in Alice's account + let extension = alice_state.get_extension::().unwrap(); + assert_eq!(extension.withheld_amount, 0.into()); + + // Check the fee tokens are withheld in Bobs's account + let extension = bob_state.get_extension::().unwrap(); + assert_eq!(extension.withheld_amount, fee.into()); + + // Check that the correct amount was added to Bobs's account + assert_eq!(bob_state.base.amount, transfer_amount - fee); + + // the example program checks that the transferring flag was set to true, + // so make sure that it was correctly unset by the token program + assert_eq!( + bob_state + .get_extension::() + .unwrap() + .transferring, + false.into() + ); + + assert_eq!( + alice_state + .get_extension::() + .unwrap() + .transferring, + false.into() + ); +} + #[tokio::test] async fn fail_transfer_hook_program() { let authority = Pubkey::new_unique(); @@ -821,6 +1009,150 @@ async fn success_transfers_using_onchain_helper() { .unwrap(); } +#[tokio::test] +async fn success_transfers_with_fee_using_onchain_helper() { + let authority = Pubkey::new_unique(); + let program_id = Pubkey::new_unique(); + let mint_a_keypair = Keypair::new(); + let mint_a = mint_a_keypair.pubkey(); + let mint_b_keypair = Keypair::new(); + let mint_b = mint_b_keypair.pubkey(); + let amount = 10_000_000_000; + + let transfer_fee_config_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; + let maximum_fee = TEST_MAXIMUM_FEE; + + let swap_program_id = Pubkey::new_unique(); + let mut program_test = setup_program_test(&program_id); + program_test.add_program( + "my_swap", + swap_program_id, + processor!(process_instruction_swap_with_fee), + ); + add_validation_account(&mut program_test, &mint_a, &program_id); + add_validation_account(&mut program_test, &mint_b, &program_id); + + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + let mut context_a = TestContext { + context: context.clone(), + token_context: None, + }; + context_a + .init_token_with_mint_keypair_and_freeze_authority( + mint_a_keypair, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(authority), + program_id: Some(program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + let token_a_context = context_a.token_context.unwrap(); + let (source_a_account, destination_a_account) = + setup_accounts(&token_a_context, Keypair::new(), Keypair::new(), amount).await; + let authority_a = token_a_context.alice; + let token_a = token_a_context.token; + let mut context_b = TestContext { + context, + token_context: None, + }; + context_b + .init_token_with_mint_keypair_and_freeze_authority( + mint_b_keypair, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(authority), + program_id: Some(program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + let token_b_context = context_b.token_context.unwrap(); + let (source_b_account, destination_b_account) = + setup_accounts(&token_b_context, Keypair::new(), Keypair::new(), amount).await; + let authority_b = token_b_context.alice; + let account_metas = vec![ + AccountMeta::new(source_a_account, false), + AccountMeta::new_readonly(mint_a, false), + AccountMeta::new(destination_a_account, false), + AccountMeta::new_readonly(authority_a.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(source_b_account, false), + AccountMeta::new_readonly(mint_b, false), + AccountMeta::new(destination_b_account, false), + AccountMeta::new_readonly(authority_b.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ]; + + let mut instruction = Instruction::new_with_bytes(swap_program_id, &[], account_metas); + + add_extra_account_metas_for_execute( + &mut instruction, + &program_id, + &source_a_account, + &mint_a, + &destination_a_account, + &authority_a.pubkey(), + amount, + |address| { + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) + }, + ) + .await + .unwrap(); + add_extra_account_metas_for_execute( + &mut instruction, + &program_id, + &source_b_account, + &mint_b, + &destination_b_account, + &authority_b.pubkey(), + amount, + |address| { + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) + }, + ) + .await + .unwrap(); + + token_a + .process_ixs(&[instruction], &[&authority_a, &authority_b]) + .await + .unwrap(); +} + #[tokio::test] async fn success_confidential_transfer() { let authority = Keypair::new(); diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 800f3dd2b31..eda4d658620 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -3,7 +3,7 @@ pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError}; use { crate::{ - extension::{transfer_hook, StateWithExtensions}, + extension::{transfer_fee, transfer_hook, StateWithExtensions}, state::Mint, }, solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, @@ -74,6 +74,72 @@ where Ok(transfer_instruction) } +/// Offchain helper to create a `TransferCheckedWithFee` instruction with all +/// additional required account metas for a transfer, including the ones +/// required by the transfer hook. +/// +/// To be client-agnostic and to avoid pulling in the full solana-sdk, this +/// simply takes a function that will return its data as `Future>` for +/// the given address. Can be called in the following way: +/// +/// ```rust,ignore +/// let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( +/// &spl_token_2022::id(), +/// &source, +/// &mint, +/// &destination, +/// &authority, +/// &[], +/// amount, +/// decimals, +/// fee, +/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), +/// ) +/// .await? +/// ``` +#[allow(clippy::too_many_arguments)] +pub async fn create_transfer_checked_with_fee_instruction_with_extra_metas( + token_program_id: &Pubkey, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + signer_pubkeys: &[&Pubkey], + amount: u64, + decimals: u8, + fee: u64, + fetch_account_data_fn: F, +) -> Result +where + F: Fn(Pubkey) -> Fut, + Fut: Future, +{ + let mut transfer_instruction = transfer_fee::instruction::transfer_checked_with_fee( + token_program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + signer_pubkeys, + amount, + decimals, + fee, + )?; + + add_extra_account_metas( + &mut transfer_instruction, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + amount, + fetch_account_data_fn, + ) + .await?; + + Ok(transfer_instruction) +} + /// Offchain helper to add required account metas to an instruction, including /// the ones required by the transfer hook. /// @@ -318,4 +384,103 @@ mod tests { assert_eq!(instruction.accounts, check_metas); } + + #[tokio::test] + async fn test_create_transfer_checked_with_fee_instruction_with_extra_metas() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let amount = 100u64; + let fee = 1u64; + + let validate_state_pubkey = + get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID); + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + source.as_ref(), + destination.as_ref(), + validate_state_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + let extra_meta_4_pubkey = Pubkey::find_program_address( + &[ + amount.to_le_bytes().as_ref(), + destination.as_ref(), + EXTRA_META_1.as_ref(), + extra_meta_3_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + + let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[], + amount, + DECIMALS, + fee, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + + // With additional signers + let signer_1 = Pubkey::new_unique(); + let signer_2 = Pubkey::new_unique(); + let signer_3 = Pubkey::new_unique(); + + let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[&signer_1, &signer_2, &signer_3], + amount, + DECIMALS, + fee, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, false), // False because of additional signers + AccountMeta::new_readonly(signer_1, true), + AccountMeta::new_readonly(signer_2, true), + AccountMeta::new_readonly(signer_3, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + } } diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index 255800d7877..874822f00fc 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -3,7 +3,7 @@ use { crate::{ - extension::{transfer_hook, StateWithExtensions}, + extension::{transfer_fee, transfer_hook, StateWithExtensions}, instruction, state::Mint, }, @@ -78,3 +78,71 @@ pub fn invoke_transfer_checked<'a>( invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) } + +/// Helper to CPI into token-2022 on-chain, looking through the additional +/// account infos to create the proper instruction with the fee +/// and proper account infos +#[allow(clippy::too_many_arguments)] +pub fn invoke_transfer_checked_with_fee<'a>( + token_program_id: &Pubkey, + source_info: AccountInfo<'a>, + mint_info: AccountInfo<'a>, + destination_info: AccountInfo<'a>, + authority_info: AccountInfo<'a>, + additional_accounts: &[AccountInfo<'a>], + amount: u64, + decimals: u8, + fee: u64, + seeds: &[&[&[u8]]], +) -> ProgramResult { + let mut cpi_instruction = transfer_fee::instruction::transfer_checked_with_fee( + token_program_id, + source_info.key, + mint_info.key, + destination_info.key, + authority_info.key, + &[], // add them later, to avoid unnecessary clones + amount, + decimals, + fee, + )?; + + let mut cpi_account_infos = vec![ + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + ]; + + // if it's a signer, it might be a multisig signer, throw it in! + additional_accounts + .iter() + .filter(|ai| ai.is_signer) + .for_each(|ai| { + cpi_account_infos.push(ai.clone()); + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); + }); + + // scope the borrowing to avoid a double-borrow during CPI + { + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &program_id, + source_info, + mint_info.clone(), + destination_info, + authority_info, + amount, + additional_accounts, + )?; + } + } + + invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) +}