The Transfer Fee Extension
The TransferFee
extension is a Mint
extension that lets the creator set a "tax" on the token that is collected every time somebody performs a swap.
To make sure that the fee recipient doesn't get write-locked every time somebody performs a swap, and to ensure that we can parallelize transactions containing a Mint
with this extension, the fee is set aside in the recipient's Token Account that only the Withdraw Authority
can withdraw.
Initializing the Mint Account
Since Anchor
doesn't have any macros for the transfer_fee
extension we're going to create a Mint
account using the raw CPIs.
Here's how to create a mint with the Transfer Fee extension:
use anchor_lang::prelude::*;
use anchor_lang::system_program::{create_account, CreateAccount};
use anchor_spl::{
token_2022::{
initialize_mint2,
spl_token_2022::{
extension::{
transfer_fee::TransferFeeConfig, BaseStateWithExtensions, ExtensionType,
StateWithExtensions,
},
pod::PodMint,
state::Mint as MintState,
},
InitializeMint2,
},
token_interface::{
spl_pod::optional_keys::OptionalNonZeroPubkey, transfer_fee_initialize, Token2022,
TransferFeeInitialize,
},
};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub mint_account: Signer<'info>,
pub token_program: Program<'info, Token2022>,
pub system_program: Program<'info, System>,
}
pub fn initialize_transfer_fee_config(
ctx: Context<Initialize>,
transfer_fee_basis_points: u16,
maximum_fee: u64,
) -> Result<()> {
// Calculate space required for mint and extension data
let mint_size =
ExtensionType::try_calculate_account_len::<PodMint>(&[ExtensionType::TransferFeeConfig])?;
// Calculate minimum lamports required for size of mint account with extensions
let lamports = (Rent::get()?).minimum_balance(mint_size);
// Invoke System Program to create new account with space for mint and extension data
create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.mint_account.to_account_info(),
},
),
lamports, // Lamports
mint_size as u64, // Space
&ctx.accounts.token_program.key(), // Owner Program
)?;
// Initialize the transfer fee extension data
// This instruction must come before the instruction to initialize the mint data
transfer_fee_initialize(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferFeeInitialize {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint_account.to_account_info(),
},
),
Some(&ctx.accounts.payer.key()), // transfer fee config authority (update fee)
Some(&ctx.accounts.payer.key()), // withdraw authority (withdraw fees)
transfer_fee_basis_points, // transfer fee basis points (% fee per transfer)
maximum_fee, // maximum fee (maximum units of token per transfer)
)?;
// Initialize the standard mint account data
initialize_mint2(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InitializeMint2 {
mint: ctx.accounts.mint_account.to_account_info(),
},
),
2, // decimals
&ctx.accounts.payer.key(), // mint authority
Some(&ctx.accounts.payer.key()), // freeze authority
)?;
Ok(())
}
Transferring Tokens with the Fee
To transfer tokens for Mint
that have a TransferFee` extension we have to routes:
- We can use the normal
transfer_checked()
instruction and by doing this the calculation of the fee is handled automatically - We can use the
transfer_checked_with_fee()
instruction and supply manually thefee
we're going to pay in that tansfer. This is very useful if we want to make sure to not get "rugged" if the authority change the fee and create a abnormally high fee; it's like setting the slippage for a transfer.
Even if the authority change the fee, the new fee become active 2 epoch after being set
Here's how to create a transfer using the transfer_checked_with_fee()
instruction:
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_2022::spl_token_2022::{
extension::{
transfer_fee::TransferFeeConfig, BaseStateWithExtensions, StateWithExtensions,
},
state::Mint as MintState,
},
token_interface::{
transfer_checked_with_fee, Mint, Token2022, TokenAccount, TransferCheckedWithFee,
},
};
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub sender: Signer<'info>,
pub recipient: SystemAccount<'info>,
#[account(mut)]
pub mint_account: InterfaceAccount<'info, Mint>,
#[account(
mut,
associated_token::mint = mint_account,
associated_token::authority = sender,
associated_token::token_program = token_program
)]
pub sender_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
init_if_needed,
payer = sender,
associated_token::mint = mint_account,
associated_token::authority = recipient,
associated_token::token_program = token_program
)]
pub recipient_token_account: InterfaceAccount<'info, TokenAccount>,
pub token_program: Program<'info, Token2022>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
// transfer fees are automatically deducted from the transfer amount
// recipients receives (transfer amount - fees)
// transfer fees are stored directly on the recipient token account and must be "harvested"
pub fn transfer_checked_with_fee(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// read mint account extension data
let mint = &ctx.accounts.mint_account.to_account_info();
let mint_data = mint.data.borrow();
let mint_with_extension = StateWithExtensions::<MintState>::unpack(&mint_data)?;
let extension_data = mint_with_extension.get_extension::<TransferFeeConfig>()?;
// calculate expected fee
let epoch = Clock::get()?.epoch;
let fee = extension_data.calculate_epoch_fee(epoch, amount).unwrap();
// mint account decimals
let decimals = ctx.accounts.mint_account.decimals;
transfer_checked_with_fee(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferCheckedWithFee {
token_program_id: ctx.accounts.token_program.to_account_info(),
source: ctx.accounts.sender_token_account.to_account_info(),
mint: ctx.accounts.mint_account.to_account_info(),
destination: ctx.accounts.recipient_token_account.to_account_info(),
authority: ctx.accounts.sender.to_account_info(),
},
),
amount, // transfer amount
decimals, // decimals
fee, // fee
)?;
Ok(())
}
To then withdraw the fees, we can gather all the Token Account
that have fees "trapped" inside of them and pass them as remaining account. On the Typescript side it would look something like this:
// Retrieve all Token Accounts for the Mint Account
const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: "confirmed",
filters: [
{
memcmp: {
offset: 0,
bytes: mint.publicKey.toString(), // Mint Account address
},
},
],
});
// List of Token Accounts to withdraw fees from
const accountsToWithdrawFrom: PublicKey[] = [];
for (const accountInfo of allAccounts) {
const account = unpackAccount(
accountInfo.pubkey,
accountInfo.account,
TOKEN_2022_PROGRAM_ID,
);
// Extract transfer fee data from each account
const transferFeeAmount = getTransferFeeAmount(account);
// Check if fees are available to be withdrawn
if (transferFeeAmount !== null && transferFeeAmount.withheldAmount > 0) {
accountsToWithdrawFrom.push(accountInfo.pubkey);
}
}
// ...instruction data and accounts
remainingAccounts: accountsToWithdrawFrom.map(pubkey => ({
pubkey,
isWritable: true,
isSigner: false
})),
After we set up all the accounts that we need, to withdraw tokens to the authority_token_account
we need to "harvest" all fees from token accounts using the harvest_withheld_tokens_to_mint()
instruction and then claiming them to a token_account
choosen by the authority
using the withdraw_withheld_tokens_from_mint()
instruction.
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
harvest_withheld_tokens_to_mint, HarvestWithheldTokensToMint, withdraw_withheld_tokens_from_mint, WithdrawWithheldTokensFromMint, Mint, Token2022, TokenAccount,
};
#[derive(Accounts)]
pub struct HarvestAndWithdrawFees<'info> {
pub authority: Signer<'info>,
#[account(mut)]
pub mint_account: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub token_program: Program<'info, Token2022>,
}
pub fn harvest_and_withdraw_fees<'info>(ctx: Context<'_, '_, 'info, 'info, HarvestAndWithdrawFees<'info>>) -> Result<()> {
// Using remaining accounts to allow for passing in an unknown number of token accounts to harvest from
// Check that remaining accounts are token accounts for the mint to harvest to
let sources = ctx
.remaining_accounts
.iter()
.filter_map(|account| {
InterfaceAccount::<TokenAccount>::try_from(account)
.ok()
.filter(|token_account| token_account.mint == ctx.accounts.mint_account.key())
.map(|_| account.to_account_info())
})
.collect::<Vec<_>>();
// transfer fees are stored directly on the recipient token account and must be "harvested"
// "harvesting" transfers fees accumulated on token accounts to the mint account
harvest_withheld_tokens_to_mint(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
HarvestWithheldTokensToMint {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint_account.to_account_info(),
},
),
sources, // token accounts to harvest from
)?;
// transfer fees "harvested" to the mint account can then be withdraw by the withdraw authority
// this transfers fees on the mint account to the specified token account
withdraw_withheld_tokens_from_mint(CpiContext::new(
ctx.accounts.token_program.to_account_info(),
WithdrawWithheldTokensFromMint {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint_account.to_account_info(),
destination: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
))?;
Ok(())
}
Updating the Fee
After having intialized our Mint
with the TranferFee
extension, we might need to update that specific fee in the future. And to make sure that the creator doesn't "rug" their token holder with a "bait and switch" by setting the fee very high everytime a transfer is executed the new TranferFee
will be activated after 2 epoch.
To accomodate for this, this is how the data of the TransferFee
extension looks like:
pub struct TransferFeeConfig {
pub transfer_fee_config_authority: Pubkey,
pub withdraw_withheld_authority: Pubkey,
pub withheld_amount: u64,
pub older_transfer_fee: TransferFee,
pub newer_transfer_fee: TransferFee,
}
pub struct TransferFee {
pub epoch: u64,
pub maximum_fee: u64,
pub transfer_fee_basis_point: u16,
}
So to change the fee we can use the transfer_fee_set()
instruction like so:
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{transfer_fee_set, Mint, Token2022, TransferFeeSetTransferFee};
#[derive(Accounts)]
pub struct UpdateFee<'info> {
pub authority: Signer<'info>,
#[account(mut)]
pub mint_account: InterfaceAccount<'info, Mint>,
pub token_program: Program<'info, Token2022>,
}
// Note that there is a 2 epoch delay from when new fee updates take effect
// This is a safely feature built into the extension
// https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/transfer_fee/processor.rs#L92-L109
pub fn update_fee(
ctx: Context<UpdateFee>,
transfer_fee_basis_points: u16,
maximum_fee: u64,
) -> Result<()> {
transfer_fee_set(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferFeeSetTransferFee {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
),
transfer_fee_basis_points, // transfer fee basis points (% fee per transfer)
maximum_fee, // maximum fee (maximum units of token per transfer)
)?;
Ok(())
}