Transfer Fee Extension
Phần mở rộng TransferFee
là một Mint
extension cho phép người tạo đặt "thuế" trên token được thu thập mỗi khi ai đó thực hiện swap.
Để đảm bảo rằng đối tượng nhận phí không bị write-lock mỗi khi ai đó thực hiện swap, và để đảm bảo rằng chúng ta có thể song song hóa các giao dịch chứa Mint
với phần mở rộng này, fee được đặt bên trong Token Account của người nhận mà chỉ Withdraw Authority
mới có thể rút.
Khởi tạo Mint Account
Vì Anchor
không có bất kỳ macro nào cho extension transfer_fee
, chúng ta sẽ tạo Mint
account bằng cách sử dụng một CPI thô.
Đây là cách tạo mint với phần mở rộng Transfer Fee:
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(())
}
Transfer Token với Fee
Để chuyển token của Mint
có phần mở rộng TransferFee
, chúng ta có hai cách:
- Chúng ta có thể sử dụng instruction
transfer_checked()
bình thường và bằng cách này việc tính toán fee được xử lý tự động - Chúng ta có thể sử dụng instruction
transfer_checked_with_fee()
và cung cấp thủ côngfee
mà chúng ta sẽ trả trong transfer đó. Điều này rất hữu ích nếu chúng ta muốn đảm bảo không bị "rug" nếu authority thay đổi fee và tạo ra fee cao bất thường; nó giống như việc đặt slippage cho transfer.
Đây là cách tạo transfer sử dụng instruction transfer_checked_with_fee()
:
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(())
}
Để rút fee, chúng ta có thể sử dụng instruction harvest_withheld_tokens_to_mint()
để thu tất cả fee từ các token account và sau đó sử dụng instruction withdraw_withheld_tokens_from_mint()
để rút fee đến một token_account
được chọn bởi authority
.
// 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
})),
Sau khi chúng ta thiết lập tất cả account mà chúng ta cần, để rút token đến authority_token_account
, chúng ta cần "thu hoạch" tất cả fee từ token account bằng cách sử dụng instruction harvest_withheld_tokens_to_mint()
và sau đó chuyển chúng đến token_account
được chọn bởi authority
bằng cách sử dụng instruction withdraw_withheld_tokens_from_mint()
.
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(())
}
Cập nhật Fee
Sau khi đã khởi tạo Mint
của chúng ta với phần mở rộng TranferFee
, chúng ta có thể cần cập nhật fee cụ thể đó trong tương lai. Và để đảm bảo rằng người tạo không "rug" những người nắm giữ token của họ với "bait và switch" bằng cách đặt fee rất cao mỗi khi transfer được thực thi, TranferFee
mới sẽ chỉ được kích hoạt sau 2 epoch.
Để phù hợp với điều này, đây là cách dữ liệu của phần mở rộng TransferFee
thể hiện:
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,
}
Vì vậy để thay đổi fee, chúng ta có thể sử dụng instruction transfer_fee_set()
như thế này:
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(())
}