Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

transfer-hook: Relax requirement of validation account #7099

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]);
Comment on lines +966 to +969
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe someday this can come out, too!

solana-foundation/solana-improvement-documents#163

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
Loading