spl_transfer_hook_interface/
onchain.rs

1//! On-chain program invoke helper to perform on-chain `execute` with correct
2//! accounts
3
4use {
5    crate::{error::TransferHookError, get_extra_account_metas_address, instruction},
6    solana_account_info::AccountInfo,
7    solana_cpi::invoke,
8    solana_instruction::{AccountMeta, Instruction},
9    solana_program_error::ProgramResult,
10    solana_pubkey::Pubkey,
11    spl_tlv_account_resolution::state::ExtraAccountMetaList,
12};
13/// Helper to CPI into a transfer-hook program on-chain, looking through the
14/// additional account infos to create the proper instruction
15pub fn invoke_execute<'a>(
16    program_id: &Pubkey,
17    source_info: AccountInfo<'a>,
18    mint_info: AccountInfo<'a>,
19    destination_info: AccountInfo<'a>,
20    authority_info: AccountInfo<'a>,
21    additional_accounts: &[AccountInfo<'a>],
22    amount: u64,
23) -> ProgramResult {
24    let mut cpi_instruction = instruction::execute(
25        program_id,
26        source_info.key,
27        mint_info.key,
28        destination_info.key,
29        authority_info.key,
30        amount,
31    );
32
33    let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
34
35    let mut cpi_account_infos = vec![source_info, mint_info, destination_info, authority_info];
36
37    if let Some(validation_info) = additional_accounts
38        .iter()
39        .find(|&x| *x.key == validation_pubkey)
40    {
41        cpi_instruction
42            .accounts
43            .push(AccountMeta::new_readonly(validation_pubkey, false));
44        cpi_account_infos.push(validation_info.clone());
45
46        ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
47            &mut cpi_instruction,
48            &mut cpi_account_infos,
49            &validation_info.try_borrow_data()?,
50            additional_accounts,
51        )?;
52    }
53
54    invoke(&cpi_instruction, &cpi_account_infos)
55}
56
57/// Helper to add accounts required for an `ExecuteInstruction` on-chain,
58/// looking through the additional account infos to add the proper accounts.
59///
60/// Note this helper is designed to add the extra accounts that will be
61/// required for a CPI to a transfer hook program. However, the instruction
62/// being provided to this helper is for the program that will CPI to the
63/// transfer hook program. Because of this, we must resolve the extra accounts
64/// for the `ExecuteInstruction` CPI, then add those extra resolved accounts to
65/// the provided instruction.
66#[allow(clippy::too_many_arguments)]
67pub fn add_extra_accounts_for_execute_cpi<'a>(
68    cpi_instruction: &mut Instruction,
69    cpi_account_infos: &mut Vec<AccountInfo<'a>>,
70    program_id: &Pubkey,
71    source_info: AccountInfo<'a>,
72    mint_info: AccountInfo<'a>,
73    destination_info: AccountInfo<'a>,
74    authority_info: AccountInfo<'a>,
75    amount: u64,
76    additional_accounts: &[AccountInfo<'a>],
77) -> ProgramResult {
78    let validate_state_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
79
80    let program_info = additional_accounts
81        .iter()
82        .find(|&x| x.key == program_id)
83        .ok_or(TransferHookError::IncorrectAccount)?;
84
85    if let Some(validate_state_info) = additional_accounts
86        .iter()
87        .find(|&x| *x.key == validate_state_pubkey)
88    {
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            amount,
96        );
97        execute_instruction
98            .accounts
99            .push(AccountMeta::new_readonly(validate_state_pubkey, false));
100        let mut execute_account_infos = vec![
101            source_info,
102            mint_info,
103            destination_info,
104            authority_info,
105            validate_state_info.clone(),
106        ];
107
108        ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
109            &mut execute_instruction,
110            &mut execute_account_infos,
111            &validate_state_info.try_borrow_data()?,
112            additional_accounts,
113        )?;
114
115        // Add only the extra accounts resolved from the validation state
116        cpi_instruction
117            .accounts
118            .extend_from_slice(&execute_instruction.accounts[5..]);
119        cpi_account_infos.extend_from_slice(&execute_account_infos[5..]);
120
121        // Add the validation state account
122        cpi_instruction
123            .accounts
124            .push(AccountMeta::new_readonly(validate_state_pubkey, false));
125        cpi_account_infos.push(validate_state_info.clone());
126    }
127
128    // Add the program id
129    cpi_instruction
130        .accounts
131        .push(AccountMeta::new_readonly(*program_id, false));
132    cpi_account_infos.push(program_info.clone());
133
134    Ok(())
135}
136
137#[cfg(test)]
138mod tests {
139    use {
140        super::*,
141        crate::instruction::ExecuteInstruction,
142        solana_program::{bpf_loader_upgradeable, system_program},
143        spl_tlv_account_resolution::{
144            account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed,
145        },
146    };
147
148    const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]);
149    const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]);
150
151    fn setup_validation_data() -> Vec<u8> {
152        let extra_metas = vec![
153            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
154            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
155            ExtraAccountMeta::new_with_seeds(
156                &[
157                    Seed::AccountKey { index: 0 }, // source
158                    Seed::AccountKey { index: 2 }, // destination
159                    Seed::AccountKey { index: 4 }, // validation state
160                ],
161                false,
162                true,
163            )
164            .unwrap(),
165            ExtraAccountMeta::new_with_seeds(
166                &[
167                    Seed::InstructionData {
168                        index: 8,
169                        length: 8,
170                    }, // amount
171                    Seed::AccountKey { index: 2 }, // destination
172                    Seed::AccountKey { index: 5 }, // extra meta 1
173                    Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
174                ],
175                false,
176                true,
177            )
178            .unwrap(),
179        ];
180        let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
181        let mut data = vec![0u8; account_size];
182        ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas).unwrap();
183        data
184    }
185
186    #[test]
187    fn test_add_extra_accounts_for_execute_cpi() {
188        let spl_token_2022_program_id = Pubkey::new_unique(); // Mock
189        let transfer_hook_program_id = Pubkey::new_unique();
190
191        let amount = 100u64;
192
193        let source_pubkey = Pubkey::new_unique();
194        let mut source_data = vec![0; 165]; // Mock
195        let mut source_lamports = 0; // Mock
196        let source_account_info = AccountInfo::new(
197            &source_pubkey,
198            false,
199            true,
200            &mut source_lamports,
201            &mut source_data,
202            &spl_token_2022_program_id,
203            false,
204            0,
205        );
206
207        let mint_pubkey = Pubkey::new_unique();
208        let mut mint_data = vec![0; 165]; // Mock
209        let mut mint_lamports = 0; // Mock
210        let mint_account_info = AccountInfo::new(
211            &mint_pubkey,
212            false,
213            true,
214            &mut mint_lamports,
215            &mut mint_data,
216            &spl_token_2022_program_id,
217            false,
218            0,
219        );
220
221        let destination_pubkey = Pubkey::new_unique();
222        let mut destination_data = vec![0; 165]; // Mock
223        let mut destination_lamports = 0; // Mock
224        let destination_account_info = AccountInfo::new(
225            &destination_pubkey,
226            false,
227            true,
228            &mut destination_lamports,
229            &mut destination_data,
230            &spl_token_2022_program_id,
231            false,
232            0,
233        );
234
235        let authority_pubkey = Pubkey::new_unique();
236        let mut authority_data = vec![]; // Mock
237        let mut authority_lamports = 0; // Mock
238        let authority_account_info = AccountInfo::new(
239            &authority_pubkey,
240            false,
241            true,
242            &mut authority_lamports,
243            &mut authority_data,
244            &system_program::ID,
245            false,
246            0,
247        );
248
249        let validate_state_pubkey =
250            get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id);
251
252        let extra_meta_1_pubkey = EXTRA_META_1;
253        let mut extra_meta_1_data = vec![]; // Mock
254        let mut extra_meta_1_lamports = 0; // Mock
255        let extra_meta_1_account_info = AccountInfo::new(
256            &extra_meta_1_pubkey,
257            true,
258            false,
259            &mut extra_meta_1_lamports,
260            &mut extra_meta_1_data,
261            &system_program::ID,
262            false,
263            0,
264        );
265
266        let extra_meta_2_pubkey = EXTRA_META_2;
267        let mut extra_meta_2_data = vec![]; // Mock
268        let mut extra_meta_2_lamports = 0; // Mock
269        let extra_meta_2_account_info = AccountInfo::new(
270            &extra_meta_2_pubkey,
271            true,
272            false,
273            &mut extra_meta_2_lamports,
274            &mut extra_meta_2_data,
275            &system_program::ID,
276            false,
277            0,
278        );
279
280        let extra_meta_3_pubkey = Pubkey::find_program_address(
281            &[
282                &source_pubkey.to_bytes(),
283                &destination_pubkey.to_bytes(),
284                &validate_state_pubkey.to_bytes(),
285            ],
286            &transfer_hook_program_id,
287        )
288        .0;
289        let mut extra_meta_3_data = vec![]; // Mock
290        let mut extra_meta_3_lamports = 0; // Mock
291        let extra_meta_3_account_info = AccountInfo::new(
292            &extra_meta_3_pubkey,
293            false,
294            true,
295            &mut extra_meta_3_lamports,
296            &mut extra_meta_3_data,
297            &transfer_hook_program_id,
298            false,
299            0,
300        );
301
302        let extra_meta_4_pubkey = Pubkey::find_program_address(
303            &[
304                &amount.to_le_bytes(),
305                &destination_pubkey.to_bytes(),
306                &extra_meta_1_pubkey.to_bytes(),
307                &extra_meta_3_pubkey.to_bytes(),
308            ],
309            &transfer_hook_program_id,
310        )
311        .0;
312        let mut extra_meta_4_data = vec![]; // Mock
313        let mut extra_meta_4_lamports = 0; // Mock
314        let extra_meta_4_account_info = AccountInfo::new(
315            &extra_meta_4_pubkey,
316            false,
317            true,
318            &mut extra_meta_4_lamports,
319            &mut extra_meta_4_data,
320            &transfer_hook_program_id,
321            false,
322            0,
323        );
324
325        let mut validate_state_data = setup_validation_data();
326        let mut validate_state_lamports = 0; // Mock
327        let validate_state_account_info = AccountInfo::new(
328            &validate_state_pubkey,
329            false,
330            true,
331            &mut validate_state_lamports,
332            &mut validate_state_data,
333            &transfer_hook_program_id,
334            false,
335            0,
336        );
337
338        let mut transfer_hook_program_data = vec![]; // Mock
339        let mut transfer_hook_program_lamports = 0; // Mock
340        let transfer_hook_program_account_info = AccountInfo::new(
341            &transfer_hook_program_id,
342            false,
343            true,
344            &mut transfer_hook_program_lamports,
345            &mut transfer_hook_program_data,
346            &bpf_loader_upgradeable::ID,
347            false,
348            0,
349        );
350
351        let mut cpi_instruction = Instruction::new_with_bytes(
352            spl_token_2022_program_id,
353            &[],
354            vec![
355                AccountMeta::new(source_pubkey, false),
356                AccountMeta::new_readonly(mint_pubkey, false),
357                AccountMeta::new(destination_pubkey, false),
358                AccountMeta::new_readonly(authority_pubkey, true),
359            ],
360        );
361        let mut cpi_account_infos = vec![
362            source_account_info.clone(),
363            mint_account_info.clone(),
364            destination_account_info.clone(),
365            authority_account_info.clone(),
366        ];
367        let additional_account_infos = vec![
368            extra_meta_1_account_info.clone(),
369            extra_meta_2_account_info.clone(),
370            extra_meta_3_account_info.clone(),
371            extra_meta_4_account_info.clone(),
372            transfer_hook_program_account_info.clone(),
373            validate_state_account_info.clone(),
374        ];
375
376        // Allow missing validation info from additional account infos
377        {
378            let additional_account_infos_missing_infos = vec![
379                extra_meta_1_account_info.clone(),
380                extra_meta_2_account_info.clone(),
381                extra_meta_3_account_info.clone(),
382                extra_meta_4_account_info.clone(),
383                // validate state missing
384                transfer_hook_program_account_info.clone(),
385            ];
386            let mut cpi_instruction = cpi_instruction.clone();
387            let mut cpi_account_infos = cpi_account_infos.clone();
388            add_extra_accounts_for_execute_cpi(
389                &mut cpi_instruction,
390                &mut cpi_account_infos,
391                &transfer_hook_program_id,
392                source_account_info.clone(),
393                mint_account_info.clone(),
394                destination_account_info.clone(),
395                authority_account_info.clone(),
396                amount,
397                &additional_account_infos_missing_infos,
398            )
399            .unwrap();
400            let check_metas = [
401                AccountMeta::new(source_pubkey, false),
402                AccountMeta::new_readonly(mint_pubkey, false),
403                AccountMeta::new(destination_pubkey, false),
404                AccountMeta::new_readonly(authority_pubkey, true),
405                AccountMeta::new_readonly(transfer_hook_program_id, false),
406            ];
407
408            let check_account_infos = vec![
409                source_account_info.clone(),
410                mint_account_info.clone(),
411                destination_account_info.clone(),
412                authority_account_info.clone(),
413                transfer_hook_program_account_info.clone(),
414            ];
415
416            assert_eq!(cpi_instruction.accounts, check_metas);
417            for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
418                assert_eq!(a.key, b.key);
419                assert_eq!(a.is_signer, b.is_signer);
420                assert_eq!(a.is_writable, b.is_writable);
421            }
422        }
423
424        // Fail missing program info from additional account infos
425        let additional_account_infos_missing_infos = vec![
426            extra_meta_1_account_info.clone(),
427            extra_meta_2_account_info.clone(),
428            extra_meta_3_account_info.clone(),
429            extra_meta_4_account_info.clone(),
430            validate_state_account_info.clone(),
431            // transfer hook program missing
432        ];
433        assert_eq!(
434            add_extra_accounts_for_execute_cpi(
435                &mut cpi_instruction,
436                &mut cpi_account_infos,
437                &transfer_hook_program_id,
438                source_account_info.clone(),
439                mint_account_info.clone(),
440                destination_account_info.clone(),
441                authority_account_info.clone(),
442                amount,
443                &additional_account_infos_missing_infos, // Missing account info
444            )
445            .unwrap_err(),
446            TransferHookError::IncorrectAccount.into()
447        );
448
449        // Fail missing extra meta info from additional account infos
450        let additional_account_infos_missing_infos = vec![
451            extra_meta_1_account_info.clone(),
452            extra_meta_2_account_info.clone(),
453            // extra meta 3 missing
454            extra_meta_4_account_info.clone(),
455            validate_state_account_info.clone(),
456            transfer_hook_program_account_info.clone(),
457        ];
458        assert_eq!(
459            add_extra_accounts_for_execute_cpi(
460                &mut cpi_instruction,
461                &mut cpi_account_infos,
462                &transfer_hook_program_id,
463                source_account_info.clone(),
464                mint_account_info.clone(),
465                destination_account_info.clone(),
466                authority_account_info.clone(),
467                amount,
468                &additional_account_infos_missing_infos, // Missing account info
469            )
470            .unwrap_err(),
471            AccountResolutionError::IncorrectAccount.into() // Note the error
472        );
473
474        // Success
475        add_extra_accounts_for_execute_cpi(
476            &mut cpi_instruction,
477            &mut cpi_account_infos,
478            &transfer_hook_program_id,
479            source_account_info.clone(),
480            mint_account_info.clone(),
481            destination_account_info.clone(),
482            authority_account_info.clone(),
483            amount,
484            &additional_account_infos,
485        )
486        .unwrap();
487
488        let check_metas = [
489            AccountMeta::new(source_pubkey, false),
490            AccountMeta::new_readonly(mint_pubkey, false),
491            AccountMeta::new(destination_pubkey, false),
492            AccountMeta::new_readonly(authority_pubkey, true),
493            AccountMeta::new_readonly(EXTRA_META_1, true),
494            AccountMeta::new_readonly(EXTRA_META_2, true),
495            AccountMeta::new(extra_meta_3_pubkey, false),
496            AccountMeta::new(extra_meta_4_pubkey, false),
497            AccountMeta::new_readonly(validate_state_pubkey, false),
498            AccountMeta::new_readonly(transfer_hook_program_id, false),
499        ];
500
501        let check_account_infos = vec![
502            source_account_info,
503            mint_account_info,
504            destination_account_info,
505            authority_account_info,
506            extra_meta_1_account_info,
507            extra_meta_2_account_info,
508            extra_meta_3_account_info,
509            extra_meta_4_account_info,
510            validate_state_account_info,
511            transfer_hook_program_account_info,
512        ];
513
514        assert_eq!(cpi_instruction.accounts, check_metas);
515        for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
516            assert_eq!(a.key, b.key);
517            assert_eq!(a.is_signer, b.is_signer);
518            assert_eq!(a.is_writable, b.is_writable);
519        }
520    }
521}