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

Commit ea94e5d

Browse files
authored
transfer-hook: Relax requirement of validation account (#7099)
* transfer-hook-interface: Allow validation pubkey to be missing * token-2022: Add end-to-end test with it working
1 parent b01b4b5 commit ea94e5d

File tree

4 files changed

+179
-80
lines changed

4 files changed

+179
-80
lines changed

token/program-2022-test/tests/transfer_hook.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ pub fn process_instruction_fail(
4545
Err(ProgramError::InvalidInstructionData)
4646
}
4747

48+
/// Test program to succeed transfer hook, conforms to transfer-hook-interface
49+
pub fn process_instruction_success(
50+
_program_id: &Pubkey,
51+
_accounts: &[AccountInfo],
52+
_input: &[u8],
53+
) -> ProgramResult {
54+
Ok(())
55+
}
56+
4857
/// Test program to check signer / write downgrade for repeated accounts,
4958
/// conforms to transfer-hook-interface
5059
pub fn process_instruction_downgrade(
@@ -913,3 +922,61 @@ async fn success_confidential_transfer() {
913922
false.into()
914923
);
915924
}
925+
926+
#[tokio::test]
927+
async fn success_without_validation_account() {
928+
let authority = Pubkey::new_unique();
929+
let program_id = Pubkey::new_unique();
930+
let mint = Keypair::new();
931+
let mut program_test = ProgramTest::default();
932+
program_test.prefer_bpf(false);
933+
program_test.add_program(
934+
"spl_token_2022",
935+
spl_token_2022::id(),
936+
processor!(Processor::process),
937+
);
938+
program_test.add_program(
939+
"my_transfer_hook",
940+
program_id,
941+
processor!(process_instruction_success),
942+
);
943+
let context = program_test.start_with_context().await;
944+
let context = Arc::new(tokio::sync::Mutex::new(context));
945+
let mut context = TestContext {
946+
context,
947+
token_context: None,
948+
};
949+
context
950+
.init_token_with_mint_keypair_and_freeze_authority(
951+
mint,
952+
vec![ExtensionInitializationParams::TransferHook {
953+
authority: Some(authority),
954+
program_id: Some(program_id),
955+
}],
956+
None,
957+
)
958+
.await
959+
.unwrap();
960+
let token_context = context.token_context.take().unwrap();
961+
962+
let amount = 10;
963+
let (alice_account, bob_account) =
964+
setup_accounts(&token_context, Keypair::new(), Keypair::new(), amount).await;
965+
966+
// only add the transfer hook program id, nothing else
967+
let token = token_context
968+
.token
969+
.with_transfer_hook_accounts(vec![AccountMeta::new_readonly(program_id, false)]);
970+
token
971+
.transfer(
972+
&alice_account,
973+
&bob_account,
974+
&token_context.alice.pubkey(),
975+
amount,
976+
&[&token_context.alice],
977+
)
978+
.await
979+
.unwrap();
980+
let destination = token.get_account_info(&bob_account).await.unwrap();
981+
assert_eq!(destination.base.amount, amount);
982+
}

token/transfer-hook/interface/src/instruction.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ pub enum TransferHookInstruction {
2525
/// 1. `[]` Token mint
2626
/// 2. `[]` Destination account
2727
/// 3. `[]` Source account's owner/delegate
28-
/// 4. `[]` Validation account
29-
/// 5..5+M `[]` `M` additional accounts, written in validation account
30-
/// data
28+
/// 4. `[]` (Optional) Validation account
29+
/// 5..5+M `[]` `M` optional additional accounts, written in validation
30+
/// account data
3131
Execute {
3232
/// Amount of tokens to transfer
3333
amount: u64,
@@ -165,9 +165,11 @@ pub fn execute_with_extra_account_metas(
165165
mint_pubkey,
166166
destination_pubkey,
167167
authority_pubkey,
168-
validate_state_pubkey,
169168
amount,
170169
);
170+
instruction
171+
.accounts
172+
.push(AccountMeta::new_readonly(*validate_state_pubkey, false));
171173
instruction.accounts.extend_from_slice(additional_accounts);
172174
instruction
173175
}
@@ -180,7 +182,6 @@ pub fn execute(
180182
mint_pubkey: &Pubkey,
181183
destination_pubkey: &Pubkey,
182184
authority_pubkey: &Pubkey,
183-
validate_state_pubkey: &Pubkey,
184185
amount: u64,
185186
) -> Instruction {
186187
let data = TransferHookInstruction::Execute { amount }.pack();
@@ -189,7 +190,6 @@ pub fn execute(
189190
AccountMeta::new_readonly(*mint_pubkey, false),
190191
AccountMeta::new_readonly(*destination_pubkey, false),
191192
AccountMeta::new_readonly(*authority_pubkey, false),
192-
AccountMeta::new_readonly(*validate_state_pubkey, false),
193193
];
194194
Instruction {
195195
program_id: *program_id,

token/transfer-hook/interface/src/offchain.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ where
8585
mint_pubkey,
8686
destination_pubkey,
8787
authority_pubkey,
88-
&validate_state_pubkey,
8988
amount,
9089
);
90+
execute_instruction
91+
.accounts
92+
.push(AccountMeta::new_readonly(validate_state_pubkey, false));
9193

9294
ExtraAccountMetaList::add_to_instruction::<ExecuteInstruction, _, _>(
9395
&mut execute_instruction,

token/transfer-hook/interface/src/onchain.rs

Lines changed: 103 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,36 @@ pub fn invoke_execute<'a>(
2323
additional_accounts: &[AccountInfo<'a>],
2424
amount: u64,
2525
) -> ProgramResult {
26-
let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
27-
let validation_info = additional_accounts
28-
.iter()
29-
.find(|&x| *x.key == validation_pubkey)
30-
.ok_or(TransferHookError::IncorrectAccount)?;
3126
let mut cpi_instruction = instruction::execute(
3227
program_id,
3328
source_info.key,
3429
mint_info.key,
3530
destination_info.key,
3631
authority_info.key,
37-
&validation_pubkey,
3832
amount,
3933
);
4034

41-
let mut cpi_account_infos = vec![
42-
source_info,
43-
mint_info,
44-
destination_info,
45-
authority_info,
46-
validation_info.clone(),
47-
];
48-
ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
49-
&mut cpi_instruction,
50-
&mut cpi_account_infos,
51-
&validation_info.try_borrow_data()?,
52-
additional_accounts,
53-
)?;
35+
let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
36+
37+
let mut cpi_account_infos = vec![source_info, mint_info, destination_info, authority_info];
38+
39+
if let Some(validation_info) = additional_accounts
40+
.iter()
41+
.find(|&x| *x.key == validation_pubkey)
42+
{
43+
cpi_instruction
44+
.accounts
45+
.push(AccountMeta::new_readonly(validation_pubkey, false));
46+
cpi_account_infos.push(validation_info.clone());
47+
48+
ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
49+
&mut cpi_instruction,
50+
&mut cpi_account_infos,
51+
&validation_info.try_borrow_data()?,
52+
additional_accounts,
53+
)?;
54+
}
55+
5456
invoke(&cpi_instruction, &cpi_account_infos)
5557
}
5658

@@ -76,55 +78,60 @@ pub fn add_extra_accounts_for_execute_cpi<'a>(
7678
additional_accounts: &[AccountInfo<'a>],
7779
) -> ProgramResult {
7880
let validate_state_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
79-
let validate_state_info = additional_accounts
80-
.iter()
81-
.find(|&x| *x.key == validate_state_pubkey)
82-
.ok_or(TransferHookError::IncorrectAccount)?;
8381

8482
let program_info = additional_accounts
8583
.iter()
8684
.find(|&x| x.key == program_id)
8785
.ok_or(TransferHookError::IncorrectAccount)?;
8886

89-
let mut execute_instruction = instruction::execute(
90-
program_id,
91-
source_info.key,
92-
mint_info.key,
93-
destination_info.key,
94-
authority_info.key,
95-
&validate_state_pubkey,
96-
amount,
97-
);
98-
let mut execute_account_infos = vec![
99-
source_info,
100-
mint_info,
101-
destination_info,
102-
authority_info,
103-
validate_state_info.clone(),
104-
];
105-
106-
ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
107-
&mut execute_instruction,
108-
&mut execute_account_infos,
109-
&validate_state_info.try_borrow_data()?,
110-
additional_accounts,
111-
)?;
112-
113-
// Add only the extra accounts resolved from the validation state
114-
cpi_instruction
115-
.accounts
116-
.extend_from_slice(&execute_instruction.accounts[5..]);
117-
cpi_account_infos.extend_from_slice(&execute_account_infos[5..]);
87+
if let Some(validate_state_info) = additional_accounts
88+
.iter()
89+
.find(|&x| *x.key == validate_state_pubkey)
90+
{
91+
let mut execute_instruction = instruction::execute(
92+
program_id,
93+
source_info.key,
94+
mint_info.key,
95+
destination_info.key,
96+
authority_info.key,
97+
amount,
98+
);
99+
execute_instruction
100+
.accounts
101+
.push(AccountMeta::new_readonly(validate_state_pubkey, false));
102+
let mut execute_account_infos = vec![
103+
source_info,
104+
mint_info,
105+
destination_info,
106+
authority_info,
107+
validate_state_info.clone(),
108+
];
118109

119-
// Add the program id and validation state account
110+
ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
111+
&mut execute_instruction,
112+
&mut execute_account_infos,
113+
&validate_state_info.try_borrow_data()?,
114+
additional_accounts,
115+
)?;
116+
117+
// Add only the extra accounts resolved from the validation state
118+
cpi_instruction
119+
.accounts
120+
.extend_from_slice(&execute_instruction.accounts[5..]);
121+
cpi_account_infos.extend_from_slice(&execute_account_infos[5..]);
122+
123+
// Add the validation state account
124+
cpi_instruction
125+
.accounts
126+
.push(AccountMeta::new_readonly(validate_state_pubkey, false));
127+
cpi_account_infos.push(validate_state_info.clone());
128+
}
129+
130+
// Add the program id
120131
cpi_instruction
121132
.accounts
122133
.push(AccountMeta::new_readonly(*program_id, false));
123-
cpi_instruction
124-
.accounts
125-
.push(AccountMeta::new_readonly(validate_state_pubkey, false));
126134
cpi_account_infos.push(program_info.clone());
127-
cpi_account_infos.push(validate_state_info.clone());
128135

129136
Ok(())
130137
}
@@ -368,16 +375,18 @@ mod tests {
368375
validate_state_account_info.clone(),
369376
];
370377

