From dfb7febe057bb76884d5e11d1d1774d68feb0127 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 10 Jan 2024 11:59:49 -0600 Subject: [PATCH] transfer hook: add new onchain helper --- .../program-2022-test/tests/transfer_hook.rs | 4 +- token/program-2022/src/onchain.rs | 4 +- token/transfer-hook/interface/src/onchain.rs | 515 ++++++++++++++++++ 3 files changed, 520 insertions(+), 3 deletions(-) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 9a36b1f5b93..b44375f97ee 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -27,7 +27,9 @@ use { processor::Processor, }, spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_transfer_hook_interface::{get_extra_account_metas_address, offchain::add_extra_account_metas_for_execute}, + spl_transfer_hook_interface::{ + get_extra_account_metas_address, offchain::add_extra_account_metas_for_execute, + }, std::{convert::TryInto, sync::Arc}, }; diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index a174b499aa8..c6056ceae9b 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -11,7 +11,6 @@ use { account_info::AccountInfo, entrypoint::ProgramResult, instruction::AccountMeta, program::invoke_signed, pubkey::Pubkey, }, - spl_transfer_hook_interface::onchain::add_cpi_accounts_for_execute, }; /// Helper to CPI into token-2022 on-chain, looking through the additional @@ -62,7 +61,8 @@ pub fn invoke_transfer_checked<'a>( 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_cpi_accounts_for_execute( + #[allow(deprecated)] + spl_transfer_hook_interface::onchain::add_cpi_accounts_for_execute( &mut cpi_instruction, &mut cpi_account_infos, mint_info.key, diff --git a/token/transfer-hook/interface/src/onchain.rs b/token/transfer-hook/interface/src/onchain.rs index 7eb52e3752a..99e6961f284 100644 --- a/token/transfer-hook/interface/src/onchain.rs +++ b/token/transfer-hook/interface/src/onchain.rs @@ -56,6 +56,10 @@ pub fn invoke_execute<'a>( /// Helper to add accounts required for the transfer-hook program on-chain, /// looking through the additional account infos to add the proper accounts +#[deprecated( + since = "0.5.0", + note = "Please use `add_extra_accounts_for_execute_cpi` instead" +)] pub fn add_cpi_accounts_for_execute<'a>( cpi_instruction: &mut Instruction, cpi_account_infos: &mut Vec>, @@ -92,3 +96,514 @@ pub fn add_cpi_accounts_for_execute<'a>( .push(AccountMeta::new_readonly(*program_id, false)); Ok(()) } + +/// Helper to add accounts required for an `ExecuteInstruction` on-chain, +/// looking through the additional account infos to add the proper accounts. +/// +/// Note this helper is designed to add the extra accounts that will be +/// required for a CPI to a transfer hook program. However, the instruction +/// being provided to this helper is for the program that will CPI to the +/// transfer hook program. Because of this, we must resolve the extra accounts +/// for the `ExecuteInstruction` CPI, then add those extra resolved accounts to +/// the provided instruction. +#[allow(clippy::too_many_arguments)] +pub fn add_extra_accounts_for_execute_cpi<'a>( + cpi_instruction: &mut Instruction, + cpi_account_infos: &mut Vec>, + program_id: &Pubkey, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + amount: u64, + additional_accounts: &[AccountInfo<'a>], +) -> ProgramResult { + let extract_account_info = |key: &Pubkey, infos: &[AccountInfo<'a>]| { + infos + .iter() + .find(|&info| info.key == key) + .ok_or(TransferHookError::IncorrectAccount) + .map(|info| info.clone()) + }; + + let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id); + + let source_info = extract_account_info(source_pubkey, cpi_account_infos)?; + let mint_info = extract_account_info(mint_pubkey, cpi_account_infos)?; + let destination_info = extract_account_info(destination_pubkey, cpi_account_infos)?; + let authority_info = extract_account_info(authority_pubkey, cpi_account_infos)?; + let program_info = extract_account_info(program_id, additional_accounts)?; + let validate_state_info = extract_account_info(&validate_state_pubkey, additional_accounts)?; + + // Check to make sure the provided keys are in the instruction + if [ + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + ] + .iter() + .any(|&key| { + !cpi_instruction + .accounts + .iter() + .any(|meta| meta.pubkey == *key) + }) { + Err(TransferHookError::IncorrectAccount)?; + } + + let mut execute_instruction = instruction::execute( + program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + &validate_state_pubkey, + amount, + ); + let mut execute_account_infos = vec![ + source_info, + mint_info, + destination_info, + authority_info, + validate_state_info.clone(), + ]; + + ExtraAccountMetaList::add_to_cpi_instruction::( + &mut execute_instruction, + &mut execute_account_infos, + &validate_state_info.try_borrow_data()?, + additional_accounts, + )?; + + // Add only the extra accounts resolved from the validation state + cpi_instruction + .accounts + .extend_from_slice(&execute_instruction.accounts[5..]); + cpi_account_infos.extend_from_slice(&execute_account_infos[5..]); + + // Add the program id and validation state account + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(*program_id, false)); + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(validate_state_pubkey, false)); + cpi_account_infos.push(program_info); + cpi_account_infos.push(validate_state_info); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::instruction::ExecuteInstruction, + solana_program::{bpf_loader_upgradeable, system_program}, + spl_tlv_account_resolution::{ + account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, + }, + }; + + const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]); + const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]); + + fn setup_validation_data() -> Vec { + let extra_metas = vec![ + ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::AccountKey { index: 0 }, // source + Seed::AccountKey { index: 2 }, // destination + Seed::AccountKey { index: 4 }, // validation state + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, + length: 8, + }, // amount + Seed::AccountKey { index: 2 }, // destination + Seed::AccountKey { index: 5 }, // extra meta 1 + Seed::AccountKey { index: 7 }, // extra meta 3 (PDA) + ], + false, + true, + ) + .unwrap(), + ]; + let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); + let mut data = vec![0u8; account_size]; + ExtraAccountMetaList::init::(&mut data, &extra_metas).unwrap(); + data + } + + #[test] + fn test_add_extra_accounts_for_execute_cpi() { + let spl_token_2022_program_id = Pubkey::new_unique(); // Mock + let transfer_hook_program_id = Pubkey::new_unique(); + + let amount = 100u64; + + let source_pubkey = Pubkey::new_unique(); + let mut source_data = vec![0; 165]; // Mock + let mut source_lamports = 0; // Mock + let source_account_info = AccountInfo::new( + &source_pubkey, + false, + true, + &mut source_lamports, + &mut source_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let mint_pubkey = Pubkey::new_unique(); + let mut mint_data = vec![0; 165]; // Mock + let mut mint_lamports = 0; // Mock + let mint_account_info = AccountInfo::new( + &mint_pubkey, + false, + true, + &mut mint_lamports, + &mut mint_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let destination_pubkey = Pubkey::new_unique(); + let mut destination_data = vec![0; 165]; // Mock + let mut destination_lamports = 0; // Mock + let destination_account_info = AccountInfo::new( + &destination_pubkey, + false, + true, + &mut destination_lamports, + &mut destination_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let authority_pubkey = Pubkey::new_unique(); + let mut authority_data = vec![]; // Mock + let mut authority_lamports = 0; // Mock + let authority_account_info = AccountInfo::new( + &authority_pubkey, + false, + true, + &mut authority_lamports, + &mut authority_data, + &system_program::ID, + false, + 0, + ); + + let validate_state_pubkey = + get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id); + + let extra_meta_1_pubkey = EXTRA_META_1; + let mut extra_meta_1_data = vec![]; // Mock + let mut extra_meta_1_lamports = 0; // Mock + let extra_meta_1_account_info = AccountInfo::new( + &extra_meta_1_pubkey, + true, + false, + &mut extra_meta_1_lamports, + &mut extra_meta_1_data, + &system_program::ID, + false, + 0, + ); + + let extra_meta_2_pubkey = EXTRA_META_2; + let mut extra_meta_2_data = vec![]; // Mock + let mut extra_meta_2_lamports = 0; // Mock + let extra_meta_2_account_info = AccountInfo::new( + &extra_meta_2_pubkey, + true, + false, + &mut extra_meta_2_lamports, + &mut extra_meta_2_data, + &system_program::ID, + false, + 0, + ); + + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + &source_pubkey.to_bytes(), + &destination_pubkey.to_bytes(), + &validate_state_pubkey.to_bytes(), + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_3_data = vec![]; // Mock + let mut extra_meta_3_lamports = 0; // Mock + let extra_meta_3_account_info = AccountInfo::new( + &extra_meta_3_pubkey, + false, + true, + &mut extra_meta_3_lamports, + &mut extra_meta_3_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_4_pubkey = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), + &destination_pubkey.to_bytes(), + &extra_meta_1_pubkey.to_bytes(), + &extra_meta_3_pubkey.to_bytes(), + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_4_data = vec![]; // Mock + let mut extra_meta_4_lamports = 0; // Mock + let extra_meta_4_account_info = AccountInfo::new( + &extra_meta_4_pubkey, + false, + true, + &mut extra_meta_4_lamports, + &mut extra_meta_4_data, + &transfer_hook_program_id, + false, + 0, + ); + + let mut validate_state_data = setup_validation_data(); + let mut validate_state_lamports = 0; // Mock + let validate_state_account_info = AccountInfo::new( + &validate_state_pubkey, + false, + true, + &mut validate_state_lamports, + &mut validate_state_data, + &transfer_hook_program_id, + false, + 0, + ); + + let mut transfer_hook_program_data = vec![]; // Mock + let mut transfer_hook_program_lamports = 0; // Mock + let transfer_hook_program_account_info = AccountInfo::new( + &transfer_hook_program_id, + false, + true, + &mut transfer_hook_program_lamports, + &mut transfer_hook_program_data, + &bpf_loader_upgradeable::ID, + false, + 0, + ); + + let mut cpi_instruction = Instruction::new_with_bytes( + spl_token_2022_program_id, + &[], + vec![ + AccountMeta::new(source_pubkey, false), + AccountMeta::new_readonly(mint_pubkey, false), + AccountMeta::new(destination_pubkey, false), + AccountMeta::new_readonly(authority_pubkey, true), + ], + ); + let mut cpi_account_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + ]; + let additional_account_infos = vec![ + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + transfer_hook_program_account_info.clone(), + validate_state_account_info.clone(), + ]; + + // Fail missing key + let mut cpi_instruction_missing_key = Instruction::new_with_bytes( + spl_token_2022_program_id, + &[], + vec![ + // source missing + AccountMeta::new_readonly(mint_pubkey, false), + AccountMeta::new(destination_pubkey, false), + AccountMeta::new_readonly(authority_pubkey, true), + ], + ); + assert_eq!( + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction_missing_key, // Missing key + &mut cpi_account_infos, + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos, + ) + .unwrap_err(), + TransferHookError::IncorrectAccount.into() + ); + + // Fail missing account info + let mut cpi_account_infos_missing_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + // destination missing + authority_account_info.clone(), + ]; + assert_eq!( + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos_missing_infos, // Missing account info + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos, + ) + .unwrap_err(), + TransferHookError::IncorrectAccount.into() + ); + + // Fail missing validation info from additional account infos + let additional_account_infos_missing_infos = vec![ + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + // validate state missing + transfer_hook_program_account_info.clone(), + ]; + assert_eq!( + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos_missing_infos, // Missing account info + ) + .unwrap_err(), + TransferHookError::IncorrectAccount.into() + ); + + // Fail missing program info from additional account infos + let additional_account_infos_missing_infos = vec![ + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + validate_state_account_info.clone(), + // transfer hook program missing + ]; + assert_eq!( + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos_missing_infos, // Missing account info + ) + .unwrap_err(), + TransferHookError::IncorrectAccount.into() + ); + + // Fail missing extra meta info from additional account infos + let additional_account_infos_missing_infos = vec![ + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + // extra meta 3 missing + extra_meta_4_account_info.clone(), + validate_state_account_info.clone(), + transfer_hook_program_account_info.clone(), + ]; + assert_eq!( + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos_missing_infos, // Missing account info + ) + .unwrap_err(), + AccountResolutionError::IncorrectAccount.into() // Note the error + ); + + // Success + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + amount, + &additional_account_infos, + ) + .unwrap(); + + let check_metas = [ + AccountMeta::new(source_pubkey, false), + AccountMeta::new_readonly(mint_pubkey, false), + AccountMeta::new(destination_pubkey, false), + AccountMeta::new_readonly(authority_pubkey, 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), + ]; + + let check_account_infos = vec![ + source_account_info, + mint_account_info, + destination_account_info, + authority_account_info, + extra_meta_1_account_info, + extra_meta_2_account_info, + extra_meta_3_account_info, + extra_meta_4_account_info, + transfer_hook_program_account_info, + validate_state_account_info, + ]; + + assert_eq!(cpi_instruction.accounts, check_metas); + for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) { + assert_eq!(a.key, b.key); + assert_eq!(a.is_signer, b.is_signer); + assert_eq!(a.is_writable, b.is_writable); + } + } +}