spl_transfer_hook_example/
processor.rs

1//! Program state processor
2
3use {
4    solana_program::{
5        account_info::{next_account_info, AccountInfo},
6        entrypoint::ProgramResult,
7        msg,
8        program::invoke_signed,
9        program_error::ProgramError,
10        pubkey::Pubkey,
11        system_instruction,
12    },
13    spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList},
14    spl_token_2022::{
15        extension::{
16            transfer_hook::TransferHookAccount, BaseStateWithExtensions, StateWithExtensions,
17        },
18        state::{Account, Mint},
19    },
20    spl_transfer_hook_interface::{
21        collect_extra_account_metas_signer_seeds,
22        error::TransferHookError,
23        get_extra_account_metas_address, get_extra_account_metas_address_and_bump_seed,
24        instruction::{ExecuteInstruction, TransferHookInstruction},
25    },
26};
27
28fn check_token_account_is_transferring(account_info: &AccountInfo) -> Result<(), ProgramError> {
29    let account_data = account_info.try_borrow_data()?;
30    let token_account = StateWithExtensions::<Account>::unpack(&account_data)?;
31    let extension = token_account.get_extension::<TransferHookAccount>()?;
32    if bool::from(extension.transferring) {
33        Ok(())
34    } else {
35        Err(TransferHookError::ProgramCalledOutsideOfTransfer.into())
36    }
37}
38
39/// Processes an [Execute](enum.TransferHookInstruction.html) instruction.
40pub fn process_execute(
41    program_id: &Pubkey,
42    accounts: &[AccountInfo],
43    amount: u64,
44) -> ProgramResult {
45    let account_info_iter = &mut accounts.iter();
46
47    let source_account_info = next_account_info(account_info_iter)?;
48    let mint_info = next_account_info(account_info_iter)?;
49    let destination_account_info = next_account_info(account_info_iter)?;
50    let _authority_info = next_account_info(account_info_iter)?;
51    let extra_account_metas_info = next_account_info(account_info_iter)?;
52
53    // Check that the accounts are properly in "transferring" mode
54    check_token_account_is_transferring(source_account_info)?;
55    check_token_account_is_transferring(destination_account_info)?;
56
57    // For the example program, we just check that the correct pda and validation
58    // pubkeys are provided
59    let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
60    if expected_validation_address != *extra_account_metas_info.key {
61        return Err(ProgramError::InvalidSeeds);
62    }
63
64    let data = extra_account_metas_info.try_borrow_data()?;
65
66    ExtraAccountMetaList::check_account_infos::<ExecuteInstruction>(
67        accounts,
68        &TransferHookInstruction::Execute { amount }.pack(),
69        program_id,
70        &data,
71    )?;
72
73    Ok(())
74}
75
76/// Processes a
77/// [InitializeExtraAccountMetaList](enum.TransferHookInstruction.html)
78/// instruction.
79pub fn process_initialize_extra_account_meta_list(
80    program_id: &Pubkey,
81    accounts: &[AccountInfo],
82    extra_account_metas: &[ExtraAccountMeta],
83) -> ProgramResult {
84    let account_info_iter = &mut accounts.iter();
85
86    let extra_account_metas_info = next_account_info(account_info_iter)?;
87    let mint_info = next_account_info(account_info_iter)?;
88    let authority_info = next_account_info(account_info_iter)?;
89    let _system_program_info = next_account_info(account_info_iter)?;
90
91    // check that the one mint we want to target is trying to create extra
92    // account metas
93    #[cfg(feature = "forbid-additional-mints")]
94    if *mint_info.key != crate::mint::id() {
95        return Err(ProgramError::InvalidArgument);
96    }
97
98    // check that the mint authority is valid without fully deserializing
99    let mint_data = mint_info.try_borrow_data()?;
100    let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
101    let mint_authority = mint
102        .base
103        .mint_authority
104        .ok_or(TransferHookError::MintHasNoMintAuthority)?;
105
106    // Check signers
107    if !authority_info.is_signer {
108        return Err(ProgramError::MissingRequiredSignature);
109    }
110    if *authority_info.key != mint_authority {
111        return Err(TransferHookError::IncorrectMintAuthority.into());
112    }
113
114    // Check validation account
115    let (expected_validation_address, bump_seed) =
116        get_extra_account_metas_address_and_bump_seed(mint_info.key, program_id);
117    if expected_validation_address != *extra_account_metas_info.key {
118        return Err(ProgramError::InvalidSeeds);
119    }
120
121    // Create the account
122    let bump_seed = [bump_seed];
123    let signer_seeds = collect_extra_account_metas_signer_seeds(mint_info.key, &bump_seed);
124    let length = extra_account_metas.len();
125    let account_size = ExtraAccountMetaList::size_of(length)?;
126    invoke_signed(
127        &system_instruction::allocate(extra_account_metas_info.key, account_size as u64),
128        &[extra_account_metas_info.clone()],
129        &[&signer_seeds],
130    )?;
131    invoke_signed(
132        &system_instruction::assign(extra_account_metas_info.key, program_id),
133        &[extra_account_metas_info.clone()],
134        &[&signer_seeds],
135    )?;
136
137    // Write the data
138    let mut data = extra_account_metas_info.try_borrow_mut_data()?;
139    ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, extra_account_metas)?;
140
141    Ok(())
142}
143
144/// Processes a
145/// [UpdateExtraAccountMetaList](enum.TransferHookInstruction.html)
146/// instruction.
147pub fn process_update_extra_account_meta_list(
148    program_id: &Pubkey,
149    accounts: &[AccountInfo],
150    extra_account_metas: &[ExtraAccountMeta],
151) -> ProgramResult {
152    let account_info_iter = &mut accounts.iter();
153
154    let extra_account_metas_info = next_account_info(account_info_iter)?;
155    let mint_info = next_account_info(account_info_iter)?;
156    let authority_info = next_account_info(account_info_iter)?;
157
158    // check that the mint authority is valid without fully deserializing
159    let mint_data = mint_info.try_borrow_data()?;
160    let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
161    let mint_authority = mint
162        .base
163        .mint_authority
164        .ok_or(TransferHookError::MintHasNoMintAuthority)?;
165
166    // Check signers
167    if !authority_info.is_signer {
168        return Err(ProgramError::MissingRequiredSignature);
169    }
170    if *authority_info.key != mint_authority {
171        return Err(TransferHookError::IncorrectMintAuthority.into());
172    }
173
174    // Check validation account
175    let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
176    if expected_validation_address != *extra_account_metas_info.key {
177        return Err(ProgramError::InvalidSeeds);
178    }
179
180    // Check if the extra metas have been initialized
181    let min_account_size = ExtraAccountMetaList::size_of(0)?;
182    let original_account_size = extra_account_metas_info.data_len();
183    if program_id != extra_account_metas_info.owner || original_account_size < min_account_size {
184        return Err(ProgramError::UninitializedAccount);
185    }
186
187    // If the new extra_account_metas length is different, resize the account and
188    // update
189    let length = extra_account_metas.len();
190    let account_size = ExtraAccountMetaList::size_of(length)?;
191    if account_size >= original_account_size {
192        extra_account_metas_info.realloc(account_size, false)?;
193        let mut data = extra_account_metas_info.try_borrow_mut_data()?;
194        ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, extra_account_metas)?;
195    } else {
196        {
197            let mut data = extra_account_metas_info.try_borrow_mut_data()?;
198            ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, extra_account_metas)?;
199        }
200        extra_account_metas_info.realloc(account_size, false)?;
201    }
202
203    Ok(())
204}
205
206/// Processes an [Instruction](enum.Instruction.html).
207pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
208    let instruction = TransferHookInstruction::unpack(input)?;
209
210    match instruction {
211        TransferHookInstruction::Execute { amount } => {
212            msg!("Instruction: Execute");
213            process_execute(program_id, accounts, amount)
214        }
215        TransferHookInstruction::InitializeExtraAccountMetaList {
216            extra_account_metas,
217        } => {
218            msg!("Instruction: InitializeExtraAccountMetaList");
219            process_initialize_extra_account_meta_list(program_id, accounts, &extra_account_metas)
220        }
221        TransferHookInstruction::UpdateExtraAccountMetaList {
222            extra_account_metas,
223        } => {
224            msg!("Instruction: UpdateExtraAccountMetaList");
225            process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas)
226        }
227    }
228}