From 14db758341400fddf5ff84a2c617f78f51ddcc67 Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 18 Jan 2024 18:54:10 +0100 Subject: [PATCH] token-2022: Execute transfer hook during confidential transfer (#6098) * Move confidential-transfer test types for reuse * token-client: Add extra accounts for conf transfers * token-2022: Execute transfer hook in conf transfer * Test transfer hook with a confidential transfer * Featurize "confidential-hook" * Add new helper for adding the account metas * Replace self.pubkey -> self.get_address() --- token/client/src/token.rs | 198 ++++++++++++------ .../tests/confidential_transfer.rs | 185 +--------------- token/program-2022-test/tests/program_test.rs | 198 +++++++++++++++++- .../program-2022-test/tests/transfer_hook.rs | 141 ++++++++++++- token/program-2022/Cargo.toml | 4 +- .../confidential_transfer/processor.rs | 66 ++++-- token/program-2022/src/offchain.rs | 59 +++++- 7 files changed, 593 insertions(+), 258 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 37ba324df53..c6c815e0632 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -2149,20 +2149,32 @@ where .new_decryptable_available_balance(transfer_amount, source_aes_key) .map_err(|_| TokenError::AccountDecryption)?; - self.process_ixs( - &confidential_transfer::instruction::transfer( - &self.program_id, - source_account, - &self.pubkey, - destination_account, - new_decryptable_available_balance, - source_authority, - &multisig_signers, - proof_location, - )?, - signing_keypairs, + let mut instructions = confidential_transfer::instruction::transfer( + &self.program_id, + source_account, + self.get_address(), + destination_account, + new_decryptable_available_balance, + source_authority, + &multisig_signers, + proof_location, + )?; + offchain::add_extra_account_metas( + &mut instructions[0], + source_account, + self.get_address(), + destination_account, + source_authority, + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&instructions, signing_keypairs).await } /// Transfer tokens confidentially using split proofs. @@ -2195,22 +2207,32 @@ where .new_decryptable_available_balance(transfer_amount, source_aes_key) .map_err(|_| TokenError::AccountDecryption)?; - self.process_ixs( - &[ - confidential_transfer::instruction::transfer_with_split_proofs( - &self.program_id, - source_account, - &self.pubkey, - destination_account, - new_decryptable_available_balance.into(), - source_authority, - context_state_accounts, - source_decrypt_handles, - )?, - ], - signing_keypairs, + let mut instruction = confidential_transfer::instruction::transfer_with_split_proofs( + &self.program_id, + source_account, + self.get_address(), + destination_account, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + source_decrypt_handles, + )?; + offchain::add_extra_account_metas( + &mut instruction, + source_account, + self.get_address(), + destination_account, + source_authority, + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&[instruction], signing_keypairs).await } /// Transfer tokens confidentially using split proofs in parallel @@ -2261,16 +2283,32 @@ where .new_decryptable_available_balance(transfer_amount, source_aes_key) .map_err(|_| TokenError::AccountDecryption)?; - let transfer_instruction = confidential_transfer::instruction::transfer_with_split_proofs( - &self.program_id, + let mut transfer_instruction = + confidential_transfer::instruction::transfer_with_split_proofs( + &self.program_id, + source_account, + self.get_address(), + destination_account, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + &source_decrypt_handles, + )?; + offchain::add_extra_account_metas( + &mut transfer_instruction, source_account, - &self.pubkey, + self.get_address(), destination_account, - new_decryptable_available_balance.into(), source_authority, - context_state_accounts, - &source_decrypt_handles, - )?; + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + ) + .await + .map_err(|_| TokenError::AccountNotFound)?; let transfer_with_equality_and_ciphertext_validity = self .create_equality_and_ciphertext_validity_proof_context_states_for_transfer_parallel( @@ -2685,17 +2723,33 @@ where // additional compute budget required for `VerifyTransferWithFee` const TRANSFER_WITH_FEE_COMPUTE_BUDGET: u32 = 500_000; + let mut instructions = confidential_transfer::instruction::transfer_with_fee( + &self.program_id, + source_account, + destination_account, + self.get_address(), + new_decryptable_available_balance, + source_authority, + &multisig_signers, + proof_location, + )?; + offchain::add_extra_account_metas( + &mut instructions[0], + source_account, + self.get_address(), + destination_account, + source_authority, + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + ) + .await + .map_err(|_| TokenError::AccountNotFound)?; self.process_ixs_with_additional_compute_budget( - &confidential_transfer::instruction::transfer_with_fee( - &self.program_id, - source_account, - destination_account, - &self.pubkey, - new_decryptable_available_balance, - source_authority, - &multisig_signers, - proof_location, - )?, + &instructions, TRANSFER_WITH_FEE_COMPUTE_BUDGET, signing_keypairs, ) @@ -2732,22 +2786,33 @@ where .new_decryptable_available_balance(transfer_amount, source_aes_key) .map_err(|_| TokenError::AccountDecryption)?; - self.process_ixs( - &[ - confidential_transfer::instruction::transfer_with_fee_and_split_proofs( - &self.program_id, - source_account, - &self.pubkey, - destination_account, - new_decryptable_available_balance.into(), - source_authority, - context_state_accounts, - source_decrypt_handles, - )?, - ], - signing_keypairs, + let mut instruction = + confidential_transfer::instruction::transfer_with_fee_and_split_proofs( + &self.program_id, + source_account, + self.get_address(), + destination_account, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + source_decrypt_handles, + )?; + offchain::add_extra_account_metas( + &mut instruction, + source_account, + self.get_address(), + destination_account, + source_authority, + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&[instruction], signing_keypairs).await } /// Transfer tokens confidentially using split proofs in parallel @@ -2823,17 +2888,32 @@ where .new_decryptable_available_balance(transfer_amount, source_aes_key) .map_err(|_| TokenError::AccountDecryption)?; - let transfer_instruction = + let mut transfer_instruction = confidential_transfer::instruction::transfer_with_fee_and_split_proofs( &self.program_id, source_account, - &self.pubkey, + self.get_address(), destination_account, new_decryptable_available_balance.into(), source_authority, context_state_accounts, &source_decrypt_handles, )?; + offchain::add_extra_account_metas( + &mut transfer_instruction, + source_account, + self.get_address(), + destination_account, + source_authority, + u64::MAX, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + ) + .await + .map_err(|_| TokenError::AccountNotFound)?; let transfer_with_equality_and_ciphertext_valdity = self .create_equality_and_ciphertext_validity_proof_context_states_for_transfer_with_fee_parallel( diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index 70658ec92eb..91a5e7fafac 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -2,7 +2,9 @@ mod program_test; use { - program_test::{TestContext, TokenContext}, + program_test::{ + ConfidentialTokenAccountBalances, ConfidentialTokenAccountMeta, TestContext, TokenContext, + }, solana_program_test::tokio, solana_sdk::{ instruction::InstructionError, @@ -36,9 +38,8 @@ use { }, }, spl_token_client::{ - client::{SendTransaction, SimulateTransaction}, proof_generation::transfer_with_fee_split_proof_data, - token::{ExtensionInitializationParams, Token, TokenError as TokenClientError}, + token::{ExtensionInitializationParams, TokenError as TokenClientError}, }, std::{convert::TryInto, mem::size_of}, }; @@ -48,181 +49,6 @@ const TEST_MAXIMUM_FEE: u64 = 100; #[cfg(feature = "zk-ops")] const TEST_FEE_BASIS_POINTS: u16 = 250; -struct ConfidentialTokenAccountMeta { - token_account: Pubkey, - elgamal_keypair: ElGamalKeypair, - aes_key: AeKey, -} - -impl ConfidentialTokenAccountMeta { - async fn new( - token: &Token, - owner: &Keypair, - maximum_pending_balance_credit_counter: Option, - require_memo: bool, - require_fee: bool, - ) -> Self - where - T: SendTransaction + SimulateTransaction, - { - let token_account_keypair = Keypair::new(); - - let mut extensions = vec![ExtensionType::ConfidentialTransferAccount]; - if require_memo { - extensions.push(ExtensionType::MemoTransfer); - } - if require_fee { - extensions.push(ExtensionType::ConfidentialTransferFeeAmount); - } - - token - .create_auxiliary_token_account_with_extension_space( - &token_account_keypair, - &owner.pubkey(), - extensions, - ) - .await - .unwrap(); - let token_account = token_account_keypair.pubkey(); - - let elgamal_keypair = - ElGamalKeypair::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - let aes_key = AeKey::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - - token - .confidential_transfer_configure_token_account( - &token_account, - &owner.pubkey(), - None, - maximum_pending_balance_credit_counter, - &elgamal_keypair, - &aes_key, - &[owner], - ) - .await - .unwrap(); - - if require_memo { - token - .enable_required_transfer_memos(&token_account, &owner.pubkey(), &[owner]) - .await - .unwrap(); - } - - Self { - token_account, - elgamal_keypair, - aes_key, - } - } - - #[allow(clippy::too_many_arguments)] - #[cfg(feature = "zk-ops")] - async fn new_with_tokens( - token: &Token, - owner: &Keypair, - maximum_pending_balance_credit_counter: Option, - require_memo: bool, - require_fee: bool, - mint_authority: &Keypair, - amount: u64, - decimals: u8, - ) -> Self - where - T: SendTransaction + SimulateTransaction, - { - let meta = Self::new( - token, - owner, - maximum_pending_balance_credit_counter, - require_memo, - require_fee, - ) - .await; - - token - .mint_to( - &meta.token_account, - &mint_authority.pubkey(), - amount, - &[mint_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_deposit( - &meta.token_account, - &owner.pubkey(), - amount, - decimals, - &[owner], - ) - .await - .unwrap(); - - token - .confidential_transfer_apply_pending_balance( - &meta.token_account, - &owner.pubkey(), - None, - meta.elgamal_keypair.secret(), - &meta.aes_key, - &[owner], - ) - .await - .unwrap(); - meta - } - - #[cfg(feature = "zk-ops")] - async fn check_balances(&self, token: &Token, expected: ConfidentialTokenAccountBalances) - where - T: SendTransaction + SimulateTransaction, - { - let state = token.get_account_info(&self.token_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - - assert_eq!( - extension - .pending_balance_lo - .decrypt(self.elgamal_keypair.secret()) - .unwrap(), - expected.pending_balance_lo, - ); - assert_eq!( - extension - .pending_balance_hi - .decrypt(self.elgamal_keypair.secret()) - .unwrap(), - expected.pending_balance_hi, - ); - assert_eq!( - extension - .available_balance - .decrypt(self.elgamal_keypair.secret()) - .unwrap(), - expected.available_balance, - ); - assert_eq!( - self.aes_key - .decrypt(&extension.decryptable_available_balance.try_into().unwrap()) - .unwrap(), - expected.decryptable_available_balance, - ); - } -} - -#[cfg(feature = "zk-ops")] -struct ConfidentialTokenAccountBalances { - pending_balance_lo: u64, - pending_balance_hi: u64, - available_balance: u64, - decryptable_available_balance: u64, -} - #[tokio::test] async fn confidential_transfer_configure_token_account() { let authority = Keypair::new(); @@ -568,12 +394,13 @@ async fn confidential_transfer_enable_disable_non_confidential_credits() { .unwrap(); assert!(bool::from(&extension.allow_non_confidential_credits)); + // transfer a different number to change the signature token .transfer( &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - 10, + 9, &[&alice], ) .await diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs index 1400285a0c0..ef106aedfb1 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -2,10 +2,24 @@ use { solana_program_test::{processor, tokio::sync::Mutex, ProgramTest, ProgramTestContext}, - solana_sdk::signer::{keypair::Keypair, Signer}, - spl_token_2022::{id, native_mint, processor::Processor}, + solana_sdk::{ + pubkey::Pubkey, + signer::{keypair::Keypair, Signer}, + }, + spl_token_2022::{ + extension::{ + confidential_transfer::ConfidentialTransferAccount, BaseStateWithExtensions, + ExtensionType, + }, + id, native_mint, + processor::Processor, + solana_zk_token_sdk::encryption::{auth_encryption::*, elgamal::*}, + }, spl_token_client::{ - client::{ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient}, + client::{ + ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, + SendTransaction, SimulateTransaction, + }, token::{ExtensionInitializationParams, Token, TokenResult}, }, std::sync::Arc, @@ -160,3 +174,181 @@ impl TestContext { pub(crate) fn keypair_clone(kp: &Keypair) -> Keypair { Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair") } + +pub(crate) struct ConfidentialTokenAccountMeta { + pub(crate) token_account: Pubkey, + pub(crate) elgamal_keypair: ElGamalKeypair, + pub(crate) aes_key: AeKey, +} + +impl ConfidentialTokenAccountMeta { + pub(crate) async fn new( + token: &Token, + owner: &Keypair, + maximum_pending_balance_credit_counter: Option, + require_memo: bool, + require_fee: bool, + ) -> Self + where + T: SendTransaction + SimulateTransaction, + { + let token_account_keypair = Keypair::new(); + + let mut extensions = vec![ExtensionType::ConfidentialTransferAccount]; + if require_memo { + extensions.push(ExtensionType::MemoTransfer); + } + if require_fee { + extensions.push(ExtensionType::ConfidentialTransferFeeAmount); + } + + token + .create_auxiliary_token_account_with_extension_space( + &token_account_keypair, + &owner.pubkey(), + extensions, + ) + .await + .unwrap(); + let token_account = token_account_keypair.pubkey(); + + let elgamal_keypair = + ElGamalKeypair::new_from_signer(owner, &token_account.to_bytes()).unwrap(); + let aes_key = AeKey::new_from_signer(owner, &token_account.to_bytes()).unwrap(); + + token + .confidential_transfer_configure_token_account( + &token_account, + &owner.pubkey(), + None, + maximum_pending_balance_credit_counter, + &elgamal_keypair, + &aes_key, + &[owner], + ) + .await + .unwrap(); + + if require_memo { + token + .enable_required_transfer_memos(&token_account, &owner.pubkey(), &[owner]) + .await + .unwrap(); + } + + Self { + token_account, + elgamal_keypair, + aes_key, + } + } + + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "zk-ops")] + pub(crate) async fn new_with_tokens( + token: &Token, + owner: &Keypair, + maximum_pending_balance_credit_counter: Option, + require_memo: bool, + require_fee: bool, + mint_authority: &Keypair, + amount: u64, + decimals: u8, + ) -> Self + where + T: SendTransaction + SimulateTransaction, + { + let meta = Self::new( + token, + owner, + maximum_pending_balance_credit_counter, + require_memo, + require_fee, + ) + .await; + + token + .mint_to( + &meta.token_account, + &mint_authority.pubkey(), + amount, + &[mint_authority], + ) + .await + .unwrap(); + + token + .confidential_transfer_deposit( + &meta.token_account, + &owner.pubkey(), + amount, + decimals, + &[owner], + ) + .await + .unwrap(); + + token + .confidential_transfer_apply_pending_balance( + &meta.token_account, + &owner.pubkey(), + None, + meta.elgamal_keypair.secret(), + &meta.aes_key, + &[owner], + ) + .await + .unwrap(); + meta + } + + #[cfg(feature = "zk-ops")] + pub(crate) async fn check_balances( + &self, + token: &Token, + expected: ConfidentialTokenAccountBalances, + ) where + T: SendTransaction + SimulateTransaction, + { + let state = token.get_account_info(&self.token_account).await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + + assert_eq!( + extension + .pending_balance_lo + .decrypt(self.elgamal_keypair.secret()) + .unwrap(), + expected.pending_balance_lo, + ); + assert_eq!( + extension + .pending_balance_hi + .decrypt(self.elgamal_keypair.secret()) + .unwrap(), + expected.pending_balance_hi, + ); + assert_eq!( + extension + .available_balance + .decrypt(self.elgamal_keypair.secret()) + .unwrap(), + expected.available_balance, + ); + assert_eq!( + self.aes_key + .decrypt(&extension.decryptable_available_balance.try_into().unwrap()) + .unwrap(), + expected.decryptable_available_balance, + ); + } +} + +#[cfg(feature = "zk-ops")] +pub(crate) struct ConfidentialTokenAccountBalances { + pub(crate) pending_balance_lo: u64, + pub(crate) pending_balance_hi: u64, + pub(crate) available_balance: u64, + pub(crate) decryptable_available_balance: u64, +} diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 7d817577477..5d9f301591e 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -3,7 +3,9 @@ mod program_test; use { futures_util::TryFutureExt, - program_test::{TestContext, TokenContext}, + program_test::{ + ConfidentialTokenAccountBalances, ConfidentialTokenAccountMeta, TestContext, TokenContext, + }, solana_program_test::{processor, tokio, ProgramTest}, solana_sdk::{ account::Account, @@ -250,6 +252,41 @@ async fn setup(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestCo context } +async fn setup_with_confidential_transfers( + mint: Keypair, + program_id: &Pubkey, + authority: &Pubkey, +) -> TestContext { + let mut program_test = setup_program_test(program_id); + 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::ConfidentialTransferMint { + authority: Some(*authority), + auto_approve_new_accounts: true, + auditor_elgamal_pubkey: None, + }, + ], + None, + ) + .await + .unwrap(); + context +} + #[tokio::test] async fn success_init() { let authority = Pubkey::new_unique(); @@ -774,3 +811,105 @@ async fn success_transfers_using_onchain_helper() { .await .unwrap(); } + +#[tokio::test] +async fn success_confidential_transfer() { + let authority = Keypair::new(); + let program_id = Pubkey::new_unique(); + let mint_keypair = Keypair::new(); + let token_context = + setup_with_confidential_transfers(mint_keypair, &program_id, &authority.pubkey()) + .await + .token_context + .take() + .unwrap(); + let amount = 10; + + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = token_context; + + let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &alice, + None, + false, + false, + &mint_authority, + amount, + decimals, + ) + .await; + + let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, Some(2), false, false).await; + + token + .confidential_transfer_transfer( + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + None, + amount, + None, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + None, // auditor + &[&alice], + ) + .await + .unwrap(); + + let destination = token + .get_account_info(&bob_meta.token_account) + .await + .unwrap(); + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + bob_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: amount, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + + // 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!( + destination + .get_extension::() + .unwrap() + .transferring, + false.into() + ); + let source = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + assert_eq!( + source + .get_extension::() + .unwrap() + .transferring, + false.into() + ); +} diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index 39d14080d2b..e55a16a8cea 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -12,11 +12,13 @@ exclude = ["js/**"] no-entrypoint = [] test-sbf = [] serde-traits = ["dep:serde", "dep:serde_with", "dep:base64", "spl-pod/serde-traits"] -default = ["token-group", "zk-ops"] +default = ["confidential-hook", "token-group", "zk-ops"] # Remove this feature once the underlying syscalls are released on all networks zk-ops = [] # Remove this feature once the token group implementation has been audited token-group = [] +# Remove this feature once the confidential transfer hook has been audited +confidential-hook = [] [dependencies] arrayref = "0.3.7" diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 71359673d76..13e63bf5566 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -1,4 +1,6 @@ // Remove feature once zk ops syscalls are enabled on all networks +#[cfg(feature = "confidential-hook")] +use crate::extension::transfer_hook; #[cfg(feature = "zk-ops")] use { crate::extension::non_transferable::NonTransferable, @@ -445,11 +447,11 @@ fn process_transfer( let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; - let destination_token_account_info = next_account_info(account_info_iter)?; + let destination_account_info = next_account_info(account_info_iter)?; check_program_account(mint_info.owner)?; - let mint_data = &mint_info.data.borrow_mut(); - let mint = StateWithExtensions::::unpack(mint_data)?; + let mint_data = mint_info.data.borrow_mut(); + let mint = StateWithExtensions::::unpack(&mint_data)?; if mint.get_extension::().is_ok() { return Err(TokenError::NonTransferable.into()); @@ -466,7 +468,7 @@ fn process_transfer( // - If the mint is extended for fees and the instruction is not a // self-transfer, then // transfer fee is required. - if mint.get_extension::().is_err() { + let authority_info = if mint.get_extension::().is_err() { // Transfer fee is not required. Decode the zero-knowledge proof as // `TransferData`. // @@ -521,7 +523,7 @@ fn process_transfer( )?; process_destination_for_transfer( - destination_token_account_info, + destination_account_info, mint_info, maybe_proof_context.as_ref(), )?; @@ -532,6 +534,7 @@ fn process_transfer( executed" ); } + authority_info } else { // Transfer fee is required. let transfer_fee_config = mint.get_extension::()?; @@ -608,9 +611,9 @@ fn process_transfer( new_source_decryptable_available_balance, )?; - let is_self_transfer = source_account_info.key == destination_token_account_info.key; + let is_self_transfer = source_account_info.key == destination_account_info.key; process_destination_for_transfer_with_fee( - destination_token_account_info, + destination_account_info, mint_info, maybe_proof_context.as_ref(), is_self_transfer, @@ -621,6 +624,43 @@ fn process_transfer( "Context state not fully initialized: returning with no op; transfer is NOT yet executed" ); } + authority_info + }; + + #[cfg(feature = "confidential-hook")] + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + // set transferring flags, scope the borrow to avoid double-borrow during CPI + { + let mut source_account_data = source_account_info.data.borrow_mut(); + let mut source_account = + StateWithExtensionsMut::::unpack(&mut source_account_data)?; + transfer_hook::set_transferring(&mut source_account)?; + } + { + let mut destination_account_data = destination_account_info.data.borrow_mut(); + let mut destination_account = + StateWithExtensionsMut::::unpack(&mut destination_account_data)?; + transfer_hook::set_transferring(&mut destination_account)?; + } + + // can't doubly-borrow the mint data either + drop(mint_data); + + // Since the amount is unknown during a confidential transfer, pass in + // u64::MAX as a convention. + spl_transfer_hook_interface::onchain::invoke_execute( + &program_id, + source_account_info.clone(), + mint_info.clone(), + destination_account_info.clone(), + authority_info.clone(), + account_info_iter.as_slice(), + u64::MAX, + )?; + + // unset transferring flag + transfer_hook::unset_transferring(source_account_info)?; + transfer_hook::unset_transferring(destination_account_info)?; } Ok(()) @@ -696,12 +736,12 @@ fn process_source_for_transfer( #[cfg(feature = "zk-ops")] fn process_destination_for_transfer( - destination_token_account_info: &AccountInfo, + destination_account_info: &AccountInfo, mint_info: &AccountInfo, maybe_transfer_proof_context_info: Option<&TransferProofContextInfo>, ) -> ProgramResult { - check_program_account(destination_token_account_info.owner)?; - let destination_token_account_data = &mut destination_token_account_info.data.borrow_mut(); + check_program_account(destination_account_info.owner)?; + let destination_token_account_data = &mut destination_account_info.data.borrow_mut(); let mut destination_token_account = StateWithExtensionsMut::::unpack(destination_token_account_data)?; @@ -824,13 +864,13 @@ fn process_source_for_transfer_with_fee( #[cfg(feature = "zk-ops")] fn process_destination_for_transfer_with_fee( - destination_token_account_info: &AccountInfo, + destination_account_info: &AccountInfo, mint_info: &AccountInfo, maybe_proof_context: Option<&TransferWithFeeProofContextInfo>, is_self_transfer: bool, ) -> ProgramResult { - check_program_account(destination_token_account_info.owner)?; - let destination_token_account_data = &mut destination_token_account_info.data.borrow_mut(); + check_program_account(destination_account_info.owner)?; + let destination_token_account_data = &mut destination_account_info.data.borrow_mut(); let mut destination_token_account = StateWithExtensionsMut::::unpack(destination_token_account_data)?; diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 65451772151..e4493c9ea3c 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -112,6 +112,61 @@ where decimals, )?; + 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. +/// +/// 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 mut transfer_instruction = spl_token_2022::instruction::transfer_checked( +/// &spl_token_2022::id(), +/// source_pubkey, +/// mint_pubkey, +/// destination_pubkey, +/// authority_pubkey, +/// signer_pubkeys, +/// amount, +/// decimals, +/// )?; +/// add_extra_account_metas( +/// &mut transfer_instruction, +/// source_pubkey, +/// mint_pubkey, +/// destination_pubkey, +/// authority_pubkey, +/// amount, +/// fetch_account_data_fn, +/// ).await?; +/// ``` +pub async fn add_extra_account_metas( + instruction: &mut Instruction, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + amount: u64, + fetch_account_data_fn: F, +) -> Result<(), AccountFetchError> +where + F: Fn(Pubkey) -> Fut, + Fut: Future, +{ let mint_data = fetch_account_data_fn(*mint_pubkey) .await? .ok_or(ProgramError::InvalidAccountData)?; @@ -119,7 +174,7 @@ where if let Some(program_id) = transfer_hook::get_program_id(&mint) { add_extra_account_metas_for_execute( - &mut transfer_instruction, + instruction, &program_id, source_pubkey, mint_pubkey, @@ -131,7 +186,7 @@ where .await?; } - Ok(transfer_instruction) + Ok(()) } #[cfg(test)]