Ekstensi Biaya Transfer
Ekstensi TransferFee
adalah ekstensi Mint
yang memungkinkan pembuat menetapkan "pajak" pada token yang dikumpulkan setiap kali seseorang melakukan pertukaran.
Untuk memastikan bahwa penerima biaya tidak mendapatkan write-lock setiap kali seseorang melakukan pertukaran, dan untuk memastikan bahwa kita dapat memparalelkan transaksi yang berisi Mint
dengan ekstensi ini, biaya disisihkan di Akun Token penerima yang hanya dapat ditarik oleh Withdraw Authority
.
Menginisialisasi Akun Mint
Karena Anchor
tidak memiliki makro untuk ekstensi transfer_fee
, kita akan membuat akun Mint
menggunakan CPI mentah.
Berikut cara membuat mint dengan ekstensi Biaya Transfer:
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(())
}
Mentransfer Token dengan Biaya
Untuk mentransfer token untuk Mint
yang memiliki ekstensi TransferFee` kita memiliki dua jalur:
- Kita dapat menggunakan instruksi
transfer_checked()
normal dan dengan melakukan ini perhitungan biaya ditangani secara otomatis - Kita dapat menggunakan instruksi
transfer_checked_with_fee()
dan menyediakan secara manualfee
yang akan kita bayar dalam transfer tersebut. Ini sangat berguna jika kita ingin memastikan untuk tidak "ditipu" jika otoritas mengubah biaya dan membuat biaya yang sangat tinggi; ini seperti mengatur slippage untuk transfer.
Berikut cara membuat transfer menggunakan instruksi 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(())
}
Untuk kemudian menarik biaya, kita dapat mengumpulkan semua Token Account
yang memiliki biaya "terjebak" di dalamnya dan meneruskannya sebagai remaining account. Di sisi Typescript, itu akan terlihat seperti ini:
// 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
})),
Setelah kita menyiapkan semua akun yang kita butuhkan, untuk menarik token ke authority_token_account
kita perlu "memanen" semua biaya dari akun token menggunakan instruksi harvest_withheld_tokens_to_mint()
dan kemudian mengklaimnya ke token_account
yang dipilih oleh authority
menggunakan instruksi 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(())
}
Memperbarui Biaya
Setelah menginisialisasi Mint
dengan ekstensi TranferFee
, kita mungkin perlu memperbarui biaya tersebut di masa depan. Dan untuk memastikan bahwa pembuat tidak "menipu" pemegang token mereka dengan "umpan dan jebakan" dengan menetapkan biaya yang sangat tinggi setiap kali transfer dilakukan, TranferFee
yang baru akan diaktifkan setelah 2 epoch.
Untuk mengakomodasi hal ini, beginilah tampilan data dari ekstensi 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,
}
Jadi untuk mengubah biaya, kita dapat menggunakan instruksi transfer_fee_set()
seperti ini:
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(())
}