Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
transfer-hook: Relax requirement of validation account (#7099)
Browse files Browse the repository at this point in the history
* transfer-hook-interface: Allow validation pubkey to be missing

* token-2022: Add end-to-end test with it working
  • Loading branch information
joncinque authored Aug 2, 2024
1 parent b01b4b5 commit ea94e5d
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 80 deletions.
67 changes: 67 additions & 0 deletions token/program-2022-test/tests/transfer_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
12 changes: 6 additions & 6 deletions token/transfer-hook/interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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();
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion token/transfer-hook/interface/src/offchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ExecuteInstruction, _, _>(
&mut execute_instruction,
Expand Down
176 changes: 103 additions & 73 deletions token/transfer-hook/interface/src/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<instruction::ExecuteInstruction>(
&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::<instruction::ExecuteInstruction>(
&mut cpi_instruction,
&mut cpi_account_infos,
&validation_info.try_borrow_data()?,
additional_accounts,
)?;
}

invoke(&cpi_instruction, &cpi_account_infos)
}

Expand All @@ -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::<instruction::ExecuteInstruction>(
&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::<instruction::ExecuteInstruction>(
&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(())
}
Expand Down Expand Up @@ -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,
Expand All @@ -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![
Expand Down Expand Up @@ -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![
Expand All @@ -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);
Expand Down

0 comments on commit ea94e5d

Please sign in to comment.