轉移費用擴展
TransferFee 擴展是一個 Mint 擴展,允許創建者設置一個「稅」,每當有人進行交換時都會收取該稅。
為了確保費用接收者在每次有人進行交換時不會被寫鎖,並確保我們可以並行處理包含此擴展的 Mint 的交易,費用會被存放在接收者的代幣帳戶中,只有 Withdraw Authority 可以提取。
初始化鑄幣帳戶
由於 Anchor 沒有任何用於 transfer_fee 擴展的宏,我們將使用原始 CPI 創建一個 Mint 帳戶。
以下是如何使用轉移費用擴展創建鑄幣的方法:
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(())
}帶費用的代幣轉移
要轉移具有 TransferFee 擴展的 Mint 代幣,我們有兩種路徑:
我們可以使用普通的
transfer_checked()指令,這樣費用的計算會自動處理我們可以使用
transfer_checked_with_fee()指令並手動提供我們在該轉移中要支付的fee。這在我們想確保不被「詐騙」時非常有用,例如當授權更改費用並設置異常高的費用時;這就像為轉移設置滑點。
以下是如何使用 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(())
}然後要提取費用,我們可以收集所有「困住」費用的 Token Account,並將它們作為剩餘帳戶傳遞。在 Typescript 端看起來會是這樣:
// 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
})),在我們設置好所有需要的帳戶後,要將代幣提取到authority_token_account,我們需要使用harvest_withheld_tokens_to_mint()指令從代幣帳戶中「收取」所有費用,然後將它們申領到由authority選擇的token_account,使用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(())
}更新費用
在我們使用TranferFee擴展初始化了Mint之後,未來可能需要更新該特定費用。為了確保創建者不會通過設置非常高的費用來「誘餌欺詐」代幣持有人,每次執行轉賬時,新TranferFee將在2個epoch後啟用。
為了適應這一點,以下是TransferFee擴展的數據結構:
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,
}因此,要更改費用,我們可以使用transfer_fee_set()指令,如下所示:
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(())
}