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_sdk_ids::{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        );
205
206        let mint_pubkey = Pubkey::new_unique();
207        let mut mint_data = vec![0; 165]; // Mock
208        let mut mint_lamports = 0; // Mock
209        let mint_account_info = AccountInfo::new(
210            &mint_pubkey,
211            false,
212            true,
213            &mut mint_lamports,
214            &mut mint_data,
215            &spl_token_2022_program_id,
216            false,
217        );
218
219        let destination_pubkey = Pubkey::new_unique();
220        let mut destination_data = vec![0; 165]; // Mock
221        let mut destination_lamports = 0; // Mock
222        let destination_account_info = AccountInfo::new(
223            &destination_pubkey,
224            false,
225            true,
226            &mut destination_lamports,
227            &mut destination_data,
228            &spl_token_2022_program_id,
229            false,
230        );
231
232        let authority_pubkey = Pubkey::new_unique();
233        let mut authority_data = vec![]; // Mock
234        let mut authority_lamports = 0; // Mock
235        let authority_account_info = AccountInfo::new(
236            &authority_pubkey,
237            false,
238            true,
239            &mut authority_lamports,
240            &mut authority_data,
241            &system_program::ID,
242            false,
243        );
244
245        let validate_state_pubkey =
246            get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id);
247
248        let extra_meta_1_pubkey = EXTRA_META_1;
249        let mut extra_meta_1_data = vec![]; // Mock
250        let mut extra_meta_1_lamports = 0; // Mock
251        let extra_meta_1_account_info = AccountInfo::new(
252            &extra_meta_1_pubkey,
253            true,
254            false,
255            &mut extra_meta_1_lamports,
256            &mut extra_meta_1_data,
257            &system_program::ID,
258            false,
259        );
260
261        let extra_meta_2_pubkey = EXTRA_META_2;
262        let mut extra_meta_2_data = vec![]; // Mock
263        let mut extra_meta_2_lamports = 0; // Mock
264        let extra_meta_2_account_info = AccountInfo::new(
265            &extra_meta_2_pubkey,
266            true,
267            false,
268            &mut extra_meta_2_lamports,
269            &mut extra_meta_2_data,
270            &system_program::ID,
271            false,
272        );
273
274        let extra_meta_3_pubkey = Pubkey::find_program_address(
275            &[
276                &source_pubkey.to_bytes(),
277                &destination_pubkey.to_bytes(),
278                &validate_state_pubkey.to_bytes(),
279            ],
280            &transfer_hook_program_id,
281        )
282        .0;
283        let mut extra_meta_3_data = vec![]; // Mock
284        let mut extra_meta_3_lamports = 0; // Mock
285        let extra_meta_3_account_info = AccountInfo::new(
286            &extra_meta_3_pubkey,
287            false,
288            true,
289            &mut extra_meta_3_lamports,
290            &mut extra_meta_3_data,
291            &transfer_hook_program_id,
292            false,
293        );
294
295        let extra_meta_4_pubkey = Pubkey::find_program_address(
296            &[
297                &amount.to_le_bytes(),
298                &destination_pubkey.to_bytes(),
299                &extra_meta_1_pubkey.to_bytes(),
300                &extra_meta_3_pubkey.to_bytes(),
301            ],
302            &transfer_hook_program_id,
303        )
304        .0;
305        let mut extra_meta_4_data = vec![]; // Mock
306        let mut extra_meta_4_lamports = 0; // Mock
307        let extra_meta_4_account_info = AccountInfo::new(
308            &extra_meta_4_pubkey,
309            false,
310            true,
311            &mut extra_meta_4_lamports,
312            &mut extra_meta_4_data,
313            &transfer_hook_program_id,
314            false,
315        );
316
317        let mut validate_state_data = setup_validation_data();
318        let mut validate_state_lamports = 0; // Mock
319        let validate_state_account_info = AccountInfo::new(
320            &validate_state_pubkey,
321            false,
322            true,
323            &mut validate_state_lamports,
324            &mut validate_state_data,
325            &transfer_hook_program_id,
326            false,
327        );
328
329        let mut transfer_hook_program_data = vec![]; // Mock
330        let mut transfer_hook_program_lamports = 0; // Mock
331        let transfer_hook_program_account_info = AccountInfo::new(
332            &transfer_hook_program_id,
333            false,
334            true,
335            &mut transfer_hook_program_lamports,
336            &mut transfer_hook_program_data,
337            &bpf_loader_upgradeable::ID,
338            false,
339        );
340
341        let mut cpi_instruction = Instruction::new_with_bytes(
342            spl_token_2022_program_id,
343            &[],
344            vec![
345                AccountMeta::new(source_pubkey, false),
346                AccountMeta::new_readonly(mint_pubkey, false),
347                AccountMeta::new(destination_pubkey, false),
348                AccountMeta::new_readonly(authority_pubkey, true),
349            ],
350        );
351        let mut cpi_account_infos = vec![
352            source_account_info.clone(),
353            mint_account_info.clone(),
354            destination_account_info.clone(),
355            authority_account_info.clone(),
356        ];
357        let additional_account_infos = vec![
358            extra_meta_1_account_info.clone(),
359            extra_meta_2_account_info.clone(),
360            extra_meta_3_account_info.clone(),
361            extra_meta_4_account_info.clone(),
362            transfer_hook_program_account_info.clone(),
363            validate_state_account_info.clone(),
364        ];
365
366        // Allow missing validation info from additional account infos
367        {
368            let additional_account_infos_missing_infos = vec![
369                extra_meta_1_account_info.clone(),
370                extra_meta_2_account_info.clone(),
371                extra_meta_3_account_info.clone(),
372                extra_meta_4_account_info.clone(),
373                // validate state missing
374                transfer_hook_program_account_info.clone(),
375            ];
376            let mut cpi_instruction = cpi_instruction.clone();
377            let mut cpi_account_infos = cpi_account_infos.clone();
378            add_extra_accounts_for_execute_cpi(
379                &mut cpi_instruction,
380                &mut cpi_account_infos,
381                &transfer_hook_program_id,
382                source_account_info.clone(),
383                mint_account_info.clone(),
384                destination_account_info.clone(),
385                authority_account_info.clone(),
386                amount,
387                &additional_account_infos_missing_infos,
388            )
389            .unwrap();
390            let check_metas = [
391                AccountMeta::new(source_pubkey, false),
392                AccountMeta::new_readonly(mint_pubkey, false),
393                AccountMeta::new(destination_pubkey, false),
394                AccountMeta::new_readonly(authority_pubkey, true),
395                AccountMeta::new_readonly(transfer_hook_program_id, false),
396            ];
397
398            let check_account_infos = vec![
399                source_account_info.clone(),
400                mint_account_info.clone(),
401                destination_account_info.clone(),
402                authority_account_info.clone(),
403                transfer_hook_program_account_info.clone(),
404            ];
405
406            assert_eq!(cpi_instruction.accounts, check_metas);
407            for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
408                assert_eq!(a.key, b.key);
409                assert_eq!(a.is_signer, b.is_signer);
410                assert_eq!(a.is_writable, b.is_writable);
411            }
412        }
413
414        // Fail missing program info from additional account infos
415        let additional_account_infos_missing_infos = vec![
416            extra_meta_1_account_info.clone(),
417            extra_meta_2_account_info.clone(),
418            extra_meta_3_account_info.clone(),
419            extra_meta_4_account_info.clone(),
420            validate_state_account_info.clone(),
421            // transfer hook program missing
422        ];
423        assert_eq!(
424            add_extra_accounts_for_execute_cpi(
425                &mut cpi_instruction,
426                &mut cpi_account_infos,
427                &transfer_hook_program_id,
428                source_account_info.clone(),
429                mint_account_info.clone(),
430                destination_account_info.clone(),
431                authority_account_info.clone(),
432                amount,
433                &additional_account_infos_missing_infos, // Missing account info
434            )
435            .unwrap_err(),
436            TransferHookError::IncorrectAccount.into()
437        );
438
439        // Fail missing extra meta info from additional account infos
440        let additional_account_infos_missing_infos = vec![
441            extra_meta_1_account_info.clone(),
442            extra_meta_2_account_info.clone(),
443            // extra meta 3 missing
444            extra_meta_4_account_info.clone(),
445            validate_state_account_info.clone(),
446            transfer_hook_program_account_info.clone(),
447        ];
448        assert_eq!(
449            add_extra_accounts_for_execute_cpi(
450                &mut cpi_instruction,
451                &mut cpi_account_infos,
452                &transfer_hook_program_id,
453                source_account_info.clone(),
454                mint_account_info.clone(),
455                destination_account_info.clone(),
456                authority_account_info.clone(),
457                amount,
458                &additional_account_infos_missing_infos, // Missing account info
459            )
460            .unwrap_err(),
461            AccountResolutionError::IncorrectAccount.into() // Note the error
462        );
463
464        // Success
465        add_extra_accounts_for_execute_cpi(
466            &mut cpi_instruction,
467            &mut cpi_account_infos,
468            &transfer_hook_program_id,
469            source_account_info.clone(),
470            mint_account_info.clone(),
471            destination_account_info.clone(),
472            authority_account_info.clone(),
473            amount,
474            &additional_account_infos,
475        )
476        .unwrap();
477
478        let check_metas = [
479            AccountMeta::new(source_pubkey, false),
480            AccountMeta::new_readonly(mint_pubkey, false),
481            AccountMeta::new(destination_pubkey, false),
482            AccountMeta::new_readonly(authority_pubkey, true),
483            AccountMeta::new_readonly(EXTRA_META_1, true),
484            AccountMeta::new_readonly(EXTRA_META_2, true),
485            AccountMeta::new(extra_meta_3_pubkey, false),
486            AccountMeta::new(extra_meta_4_pubkey, false),
487            AccountMeta::new_readonly(validate_state_pubkey, false),
488            AccountMeta::new_readonly(transfer_hook_program_id, false),
489        ];
490
491        let check_account_infos = vec![
492            source_account_info,
493            mint_account_info,
494            destination_account_info,
495            authority_account_info,
496            extra_meta_1_account_info,
497            extra_meta_2_account_info,
498            extra_meta_3_account_info,
499            extra_meta_4_account_info,
500            validate_state_account_info,
501            transfer_hook_program_account_info,
502        ];
503
504        assert_eq!(cpi_instruction.accounts, check_metas);
505        for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
506            assert_eq!(a.key, b.key);
507            assert_eq!(a.is_signer, b.is_signer);
508            assert_eq!(a.is_writable, b.is_writable);
509        }
510    }
511}