A Extensão Transfer Fee
A extensão TransferFee é uma extensão de Mint que permite ao criador definir um "imposto" sobre o token que é cobrado toda vez que alguém realiza uma troca (swap).
Para garantir que o destinatário da taxa não seja bloqueado para escrita toda vez que alguém realiza uma troca, e para garantir que possamos paralelizar transações contendo uma Mint com esta extensão, a taxa é reservada na Token Account do destinatário e somente a Withdraw Authority pode sacá-la.
Inicializando a Mint Account
Como o Anchor não possui macros para a extensão transfer_fee, vamos criar uma conta Mint usando CPIs diretamente.
Veja como criar uma mint com a extensão 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<()> {
// Calcular o espaço necessário para os dados da mint e da extensão
let mint_size =
ExtensionType::try_calculate_account_len::<PodMint>(&[ExtensionType::TransferFeeConfig])?;
// Calcular lamports mínimos necessários para o tamanho da conta mint com extensões
let lamports = (Rent::get()?).minimum_balance(mint_size);
// Invocar o System Program para criar uma nova conta com espaço para dados da mint e extensão
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, // Espaço
&ctx.accounts.token_program.key(), // Owner Program
)?;
// Inicializar os dados da extensão transfer fee
// Esta instrução deve vir antes da instrução para inicializar os dados da mint
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 (atualizar taxa)
Some(&ctx.accounts.payer.key()), // withdraw authority (sacar taxas)
transfer_fee_basis_points, // transfer fee basis points (% de taxa por transferência)
maximum_fee, // taxa máxima (unidades máximas de token por transferência)
)?;
// Inicializar os dados padrão da conta mint
initialize_mint2(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InitializeMint2 {
mint: ctx.accounts.mint_account.to_account_info(),
},
),
2, // decimais
&ctx.accounts.payer.key(), // mint authority
Some(&ctx.accounts.payer.key()), // freeze authority
)?;
Ok(())
}Transferindo Tokens com a Taxa
Para transferir tokens de uma Mint que possui a extensão TransferFee, temos duas opções:
Podemos usar a instrução
transfer_checked()normal e, ao fazer isso, o cálculo da taxa é tratado automaticamentePodemos usar a instrução
transfer_checked_with_fee()e fornecer manualmente afeeque vamos pagar nessa transferência. Isso é muito útil se quisermos ter certeza de não sermos "rugados" caso a authority altere a taxa e crie uma taxa anormalmente alta; é como definir o slippage para uma transferência.
Veja como criar uma transferência usando a instrução 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>,
}
// as taxas de transferência são deduzidas automaticamente do valor da transferência
// o destinatário recebe (valor da transferência - taxas)
// as taxas de transferência são armazenadas diretamente na token account do destinatário e devem ser "coletadas"
pub fn transfer_checked_with_fee(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// ler os dados da extensão da conta mint
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>()?;
// calcular a taxa esperada
let epoch = Clock::get()?.epoch;
let fee = extension_data.calculate_epoch_fee(epoch, amount).unwrap();
// decimais da conta mint
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, // valor da transferência
decimals, // decimais
fee, // taxa
)?;
Ok(())
}Para então sacar as taxas, podemos reunir todas as Token Account que possuem taxas "presas" dentro delas e passá-las como remaining accounts. No lado do TypeScript, ficaria algo assim:
// Recuperar todas as Token Accounts para a Mint Account
const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: "confirmed",
filters: [
{
memcmp: {
offset: 0,
bytes: mint.publicKey.toString(), // Endereço da Mint Account
},
},
],
});
// Lista de Token Accounts das quais sacar as taxas
const accountsToWithdrawFrom: PublicKey[] = [];
for (const accountInfo of allAccounts) {
const account = unpackAccount(
accountInfo.pubkey,
accountInfo.account,
TOKEN_2022_PROGRAM_ID,
);
// Extrair dados de transfer fee de cada conta
const transferFeeAmount = getTransferFeeAmount(account);
// Verificar se há taxas disponíveis para saque
if (transferFeeAmount !== null && transferFeeAmount.withheldAmount > 0) {
accountsToWithdrawFrom.push(accountInfo.pubkey);
}
}
// ...dados e contas da instrução
remainingAccounts: accountsToWithdrawFrom.map(pubkey => ({
pubkey,
isWritable: true,
isSigner: false
})),Após configurarmos todas as contas que precisamos, para sacar os tokens para a authority_token_account, precisamos "coletar" todas as taxas das token accounts usando a instrução harvest_withheld_tokens_to_mint() e, em seguida, reivindicá-las para uma token_account escolhida pela authority usando a instrução 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<()> {
// Usando remaining accounts para permitir a passagem de um número desconhecido de token accounts para coletar
// Verificar se as remaining accounts são token accounts da mint para coletar
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<_>>();
// as taxas de transferência são armazenadas diretamente na token account do destinatário e devem ser "coletadas"
// "coletar" transfere as taxas acumuladas nas token accounts para a 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 para coletar
)?;
// as taxas de transferência "coletadas" para a mint account podem então ser sacadas pela withdraw authority
// isso transfere as taxas na mint account para a token account especificada
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(())
}Atualizando a Taxa
Após termos inicializado nossa Mint com a extensão TransferFee, podemos precisar atualizar essa taxa específica no futuro. E para garantir que o criador não "ruge" seus detentores de tokens com uma "troca enganosa" definindo a taxa muito alta toda vez que uma transferência é executada, o novo TransferFee será ativado após 2 epochs.
Para acomodar isso, é assim que os dados da extensão TransferFee se parecem:
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,
}Então, para alterar a taxa, podemos usar a instrução transfer_fee_set() assim:
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>,
}
// Observe que há um atraso de 2 epochs para que as novas atualizações de taxa entrem em vigor
// Este é um recurso de segurança integrado na extensão
// 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 (% de taxa por transferência)
maximum_fee, // taxa máxima (unidades máximas de token por transferência)
)?;
Ok(())
}