spl_token_2022/extension/transfer_fee/
instruction.rs

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