solana_extra_wasm/program/spl_token_2022/extension/transfer_fee/
instruction.rs

1use {
2    crate::program::spl_token_2022::{
3        check_program_account, error::TokenError, instruction::TokenInstruction,
4    },
5    solana_sdk::{
6        instruction::{AccountMeta, Instruction},
7        program_error::ProgramError,
8        program_option::COption,
9        pubkey::Pubkey,
10    },
11    std::convert::TryFrom,
12};
13
14/// Transfer Fee extension instructions
15#[derive(Clone, Copy, Debug, PartialEq)]
16#[repr(u8)]
17pub enum TransferFeeInstruction {
18    /// Initialize the transfer fee on a new mint.
19    ///
20    /// Fails if the mint has already been initialized, so must be called before
21    /// `InitializeMint`.
22    ///
23    /// The mint must have exactly enough space allocated for the base mint (82
24    /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type,
25    /// then space required for this extension, plus any others.
26    ///
27    /// Accounts expected by this instruction:
28    ///
29    ///   0. `[writable]` The mint to initialize.
30    InitializeTransferFeeConfig {
31        /// Pubkey that may update the fees
32        transfer_fee_config_authority: COption<Pubkey>,
33        /// Withdraw instructions must be signed by this key
34        withdraw_withheld_authority: COption<Pubkey>,
35        /// Amount of transfer collected as fees, expressed as basis points of the
36        /// transfer amount
37        transfer_fee_basis_points: u16,
38        /// Maximum fee assessed on transfers
39        maximum_fee: u64,
40    },
41    /// Transfer, providing expected mint information and fees
42    ///
43    /// Accounts expected by this instruction:
44    ///
45    ///   * Single owner/delegate
46    ///   0. `[writable]` The source account. Must include the `TransferFeeAmount` extension.
47    ///   1. `[]` The token mint. Must include the `TransferFeeConfig` extension.
48    ///   2. `[writable]` The destination account. Must include the `TransferFeeAmount` extension.
49    ///   3. `[signer]` The source account's owner/delegate.
50    ///
51    ///   * Multisignature owner/delegate
52    ///   0. `[writable]` The source account.
53    ///   1. `[]` The token mint.
54    ///   2. `[writable]` The destination account.
55    ///   3. `[]` The source account's multisignature owner/delegate.
56    ///   4. ..4+M `[signer]` M signer accounts.
57    TransferCheckedWithFee {
58        /// The amount of tokens to transfer.
59        amount: u64,
60        /// Expected number of base 10 digits to the right of the decimal place.
61        decimals: u8,
62        /// Expected fee assessed on this transfer, calculated off-chain based on
63        /// the transfer_fee_basis_points and maximum_fee of the mint.
64        fee: u64,
65    },
66    /// Transfer all withheld tokens in the mint to an account. Signed by the mint's
67    /// withdraw withheld tokens authority.
68    ///
69    /// Accounts expected by this instruction:
70    ///
71    ///   * Single owner/delegate
72    ///   0. `[writable]` The token mint. Must include the `TransferFeeConfig` extension.
73    ///   1. `[writable]` The fee receiver account. Must include the `TransferFeeAmount` extension
74    ///      associated with the provided mint.
75    ///   2. `[signer]` The mint's `withdraw_withheld_authority`.
76    ///
77    ///   * Multisignature owner/delegate
78    ///   0. `[writable]` The token mint.
79    ///   1. `[writable]` The destination account.
80    ///   2. `[]` The mint's multisig `withdraw_withheld_authority`.
81    ///   3. ..3+M `[signer]` M signer accounts.
82    WithdrawWithheldTokensFromMint,
83    /// Transfer all withheld tokens to an account. Signed by the mint's
84    /// withdraw withheld tokens authority.
85    ///
86    /// Accounts expected by this instruction:
87    ///
88    ///   * Single owner/delegate
89    ///   0. `[]` The token mint. Must include the `TransferFeeConfig` extension.
90    ///   1. `[writable]` The fee receiver account. Must include the `TransferFeeAmount`
91    ///      extension and be associated with the provided mint.
92    ///   2. `[signer]` The mint's `withdraw_withheld_authority`.
93    ///   3. ..3+N `[writable]` The source accounts to withdraw from.
94    ///
95    ///   * Multisignature owner/delegate
96    ///   0. `[]` The token mint.
97    ///   1. `[writable]` The destination account.
98    ///   2. `[]` The mint's multisig `withdraw_withheld_authority`.
99    ///   3. ..3+M `[signer]` M signer accounts.
100    ///   3+M+1. ..3+M+N `[writable]` The source accounts to withdraw from.
101    WithdrawWithheldTokensFromAccounts {
102        /// Number of token accounts harvested
103        num_token_accounts: u8,
104    },
105    /// Permissionless instruction to transfer all withheld tokens to the mint.
106    ///
107    /// Succeeds for frozen accounts.
108    ///
109    /// Accounts provided should include the `TransferFeeAmount` extension. If not,
110    /// the account is skipped.
111    ///
112    /// Accounts expected by this instruction:
113    ///
114    ///   0. `[writable]` The mint.
115    ///   1. ..1+N `[writable]` The source accounts to harvest from.
116    HarvestWithheldTokensToMint,
117    /// Set transfer fee. Only supported for mints that include the `TransferFeeConfig` extension.
118    ///
119    /// Accounts expected by this instruction:
120    ///
121    ///   * Single authority
122    ///   0. `[writable]` The mint.
123    ///   1. `[signer]` The mint's fee account owner.
124    ///
125    ///   * Multisignature authority
126    ///   0. `[writable]` The mint.
127    ///   1. `[]` The mint's multisignature fee account owner.
128    ///   2. ..2+M `[signer]` M signer accounts.
129    SetTransferFee {
130        /// Amount of transfer collected as fees, expressed as basis points of the
131        /// transfer amount
132        transfer_fee_basis_points: u16,
133        /// Maximum fee assessed on transfers
134        maximum_fee: u64,
135    },
136}
137impl TransferFeeInstruction {
138    /// Unpacks a byte buffer into a TransferFeeInstruction
139    pub fn unpack(input: &[u8]) -> Result<(Self, &[u8]), ProgramError> {
140        use TokenError::InvalidInstruction;
141
142        let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
143        Ok(match tag {
144            0 => {
145                let (transfer_fee_config_authority, rest) =
146                    TokenInstruction::unpack_pubkey_option(rest)?;
147                let (withdraw_withheld_authority, rest) =
148                    TokenInstruction::unpack_pubkey_option(rest)?;
149                let (transfer_fee_basis_points, rest) = TokenInstruction::unpack_u16(rest)?;
150                let (maximum_fee, rest) = TokenInstruction::unpack_u64(rest)?;
151                let instruction = Self::InitializeTransferFeeConfig {
152                    transfer_fee_config_authority,
153                    withdraw_withheld_authority,
154                    transfer_fee_basis_points,
155                    maximum_fee,
156                };
157                (instruction, rest)
158            }
159            1 => {
160                let (amount, decimals, rest) = TokenInstruction::unpack_amount_decimals(rest)?;
161                let (fee, rest) = TokenInstruction::unpack_u64(rest)?;
162                let instruction = Self::TransferCheckedWithFee {
163                    amount,
164                    decimals,
165                    fee,
166                };
167                (instruction, rest)
168            }
169            2 => (Self::WithdrawWithheldTokensFromMint, rest),
170            3 => {
171                let (&num_token_accounts, rest) = rest.split_first().ok_or(InvalidInstruction)?;
172                let instruction = Self::WithdrawWithheldTokensFromAccounts { num_token_accounts };
173                (instruction, rest)
174            }
175            4 => (Self::HarvestWithheldTokensToMint, rest),
176            5 => {
177                let (transfer_fee_basis_points, rest) = TokenInstruction::unpack_u16(rest)?;
178                let (maximum_fee, rest) = TokenInstruction::unpack_u64(rest)?;
179                let instruction = Self::SetTransferFee {
180                    transfer_fee_basis_points,
181                    maximum_fee,
182                };
183                (instruction, rest)
184            }
185            _ => return Err(TokenError::InvalidInstruction.into()),
186        })
187    }
188
189    /// Packs a TransferFeeInstruction into a byte buffer.
190    pub fn pack(&self, buffer: &mut Vec<u8>) {
191        match *self {
192            Self::InitializeTransferFeeConfig {
193                ref transfer_fee_config_authority,
194                ref withdraw_withheld_authority,
195                transfer_fee_basis_points,
196                maximum_fee,
197            } => {
198                buffer.push(0);
199                TokenInstruction::pack_pubkey_option(transfer_fee_config_authority, buffer);
200                TokenInstruction::pack_pubkey_option(withdraw_withheld_authority, buffer);
201                buffer.extend_from_slice(&transfer_fee_basis_points.to_le_bytes());
202                buffer.extend_from_slice(&maximum_fee.to_le_bytes());
203            }
204            Self::TransferCheckedWithFee {
205                amount,
206                decimals,
207                fee,
208            } => {
209                buffer.push(1);
210                buffer.extend_from_slice(&amount.to_le_bytes());
211                buffer.extend_from_slice(&decimals.to_le_bytes());
212                buffer.extend_from_slice(&fee.to_le_bytes());
213            }
214            Self::WithdrawWithheldTokensFromMint => {
215                buffer.push(2);
216            }
217            Self::WithdrawWithheldTokensFromAccounts { num_token_accounts } => {
218                buffer.push(3);
219                buffer.push(num_token_accounts);
220            }
221            Self::HarvestWithheldTokensToMint => {
222                buffer.push(4);
223            }
224            Self::SetTransferFee {
225                transfer_fee_basis_points,
226                maximum_fee,
227            } => {
228                buffer.push(5);
229                buffer.extend_from_slice(&transfer_fee_basis_points.to_le_bytes());
230                buffer.extend_from_slice(&maximum_fee.to_le_bytes());
231            }
232        }
233    }
234}
235
236/// Create a `InitializeTransferFeeConfig` instruction
237pub fn initialize_transfer_fee_config(
238    token_program_id: &Pubkey,
239    mint: &Pubkey,
240    transfer_fee_config_authority: Option<&Pubkey>,
241    withdraw_withheld_authority: Option<&Pubkey>,
242    transfer_fee_basis_points: u16,
243    maximum_fee: u64,
244) -> Result<Instruction, ProgramError> {
245    check_program_account(token_program_id)?;
246    let transfer_fee_config_authority = transfer_fee_config_authority.cloned().into();
247    let withdraw_withheld_authority = withdraw_withheld_authority.cloned().into();
248    let data = TokenInstruction::TransferFeeExtension(
249        TransferFeeInstruction::InitializeTransferFeeConfig {
250            transfer_fee_config_authority,
251            withdraw_withheld_authority,
252            transfer_fee_basis_points,
253            maximum_fee,
254        },
255    )
256    .pack();
257
258    Ok(Instruction {
259        program_id: *token_program_id,
260        accounts: vec![AccountMeta::new(*mint, false)],
261        data,
262    })
263}
264
265/// Create a `TransferCheckedWithFee` instruction
266#[allow(clippy::too_many_arguments)]
267pub fn transfer_checked_with_fee(
268    token_program_id: &Pubkey,
269    source: &Pubkey,
270    mint: &Pubkey,
271    destination: &Pubkey,
272    authority: &Pubkey,
273    signers: &[&Pubkey],
274    amount: u64,
275    decimals: u8,
276    fee: u64,
277) -> Result<Instruction, ProgramError> {
278    check_program_account(token_program_id)?;
279    let data =
280        TokenInstruction::TransferFeeExtension(TransferFeeInstruction::TransferCheckedWithFee {
281            amount,
282            decimals,
283            fee,
284        })
285        .pack();
286
287    let mut accounts = Vec::with_capacity(4 + signers.len());
288    accounts.push(AccountMeta::new(*source, false));
289    accounts.push(AccountMeta::new_readonly(*mint, false));
290    accounts.push(AccountMeta::new(*destination, false));
291    accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty()));
292    for signer in signers.iter() {
293        accounts.push(AccountMeta::new_readonly(**signer, true));
294    }
295
296    Ok(Instruction {
297        program_id: *token_program_id,
298        accounts,
299        data,
300    })
301}
302
303/// Creates a `WithdrawWithheldTokensFromMint` instruction
304pub fn withdraw_withheld_tokens_from_mint(
305    token_program_id: &Pubkey,
306    mint: &Pubkey,
307    destination: &Pubkey,
308    authority: &Pubkey,
309    signers: &[&Pubkey],
310) -> Result<Instruction, ProgramError> {
311    check_program_account(token_program_id)?;
312    let mut accounts = Vec::with_capacity(3 + signers.len());
313    accounts.push(AccountMeta::new(*mint, false));
314    accounts.push(AccountMeta::new(*destination, false));
315    accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty()));
316    for signer in signers.iter() {
317        accounts.push(AccountMeta::new_readonly(**signer, true));
318    }
319
320    Ok(Instruction {
321        program_id: *token_program_id,
322        accounts,
323        data: TokenInstruction::TransferFeeExtension(
324            TransferFeeInstruction::WithdrawWithheldTokensFromMint,
325        )
326        .pack(),
327    })
328}
329
330/// Creates a `WithdrawWithheldTokensFromAccounts` instruction
331pub fn withdraw_withheld_tokens_from_accounts(
332    token_program_id: &Pubkey,
333    mint: &Pubkey,
334    destination: &Pubkey,
335    authority: &Pubkey,
336    signers: &[&Pubkey],
337    sources: &[&Pubkey],
338) -> Result<Instruction, ProgramError> {
339    check_program_account(token_program_id)?;
340    let num_token_accounts =
341        u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?;
342    let mut accounts = Vec::with_capacity(3 + signers.len() + sources.len());
343    accounts.push(AccountMeta::new_readonly(*mint, false));
344    accounts.push(AccountMeta::new(*destination, false));
345    accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty()));
346    for signer in signers.iter() {
347        accounts.push(AccountMeta::new_readonly(**signer, true));
348    }
349    for source in sources.iter() {
350        accounts.push(AccountMeta::new(**source, false));
351    }
352
353    Ok(Instruction {
354        program_id: *token_program_id,
355        accounts,
356        data: TokenInstruction::TransferFeeExtension(
357            TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts },
358        )
359        .pack(),
360    })
361}
362
363/// Creates a `HarvestWithheldTokensToMint` instruction
364pub fn harvest_withheld_tokens_to_mint(
365    token_program_id: &Pubkey,
366    mint: &Pubkey,
367    sources: &[&Pubkey],
368) -> Result<Instruction, ProgramError> {
369    check_program_account(token_program_id)?;
370    let mut accounts = Vec::with_capacity(1 + sources.len());
371    accounts.push(AccountMeta::new(*mint, false));
372    for source in sources.iter() {
373        accounts.push(AccountMeta::new(**source, false));
374    }
375    Ok(Instruction {
376        program_id: *token_program_id,
377        accounts,
378        data: TokenInstruction::TransferFeeExtension(
379            TransferFeeInstruction::HarvestWithheldTokensToMint,
380        )
381        .pack(),
382    })
383}
384
385/// Creates a `SetTransferFee` instruction
386pub fn set_transfer_fee(
387    token_program_id: &Pubkey,
388    mint: &Pubkey,
389    authority: &Pubkey,
390    signers: &[&Pubkey],
391    transfer_fee_basis_points: u16,
392    maximum_fee: u64,
393) -> Result<Instruction, ProgramError> {
394    check_program_account(token_program_id)?;
395    let mut accounts = Vec::with_capacity(2 + signers.len());
396    accounts.push(AccountMeta::new(*mint, false));
397    accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty()));
398    for signer in signers.iter() {
399        accounts.push(AccountMeta::new_readonly(**signer, true));
400    }
401
402    Ok(Instruction {
403        program_id: *token_program_id,
404        accounts,
405        data: TokenInstruction::TransferFeeExtension(TransferFeeInstruction::SetTransferFee {
406            transfer_fee_basis_points,
407            maximum_fee,
408        })
409        .pack(),
410    })
411}
412
413#[cfg(test)]
414mod test {
415    use super::*;
416
417    const TRANSFER_FEE_PREFIX: u8 = 26;
418
419    #[test]
420    fn test_instruction_packing() {
421        let check = TokenInstruction::TransferFeeExtension(
422            TransferFeeInstruction::InitializeTransferFeeConfig {
423                transfer_fee_config_authority: COption::Some(Pubkey::from([11u8; 32])),
424                withdraw_withheld_authority: COption::None,
425                transfer_fee_basis_points: 111,
426                maximum_fee: u64::MAX,
427            },
428        );
429        let packed = check.pack();
430        let mut expect = vec![TRANSFER_FEE_PREFIX, 0, 1];
431        expect.extend_from_slice(&[11u8; 32]);
432        expect.extend_from_slice(&[0]);
433        expect.extend_from_slice(&111u16.to_le_bytes());
434        expect.extend_from_slice(&u64::MAX.to_le_bytes());
435        assert_eq!(packed, expect);
436        let unpacked = TokenInstruction::unpack(&expect).unwrap();
437        assert_eq!(unpacked, check);
438
439        let check = TokenInstruction::TransferFeeExtension(
440            TransferFeeInstruction::TransferCheckedWithFee {
441                amount: 24,
442                decimals: 24,
443                fee: 23,
444            },
445        );
446        let packed = check.pack();
447        let mut expect = vec![TRANSFER_FEE_PREFIX, 1];
448        expect.extend_from_slice(&24u64.to_le_bytes());
449        expect.extend_from_slice(&[24u8]);
450        expect.extend_from_slice(&23u64.to_le_bytes());
451        assert_eq!(packed, expect);
452        let unpacked = TokenInstruction::unpack(&expect).unwrap();
453        assert_eq!(unpacked, check);
454
455        let check = TokenInstruction::TransferFeeExtension(
456            TransferFeeInstruction::WithdrawWithheldTokensFromMint,
457        );
458        let packed = check.pack();
459        let expect = [TRANSFER_FEE_PREFIX, 2];
460        assert_eq!(packed, expect);
461        let unpacked = TokenInstruction::unpack(&expect).unwrap();
462        assert_eq!(unpacked, check);
463
464        let num_token_accounts = 255;
465        let check = TokenInstruction::TransferFeeExtension(
466            TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts },
467        );
468        let packed = check.pack();
469        let expect = [TRANSFER_FEE_PREFIX, 3, num_token_accounts];
470        assert_eq!(packed, expect);
471        let unpacked = TokenInstruction::unpack(&expect).unwrap();
472        assert_eq!(unpacked, check);
473
474        let check = TokenInstruction::TransferFeeExtension(
475            TransferFeeInstruction::HarvestWithheldTokensToMint,
476        );
477        let packed = check.pack();
478        let expect = [TRANSFER_FEE_PREFIX, 4];
479        assert_eq!(packed, expect);
480        let unpacked = TokenInstruction::unpack(&expect).unwrap();
481        assert_eq!(unpacked, check);
482
483        let check =
484            TokenInstruction::TransferFeeExtension(TransferFeeInstruction::SetTransferFee {
485                transfer_fee_basis_points: u16::MAX,
486                maximum_fee: u64::MAX,
487            });
488        let packed = check.pack();
489        let mut expect = vec![TRANSFER_FEE_PREFIX, 5];
490        expect.extend_from_slice(&u16::MAX.to_le_bytes());
491        expect.extend_from_slice(&u64::MAX.to_le_bytes());
492        assert_eq!(packed, expect);
493        let unpacked = TokenInstruction::unpack(&expect).unwrap();
494        assert_eq!(unpacked, check);
495    }
496}