Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

token-2022: Add transfer fee types and instructions #2608

Merged
merged 10 commits into from
Dec 14, 2021
138 changes: 138 additions & 0 deletions token/program-2022/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ pub enum TokenInstruction {
/// amounts of SOL and Tokens will be transferred to the destination
/// account.
///
/// If the accounts are `AccountWithTransferFee`s, this will fail. The mint
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// is required in order to assess the fee.
///
/// Accounts expected by this instruction:
///
/// * Single owner/delegate
Expand Down Expand Up @@ -196,6 +199,8 @@ pub enum TokenInstruction {
},
/// Close an account by transferring all its SOL to the destination account.
/// Non-native accounts may only be closed if its token amount is zero.
/// Accounts with transfer fees may only be closed if its withheld amount is
/// also zero.
///
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// Accounts expected by this instruction:
///
Expand Down Expand Up @@ -251,6 +256,10 @@ pub enum TokenInstruction {
/// decimals value is checked by the caller. This may be useful when
/// creating transactions offline or within a hardware wallet.
///
/// If the accounts are `AccountWithTransferFee`s, the fee is withheld in the
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// source account. If either account is owned by the `fee_account_owner`,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// the fee is ignored.
joncinque marked this conversation as resolved.
Show resolved Hide resolved
///
/// Accounts expected by this instruction:
///
/// * Single owner/delegate
Expand Down Expand Up @@ -409,6 +418,102 @@ pub enum TokenInstruction {
/// The freeze authority/multisignature of the mint.
freeze_authority: COption<Pubkey>,
},

/// Initialize a new mint with a transfer fee
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` The mint to initialize.
///
InitializeMintWithTransferFee {
/// Number of base 10 digits to the right of the decimal place.
decimals: u8,
/// The authority/multisignature to mint tokens.
mint_authority: Pubkey,
/// The freeze authority/multisignature of the mint.
freeze_authority: COption<Pubkey>,
/// Pubkey that must own any provided fee account
fee_account_owner: Pubkey,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// Amount of transfer collected as fees, expressed as basis points of the
/// transfer amount
transfer_fee_basis_points: u16,
/// Maximum fee assessed on transfers
maximum_fee: u64,
},
/// Transfer, providing an explicit fee receiver
///
/// Accounts expected by this instruction:
///
/// * Single owner/delegate
/// 0. `[writable]` The source account. Must be an AccountWithTransferFee.
/// 1. `[]` The token mint. Must be a MintWithTransferFee.
/// 2. `[writable]` The destination account. Must be an AccountWithTransferFee.
/// 3. `[writable]` The fee receiver account. Must be owned by the mint's
/// `fee_account_owner`.
/// 4. `[signer]` The source account's owner/delegate.
///
/// * Multisignature owner/delegate
/// 0. `[writable]` The source account.
/// 1. `[]` The token mint.
/// 2. `[writable]` The destination account.
/// 3. `[writable]` The fee receiver account. Must be owned by the mint's
/// `fee_account_owner`.
/// 4. `[]` The source account's multisignature owner/delegate.
/// 5. ..5+M `[signer]` M signer accounts.
TransferWithFee {
/// The amount of tokens to transfer.
amount: u64,
/// Expected number of base 10 digits to the right of the decimal place.
decimals: u8,
},
/// Transfer withheld tokens to a fee account. Signed by either the source
/// account owner or the fee account owner.
///
/// Succeeds for frozen accounts. <-- Does this make sense?
///
/// Note: this could be made permissionless, but that may open up attack
/// opportunities.
///
/// Accounts expected by this instruction:
///
/// * Single owner/delegate
/// 0. `[writable]` The source account. Must be an AccountWithTransferFee.
/// 1. `[]` The token mint. Must be a MintWithTransferFee.
/// 2. `[writable]` The fee receiver account. Must be owned by the mint's
/// `fee_account_owner`.
/// 3. `[signer]` The source account's owner or the mint's `fee_account_owner`
///
/// * Multisignature owner/delegate
/// 0. `[writable]` The source account.
/// 1. `[]` The token mint.
/// 2. `[writable]` The destination account.
/// 3. `[]` The source or destination account's multisignature owner/delegate.
/// 4. ..4+M `[signer]` M signer accounts.
TransferWithheld {
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// The amount of tokens to transfer.
amount: u64,
/// Expected number of base 10 digits to the right of the decimal place.
decimals: u8,
},
/// Set transfer fee. Only supported for `MintWithTransferFee`s.
///
/// Accounts expected by this instruction:
///
/// * Single authority
/// 0. `[writable]` The mint.
/// 1. `[signer]` The mint's fee account owner.
///
/// * Multisignature authority
/// 0. `[writable]` The mint.
/// 1. `[]` The mint's multisignature fee account owner.
/// 2. ..2+M `[signer]` M signer accounts.
SetTransferFee {
/// Amount of transfer collected as fees, expressed as basis points of the
/// transfer amount
transfer_fee_basis_points: u16,
/// Maximum fee assessed on transfers
maximum_fee: u64,
mvines marked this conversation as resolved.
Show resolved Hide resolved
},
}
impl TokenInstruction {
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
Expand Down Expand Up @@ -625,6 +730,34 @@ impl TokenInstruction {
buf.extend_from_slice(mint_authority.as_ref());
Self::pack_pubkey_option(freeze_authority, &mut buf);
}
&Self::InitializeMintWithTransferFee {
decimals: _,
mint_authority: _,
freeze_authority: _,
fee_account_owner: _,
transfer_fee_basis_points: _,
maximum_fee: _,
} => {
panic!();
}
&Self::TransferWithFee {
amount: _,
decimals: _,
} => {
panic!();
}
&Self::TransferWithheld {
amount: _,
decimals: _,
} => {
panic!();
}
&Self::SetTransferFee {
transfer_fee_basis_points: _,
maximum_fee: _,
} => {
panic!();
}
};
buf
}
Expand Down Expand Up @@ -674,6 +807,9 @@ pub enum AuthorityType {
AccountOwner,
/// Authority to close a token account
CloseAccount,
/// Authority to set the fee and fee owner. Can only be set by the current
/// fee owner. <-- does this make sense? Also, how's the name?
FeeOwner,
}

impl AuthorityType {
Expand All @@ -683,6 +819,7 @@ impl AuthorityType {
AuthorityType::FreezeAccount => 1,
AuthorityType::AccountOwner => 2,
AuthorityType::CloseAccount => 3,
AuthorityType::FeeOwner => 4,
}
}

Expand All @@ -692,6 +829,7 @@ impl AuthorityType {
1 => Ok(AuthorityType::FreezeAccount),
2 => Ok(AuthorityType::AccountOwner),
3 => Ok(AuthorityType::CloseAccount),
4 => Ok(AuthorityType::FeeOwner),
_ => Err(TokenError::InvalidInstruction.into()),
}
}
Expand Down
28 changes: 28 additions & 0 deletions token/program-2022/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,34 @@ impl Processor {
msg!("Instruction: SyncNative");
Self::process_sync_native(program_id, accounts)
}
TokenInstruction::InitializeMintWithTransferFee {
decimals: _,
mint_authority: _,
freeze_authority: _,
fee_account_owner: _,
transfer_fee_basis_points: _,
maximum_fee: _,
} => {
panic!();
}
TokenInstruction::TransferWithFee {
amount: _,
decimals: _,
} => {
panic!();
}
TokenInstruction::TransferWithheld {
amount: _,
decimals: _,
} => {
panic!();
}
TokenInstruction::SetTransferFee {
transfer_fee_basis_points: _,
maximum_fee: _,
} => {
panic!();
}
}
}

Expand Down
100 changes: 100 additions & 0 deletions token/program-2022/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ use solana_program::{
pubkey::Pubkey,
};

/// Different kinds of accounts. Note that `Mint` and `Account` types are determined
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// exclusively by the size of the account, and are not included in the account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AccountType {
/// Nothing, uninitialized
Uninitialized,
/// Base mint
Mint,
/// Base token account
Account,
mvines marked this conversation as resolved.
Show resolved Hide resolved
/// Mints that assess a transfer fee
MintWithTransferFee,
/// Accounts that require transfer fees
AccountWithTransferFee,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
}

impl Default for AccountType {
fn default() -> Self {
Self::Uninitialized
}
}

/// Mint data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -80,6 +103,55 @@ impl Pack for Mint {
}
}

/// MintWithTransferFee data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MintWithTransferFee {
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// Base mint data, done as composition
pub base_mint: Mint,

/// ALL NEW DATA MUST START FROM HERE
/// account type, always set to "MintWithTransferFee"
pub account_type: AccountType,
/// Any provided fee account must be owned by this key. If set to `None`, then
/// this mint behaves like a normal `Mint`.
pub fee_account_owner: COption<Pubkey>,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// Amount of transfer collected as fees, expressed as basis points of the
/// transfer amount, ie. increments of 0.01%
joncinque marked this conversation as resolved.
Show resolved Hide resolved
pub transfer_fee_basis_points: u16,
/// Maximum fee assessed on transfers, expressed as an amount of tokens
pub maximum_fee: u64,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
joncinque marked this conversation as resolved.
Show resolved Hide resolved
}

/// `UberMint` contains all of the possible data in any Mint, to be used instead
/// of `Mint` in programs. Instead of using `Mint::unpack`, use
/// `UberMint::unpack`, and the unpack works for any Mint type, filling in
/// the appropriate type.
/// When writing data back to the buffer, `UberMint::pack` respects the type contained,
/// and errors if unusable fields have been populated or changed. For example,
/// if the fee is set on a normal Mint, it will error.
///
/// Name TBD. Other options: MetaMint, AllMint, OverMint, MegaMint.
/// OR we rename Mint to BaseMint, and then call this Mint.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct UberMint {
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// Base mint data, done as composition
pub base_mint: Mint,

/// Account type, can be either Mint or MintWithTransferFee
pub account_type: AccountType,
/// Fee account must be owned by this key. If set to `None`, then this mint
/// behaves like a normal `Mint`.
pub fee_account_owner: COption<Pubkey>,
/// Amount of transfer collected as fees, expressed as basis points of the
/// transfer amount, ie. increments of 0.01%
pub transfer_fee_basis_points: u16,
/// Maximum fee assessed on transfers, expressed
pub maximum_fee: u64,
// ADD ANY NEW MINT FIELDS HERE
}

/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -191,6 +263,34 @@ impl Default for AccountState {
}
}

/// AccountWithTransferFee data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AccountWithTransferFee {
joncinque marked this conversation as resolved.
Show resolved Hide resolved
/// All base account data, done through composition
pub base_account: Account,

/// ALL NEW DATA STARTS HERE
/// Account type, must always be AccountType::AccountWithTransferFee
pub account_type: AccountType,
/// Amount withheld during transfers, to be claimed by the fee recipient
pub withheld_amount: u64,
}

/// See the discussion at `UberMint` about the concept of this type.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct UberAccount {
/// All base account data, done through composition
pub base_account: Account,

/// Account type, can be either Account or AccountWithTransferFee
pub account_type: AccountType,
/// Amount withheld during transfers, to be claimed by the fee recipient
pub withheld_amount: u64,
// ADD ANY NEW ACCOUNT FIELDS HERE
}

/// Multisignature data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
Expand Down