371-
// Fail missing validation info from additional account infos
372-
let additional_account_infos_missing_infos = vec![
373-
extra_meta_1_account_info.clone(),
374-
extra_meta_2_account_info.clone(),
375-
extra_meta_3_account_info.clone(),
376-
extra_meta_4_account_info.clone(),
377-
// validate state missing
378-
transfer_hook_program_account_info.clone(),
379-
];
380-
assert_eq!(
378+
// Allow missing validation info from additional account infos
379+
{
380+
let additional_account_infos_missing_infos = vec![
381+
extra_meta_1_account_info.clone(),
382+
extra_meta_2_account_info.clone(),
383+
extra_meta_3_account_info.clone(),
384+
extra_meta_4_account_info.clone(),
385+
// validate state missing
386+
transfer_hook_program_account_info.clone(),
387+
];
388+
let mut cpi_instruction = cpi_instruction.clone();
389+
let mut cpi_account_infos = cpi_account_infos.clone();
381390
add_extra_accounts_for_execute_cpi(
382391
&mut cpi_instruction,
383392
&mut cpi_account_infos,
@@ -387,11 +396,32 @@ mod tests {
387396
destination_account_info.clone(),
388397
authority_account_info.clone(),
389398
amount,
390-
&additional_account_infos_missing_infos, // Missing account info
399+
&additional_account_infos_missing_infos,
391400
)
392-
.unwrap_err(),
393-
TransferHookError::IncorrectAccount.into()
394-
);
401+
.unwrap();
402+
let check_metas = [
403+
AccountMeta::new(source_pubkey, false),
404+
AccountMeta::new_readonly(mint_pubkey, false),
405+
AccountMeta::new(destination_pubkey, false),
406+
AccountMeta::new_readonly(authority_pubkey, true),
407+
AccountMeta::new_readonly(transfer_hook_program_id, false),
408+
];
409+
410+
let check_account_infos = vec![
411+
source_account_info.clone(),
412+
mint_account_info.clone(),
413+
destination_account_info.clone(),
414+
authority_account_info.clone(),
415+
transfer_hook_program_account_info.clone(),
416+
];
417+
418+
assert_eq!(cpi_instruction.accounts, check_metas);
419+
for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
420+
assert_eq!(a.key, b.key);
421+
assert_eq!(a.is_signer, b.is_signer);
422+
assert_eq!(a.is_writable, b.is_writable);
423+
}
424+
}
395425

396426
// Fail missing program info from additional account infos
397427
let additional_account_infos_missing_infos = vec![
@@ -466,8 +496,8 @@ mod tests {
466496
AccountMeta::new_readonly(EXTRA_META_2, true),
467497
AccountMeta::new(extra_meta_3_pubkey, false),
468498
AccountMeta::new(extra_meta_4_pubkey, false),
469-
AccountMeta::new_readonly(transfer_hook_program_id, false),
470499
AccountMeta::new_readonly(validate_state_pubkey, false),
500+
AccountMeta::new_readonly(transfer_hook_program_id, false),
471501
];
472502

473503
let check_account_infos = vec![
@@ -479,8 +509,8 @@ mod tests {
479509
extra_meta_2_account_info,
480510
extra_meta_3_account_info,
481511
extra_meta_4_account_info,
482-
transfer_hook_program_account_info,
483512
validate_state_account_info,
513+
transfer_hook_program_account_info,
484514
];
485515

486516
assert_eq!(cpi_instruction.accounts, check_metas);

0 commit comments

Comments
 (0)