diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 5d9f301591e..a10fc666ca3 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -45,6 +45,15 @@ pub fn process_instruction_fail( Err(ProgramError::InvalidInstructionData) } +/// Test program to succeed transfer hook, conforms to transfer-hook-interface +pub fn process_instruction_success( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _input: &[u8], +) -> ProgramResult { + Ok(()) +} + /// Test program to check signer / write downgrade for repeated accounts, /// conforms to transfer-hook-interface pub fn process_instruction_downgrade( @@ -913,3 +922,61 @@ async fn success_confidential_transfer() { false.into() ); } + +#[tokio::test] +async fn success_without_validation_account() { + let authority = Pubkey::new_unique(); + let program_id = Pubkey::new_unique(); + let mint = Keypair::new(); + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(false); + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(Processor::process), + ); + program_test.add_program( + "my_transfer_hook", + program_id, + processor!(process_instruction_success), + ); + 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), + }], + None, + ) + .await + .unwrap(); + let token_context = context.token_context.take().unwrap(); + + let amount = 10; + let (alice_account, bob_account) = + setup_accounts(&token_context, Keypair::new(), Keypair::new(), amount).await; + + // only add the transfer hook program id, nothing else + let token = token_context + .token + .with_transfer_hook_accounts(vec![AccountMeta::new_readonly(program_id, false)]); + token + .transfer( + &alice_account, + &bob_account, + &token_context.alice.pubkey(), + amount, + &[&token_context.alice], + ) + .await + .unwrap(); + let destination = token.get_account_info(&bob_account).await.unwrap(); + assert_eq!(destination.base.amount, amount); +} diff --git a/token/transfer-hook/interface/src/instruction.rs b/token/transfer-hook/interface/src/instruction.rs index 39639d5d646..88880546450 100644 --- a/token/transfer-hook/interface/src/instruction.rs +++ b/token/transfer-hook/interface/src/instruction.rs @@ -25,9 +25,9 @@ pub enum TransferHookInstruction { /// 1. `[]` Token mint /// 2. `[]` Destination account /// 3. `[]` Source account's owner/delegate - /// 4. `[]` Validation account - /// 5..5+M `[]` `M` additional accounts, written in validation account - /// data + /// 4. `[]` (Optional) Validation account + /// 5..5+M `[]` `M` optional additional accounts, written in validation + /// account data Execute { /// Amount of tokens to transfer amount: u64, @@ -165,9 +165,11 @@ pub fn execute_with_extra_account_metas( mint_pubkey, destination_pubkey, authority_pubkey, - validate_state_pubkey, amount, ); + instruction + .accounts + .push(AccountMeta::new_readonly(*validate_state_pubkey, false)); instruction.accounts.extend_from_slice(additional_accounts); instruction } @@ -180,7 +182,6 @@ pub fn execute( mint_pubkey: &Pubkey, destination_pubkey: &Pubkey, authority_pubkey: &Pubkey, - validate_state_pubkey: &Pubkey, amount: u64, ) -> Instruction { let data = TransferHookInstruction::Execute { amount }.pack(); @@ -189,7 +190,6 @@ pub fn execute( AccountMeta::new_readonly(*mint_pubkey, false), AccountMeta::new_readonly(*destination_pubkey, false), AccountMeta::new_readonly(*authority_pubkey, false), - AccountMeta::new_readonly(*validate_state_pubkey, false), ]; Instruction { program_id: *program_id, diff --git a/token/transfer-hook/interface/src/offchain.rs b/token/transfer-hook/interface/src/offchain.rs index 2d3b7bbdd39..50c8c57e4b7 100644 --- a/token/transfer-hook/interface/src/offchain.rs +++ b/token/transfer-hook/interface/src/offchain.rs @@ -85,9 +85,11 @@ where mint_pubkey, destination_pubkey, authority_pubkey, - &validate_state_pubkey, amount, ); + execute_instruction + .accounts + .push(AccountMeta::new_readonly(validate_state_pubkey, false)); ExtraAccountMetaList::add_to_instruction::( &mut execute_instruction, diff --git a/token/transfer-hook/interface/src/onchain.rs b/token/transfer-hook/interface/src/onchain.rs index 41c2ec1b27a..1e332579e80 100644 --- a/token/transfer-hook/interface/src/onchain.rs +++ b/token/transfer-hook/interface/src/onchain.rs @@ -23,34 +23,36 @@ pub fn invoke_execute<'a>( additional_accounts: &[AccountInfo<'a>], amount: u64, ) -> ProgramResult { - let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id); - let validation_info = additional_accounts - .iter() - .find(|&x| *x.key == validation_pubkey) - .ok_or(TransferHookError::IncorrectAccount)?; let mut cpi_instruction = instruction::execute( program_id, source_info.key, mint_info.key, destination_info.key, authority_info.key, - &validation_pubkey, amount, ); - let mut cpi_account_infos = vec![ - source_info, - mint_info, - destination_info, - authority_info, - validation_info.clone(), - ]; - ExtraAccountMetaList::add_to_cpi_instruction::( - &mut cpi_instruction, - &mut cpi_account_infos, - &validation_info.try_borrow_data()?, - additional_accounts, - )?; + let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id); + + let mut cpi_account_infos = vec![source_info, mint_info, destination_info, authority_info]; + + if let Some(validation_info) = additional_accounts + .iter() + .find(|&x| *x.key == validation_pubkey) + { + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(validation_pubkey, false)); + cpi_account_infos.push(validation_info.clone()); + + ExtraAccountMetaList::add_to_cpi_instruction::( + &mut cpi_instruction, + &mut cpi_account_infos, + &validation_info.try_borrow_data()?, + additional_accounts, + )?; + } + invoke(&cpi_instruction, &cpi_account_infos) } @@ -76,55 +78,60 @@ pub fn add_extra_accounts_for_execute_cpi<'a>( additional_accounts: &[AccountInfo<'a>], ) -> ProgramResult { let validate_state_pubkey = get_extra_account_metas_address(mint_info.key, program_id); - let validate_state_info = additional_accounts - .iter() - .find(|&x| *x.key == validate_state_pubkey) - .ok_or(TransferHookError::IncorrectAccount)?; let program_info = additional_accounts .iter() .find(|&x| x.key == program_id) .ok_or(TransferHookError::IncorrectAccount)?; - let mut execute_instruction = instruction::execute( - program_id, - source_info.key, - mint_info.key, - destination_info.key, - authority_info.key, - &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..]); + if let Some(validate_state_info) = additional_accounts + .iter() + .find(|&x| *x.key == validate_state_pubkey) + { + let mut execute_instruction = instruction::execute( + program_id, + source_info.key, + mint_info.key, + destination_info.key, + authority_info.key, + amount, + ); + execute_instruction + .accounts + .push(AccountMeta::new_readonly(validate_state_pubkey, false)); + let mut execute_account_infos = vec![ + source_info, + mint_info, + destination_info, + authority_info, + validate_state_info.clone(), + ]; - // Add the program id and validation state account + 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 validation state account + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(validate_state_pubkey, false)); + cpi_account_infos.push(validate_state_info.clone()); + } + + // Add the program id 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.clone()); - cpi_account_infos.push(validate_state_info.clone()); Ok(()) } @@ -368,16 +375,18 @@ mod tests { validate_state_account_info.clone(), ]; - // 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!( + // Allow 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(), + ]; + let mut cpi_instruction = cpi_instruction.clone(); + let mut cpi_account_infos = cpi_account_infos.clone(); add_extra_accounts_for_execute_cpi( &mut cpi_instruction, &mut cpi_account_infos, @@ -387,11 +396,32 @@ mod tests { destination_account_info.clone(), authority_account_info.clone(), amount, - &additional_account_infos_missing_infos, // Missing account info + &additional_account_infos_missing_infos, ) - .unwrap_err(), - TransferHookError::IncorrectAccount.into() - ); + .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(transfer_hook_program_id, false), + ]; + + let check_account_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + transfer_hook_program_account_info.clone(), + ]; + + 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); + } + } // Fail missing program info from additional account infos let additional_account_infos_missing_infos = vec![ @@ -466,8 +496,8 @@ mod tests { 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), + AccountMeta::new_readonly(transfer_hook_program_id, false), ]; let check_account_infos = vec![ @@ -479,8 +509,8 @@ mod tests { extra_meta_2_account_info, extra_meta_3_account_info, extra_meta_4_account_info, - transfer_hook_program_account_info, validate_state_account_info, + transfer_hook_program_account_info, ]; assert_eq!(cpi_instruction.accounts, check_metas);