From a4a6657c92914fe576f62fd2dc3d6dd80033776d Mon Sep 17 00:00:00 2001 From: Jon C Date: Tue, 9 Jan 2024 23:11:58 +0100 Subject: [PATCH 1/7] Move confidential-transfer test types for reuse --- .../tests/confidential_transfer.rs | 185 +--------------- token/program-2022-test/tests/program_test.rs | 198 +++++++++++++++++- 2 files changed, 201 insertions(+), 182 deletions(-) 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, +} From d5cd65194e125d5fce131755dff59204e3afca2d Mon Sep 17 00:00:00 2001 From: Jon C Date: Wed, 10 Jan 2024 00:45:49 +0100 Subject: [PATCH 2/7] token-client: Add extra accounts for conf transfers --- token/client/src/token.rs | 179 ++++++++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 64 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 37ba324df53..808abe4ae14 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -2149,20 +2149,28 @@ 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.pubkey, + destination_account, + new_decryptable_available_balance, + source_authority, + &multisig_signers, + proof_location, + )?; + offchain::resolve_extra_transfer_account_metas( + &mut instructions[0], + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&instructions, signing_keypairs).await } /// Transfer tokens confidentially using split proofs. @@ -2195,22 +2203,28 @@ 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.pubkey, + destination_account, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + source_decrypt_handles, + )?; + offchain::resolve_extra_transfer_account_metas( + &mut instruction, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&[instruction], signing_keypairs).await } /// Transfer tokens confidentially using split proofs in parallel @@ -2261,16 +2275,28 @@ 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, - source_account, - &self.pubkey, - destination_account, - new_decryptable_available_balance.into(), - source_authority, - context_state_accounts, - &source_decrypt_handles, - )?; + let mut transfer_instruction = + 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, + )?; + offchain::resolve_extra_transfer_account_metas( + &mut transfer_instruction, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), + ) + .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,21 +2711,28 @@ where // additional compute budget required for `VerifyTransferWithFee` const TRANSFER_WITH_FEE_COMPUTE_BUDGET: u32 = 500_000; - 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, - )?, - TRANSFER_WITH_FEE_COMPUTE_BUDGET, - signing_keypairs, + let mut instructions = 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, + )?; + offchain::resolve_extra_transfer_account_metas( + &mut instructions[0], + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs_with_additional_compute_budget(&instructions, TRANSFER_WITH_FEE_COMPUTE_BUDGET, signing_keypairs).await } /// Transfer tokens confidentially with fee using split proofs. @@ -2732,22 +2765,29 @@ 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.pubkey, + destination_account, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + source_decrypt_handles, + )?; + offchain::resolve_extra_transfer_account_metas( + &mut instruction, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), ) .await + .map_err(|_| TokenError::AccountNotFound)?; + self.process_ixs(&[instruction], signing_keypairs).await } /// Transfer tokens confidentially using split proofs in parallel @@ -2823,7 +2863,7 @@ 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, @@ -2834,6 +2874,17 @@ where context_state_accounts, &source_decrypt_handles, )?; + offchain::resolve_extra_transfer_account_metas( + &mut transfer_instruction, + |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }, + self.get_address(), + ) + .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( From ac6ba0d5cf941086b1b1a9e96f8f0055e6447951 Mon Sep 17 00:00:00 2001 From: Jon C Date: Wed, 10 Jan 2024 00:46:38 +0100 Subject: [PATCH 3/7] token-2022: Execute transfer hook in conf transfer --- .../confidential_transfer/processor.rs | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 71359673d76..853201cf06a 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -16,7 +16,7 @@ use { }, memo_transfer::{check_previous_sibling_instruction_is_memo, memo_required}, transfer_fee::TransferFeeConfig, - BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, + transfer_hook, BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, }, instruction::{decode_instruction_data, decode_instruction_type}, processor::Processor, @@ -445,11 +445,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 +466,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 +521,7 @@ fn process_transfer( )?; process_destination_for_transfer( - destination_token_account_info, + destination_account_info, mint_info, maybe_proof_context.as_ref(), )?; @@ -532,6 +532,7 @@ fn process_transfer( executed" ); } + authority_info } else { // Transfer fee is required. let transfer_fee_config = mint.get_extension::()?; @@ -608,9 +609,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 +622,42 @@ fn process_transfer( "Context state not fully initialized: returning with no op; transfer is NOT yet executed" ); } + authority_info + }; + + 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 +733,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 +861,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)?; From 3b19d8f28bdfee76d354f9068b372daaa2852ee8 Mon Sep 17 00:00:00 2001 From: Jon C Date: Wed, 10 Jan 2024 00:50:51 +0100 Subject: [PATCH 4/7] Test transfer hook with a confidential transfer --- .../program-2022-test/tests/transfer_hook.rs | 141 +++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) 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() + ); +} From 2e7a20ad837ee34b591b043c49e15a4feb5ce103 Mon Sep 17 00:00:00 2001 From: Jon C Date: Wed, 10 Jan 2024 01:00:47 +0100 Subject: [PATCH 5/7] Featurize "confidential-hook" --- token/program-2022/Cargo.toml | 4 +++- .../src/extension/confidential_transfer/processor.rs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 853201cf06a..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, @@ -16,7 +18,7 @@ use { }, memo_transfer::{check_previous_sibling_instruction_is_memo, memo_required}, transfer_fee::TransferFeeConfig, - transfer_hook, BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, + BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, }, instruction::{decode_instruction_data, decode_instruction_type}, processor::Processor, @@ -625,6 +627,7 @@ fn process_transfer( 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 { From 52a691ece162b8086c94e4084f7a4a1ebc7c0687 Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 18 Jan 2024 02:42:06 +0100 Subject: [PATCH 6/7] Add new helper for adding the account metas --- token/client/src/token.rs | 55 +++++++++++++++++++++------- token/program-2022/src/offchain.rs | 59 +++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 808abe4ae14..f2b0a86dd70 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -2159,14 +2159,18 @@ where &multisig_signers, proof_location, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; @@ -2213,14 +2217,18 @@ where context_state_accounts, source_decrypt_handles, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; @@ -2286,14 +2294,18 @@ where context_state_accounts, &source_decrypt_handles, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; @@ -2721,18 +2733,27 @@ where &multisig_signers, proof_location, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; - self.process_ixs_with_additional_compute_budget(&instructions, TRANSFER_WITH_FEE_COMPUTE_BUDGET, signing_keypairs).await + self.process_ixs_with_additional_compute_budget( + &instructions, + TRANSFER_WITH_FEE_COMPUTE_BUDGET, + signing_keypairs, + ) + .await } /// Transfer tokens confidentially with fee using split proofs. @@ -2776,14 +2797,18 @@ where context_state_accounts, source_decrypt_handles, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; @@ -2874,14 +2899,18 @@ where context_state_accounts, &source_decrypt_handles, )?; - offchain::resolve_extra_transfer_account_metas( + 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)) }, - self.get_address(), ) .await .map_err(|_| TokenError::AccountNotFound)?; 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)] From 9da68d552ea6ddf48b634abefdb4e53e4b0a7afb Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 18 Jan 2024 17:05:21 +0100 Subject: [PATCH 7/7] Replace self.pubkey -> self.get_address() --- token/client/src/token.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index f2b0a86dd70..c6c815e0632 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -2152,7 +2152,7 @@ where let mut instructions = confidential_transfer::instruction::transfer( &self.program_id, source_account, - &self.pubkey, + self.get_address(), destination_account, new_decryptable_available_balance, source_authority, @@ -2210,7 +2210,7 @@ where let mut instruction = confidential_transfer::instruction::transfer_with_split_proofs( &self.program_id, source_account, - &self.pubkey, + self.get_address(), destination_account, new_decryptable_available_balance.into(), source_authority, @@ -2287,7 +2287,7 @@ where confidential_transfer::instruction::transfer_with_split_proofs( &self.program_id, source_account, - &self.pubkey, + self.get_address(), destination_account, new_decryptable_available_balance.into(), source_authority, @@ -2727,7 +2727,7 @@ where &self.program_id, source_account, destination_account, - &self.pubkey, + self.get_address(), new_decryptable_available_balance, source_authority, &multisig_signers, @@ -2790,7 +2790,7 @@ where 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, @@ -2892,7 +2892,7 @@ where 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,