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