From 72260e48f0b9a8883f39167a7c375a57eab8def7 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:25:05 +0900 Subject: [PATCH 01/11] Added transfer_fee extension --- token/program-2022/src/offchain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 800f3dd2b31..41705a47f8f 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}, From 2ec0b9b5b497848616ce491b54b8668bcbc4e2c1 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:28:30 +0900 Subject: [PATCH 02/11] added create_transfer_checked_with_fee_instruction_with_extra_metas --- token/program-2022/src/offchain.rs | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 41705a47f8f..48ff2954b79 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -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. /// From 594b7252c9292e937b5da51bd4f8a1de8aeb8d89 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:58:45 +0900 Subject: [PATCH 03/11] add support for transfer-hook account resolution in transfer_with_fee --- token/client/src/token.rs | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 405af74c326..abd40196dd2 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 From 662545f02e4059df4cd4583baea7149fadaaf2b9 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:18:27 +0900 Subject: [PATCH 04/11] add offchain helper `invoke_transfer_checked_with_fee` --- token/program-2022/src/onchain.rs | 69 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index 255800d7877..81970f6184b 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,70 @@ 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 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) +} From d39ca6a0b68a3526dcc3a18faa385b6f7c5526a9 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Wed, 21 Aug 2024 16:59:40 +0900 Subject: [PATCH 05/11] Nit: Added better description to function --- token/program-2022/src/onchain.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index 81970f6184b..874822f00fc 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -80,7 +80,8 @@ pub fn invoke_transfer_checked<'a>( } /// Helper to CPI into token-2022 on-chain, looking through the additional -/// account infos to create the proper instruction with the proper account infos +/// 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, From 01b945cc302e5fc036ab30f576bbe3d8edec7737 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:03:48 +0900 Subject: [PATCH 06/11] add test for offchain helper `create_transfer_checked_with_fee_instruction_with_extra_metas` --- token/program-2022/src/offchain.rs | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 48ff2954b79..eda4d658620 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -384,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); + } } From e832fe3b5dafd5f674d5fa4be6e0f5831ab50c29 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:11:22 +0900 Subject: [PATCH 07/11] add `success_transfer_with_fee` test --- .../program-2022-test/tests/transfer_hook.rs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index d26511bd9fd..8af10d9a5fc 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 = 250; + /// Test program to fail transfer hook, conforms to transfer-hook-interface pub fn process_instruction_fail( _program_id: &Pubkey, @@ -261,6 +266,63 @@ 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 +} + +fn test_transfer_fee() -> TransferFee { + TransferFee { + epoch: 0.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + } +} + +fn test_transfer_fee_config() -> TransferFeeConfig { + let transfer_fee = test_transfer_fee(); + 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, + } +} + async fn setup_with_confidential_transfers( mint: Keypair, program_id: &Pubkey, @@ -549,6 +611,88 @@ 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_config = test_transfer_fee_config(); + 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(); From 56d04fd76afbfd6afeabca5b0798c264df7deeaf Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:07:02 +0900 Subject: [PATCH 08/11] add test `success_transfers_with_fee_using_onchain_helper` --- .../program-2022-test/tests/transfer_hook.rs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 8af10d9a5fc..a8a2d3e007d 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -965,6 +965,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; + + 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), + ); + 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(); From ebc47d2387d9588f5206e3e35e36aa169df1d732 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:41:47 +0900 Subject: [PATCH 09/11] Add cli test `transfer_hook_with_transfer_fee` --- token/cli/tests/command.rs | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs index 15a6e87f01e..896d5b5919b 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); From 8f0d965058bc67d131c783637ee1166f79c74ede Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:39:46 +0900 Subject: [PATCH 10/11] fix: correctly use the new onchain helper in test --- .../program-2022-test/tests/transfer_hook.rs | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index a8a2d3e007d..8732e2f16a7 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -39,7 +39,7 @@ use { }; const TEST_MAXIMUM_FEE: u64 = 10_000_000; -const TEST_FEE_BASIS_POINTS: u16 = 250; +const TEST_FEE_BASIS_POINTS: u16 = 100; /// Test program to fail transfer hook, conforms to transfer-hook-interface pub fn process_instruction_fail( @@ -144,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, @@ -973,7 +1025,7 @@ async fn success_transfers_with_fee_using_onchain_helper() { let mint_a = mint_a_keypair.pubkey(); let mint_b_keypair = Keypair::new(); let mint_b = mint_b_keypair.pubkey(); - let amount = 10; + let amount = 10_000_000_000; let transfer_fee_config_authority = Keypair::new(); let withdraw_withheld_authority = Keypair::new(); @@ -985,7 +1037,7 @@ async fn success_transfers_with_fee_using_onchain_helper() { program_test.add_program( "my_swap", swap_program_id, - processor!(process_instruction_swap), + 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); From b5ce98d8f32e841e7f1a7a25f84c5099c5740557 Mon Sep 17 00:00:00 2001 From: tonton-sol <19677766+tonton-sol@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:58:57 +0900 Subject: [PATCH 11/11] Remove unneeded helpers --- .../program-2022-test/tests/transfer_hook.rs | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 8732e2f16a7..584b88e133d 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -356,25 +356,6 @@ async fn setup_with_fee(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) context } -fn test_transfer_fee() -> TransferFee { - TransferFee { - epoch: 0.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - } -} - -fn test_transfer_fee_config() -> TransferFeeConfig { - let transfer_fee = test_transfer_fee(); - 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, - } -} - async fn setup_with_confidential_transfers( mint: Keypair, program_id: &Pubkey, @@ -682,7 +663,18 @@ async fn success_transfer_with_fee() { let (alice_account, bob_account) = setup_accounts(&token_context, Keypair::new(), Keypair::new(), alice_amount).await; - let transfer_fee_config = test_transfer_fee_config(); + 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();