safe_token_2022/extension/transfer_fee/
instruction.rs

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