Anchor
Token2022 mit Anchor

Token2022 mit Anchor

Die Transfer Fee Extension

Die TransferFee Extension ist eine Mint Extension, die es dem Ersteller ermöglicht, eine "Steuer" auf den Token festzulegen, die bei jedem Tausch erhoben wird.

Um sicherzustellen, dass der Gebührenempfänger nicht bei jedem Tausch schreibgeschützt wird und um zu gewährleisten, dass wir Transaktionen mit einer Mint mit dieser Extension parallelisieren können, wird die Gebühr im Token-Konto des Empfängers zurückgelegt, auf das nur der Withdraw Authority zugreifen kann.

Initialisierung des Mint-Kontos

Da Anchor keine Makros für die transfer_fee Extension hat, werden wir ein Mint Konto mit den rohen CPIs erstellen.

So erstellt man einen Mint mit der Transfer Fee Extension:

rust
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(())
}

Übertragung von Tokens mit der Gebühr

Um Tokens für Mint zu übertragen, die eine TransferFee` Extension haben, gibt es zwei Wege:

  • Wir können die normale transfer_checked() Anweisung verwenden, wobei die Berechnung der Gebühr automatisch erfolgt

  • Wir können die transfer_checked_with_fee() Anweisung verwenden und manuell die fee angeben, die wir bei dieser Übertragung zahlen werden. Dies ist sehr nützlich, wenn wir sicherstellen wollen, nicht "gerugged" zu werden, falls die Autorität die Gebühr ändert und eine ungewöhnlich hohe Gebühr festlegt; es ist wie das Festlegen des Slippages für eine Übertragung.

Selbst wenn die Autorität die Gebühr ändert, wird die neue Gebühr erst 2 Epochen nach der Festlegung aktiv

So erstellt man eine Übertragung mit der transfer_checked_with_fee() Anweisung:

rust
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(())
}

Um dann die Gebühren abzuheben, können wir alle Token Account sammeln, die Gebühren "gefangen" haben und sie als verbleibende Konten übergeben. Auf der Typescript-Seite würde es etwa so aussehen:

ts
// 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
})),

Nachdem wir alle benötigten Konten eingerichtet haben, müssen wir zum Abheben von Tokens auf das authority_token_account zunächst alle Gebühren von Token-Konten mit der Anweisung harvest_withheld_tokens_to_mint() "ernten" und sie dann an ein vom authority gewähltes token_account mittels der Anweisung withdraw_withheld_tokens_from_mint() überweisen.

rust
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(())
}

Aktualisierung der Gebühr

Nachdem wir unser Mint mit der Erweiterung TranferFee initialisiert haben, müssen wir diese spezifische Gebühr möglicherweise in Zukunft aktualisieren. Um sicherzustellen, dass der Ersteller seine Token-Inhaber nicht mit einem "Köder und Austausch" betrügt, indem er die Gebühr bei jeder Übertragung sehr hoch ansetzt, wird die neue TranferFee erst nach 2 Epochen aktiviert.

Um dies zu berücksichtigen, sehen die Daten der Erweiterung TransferFee wie folgt aus:

rust
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,
}

Um die Gebühr zu ändern, können wir die Anweisung transfer_fee_set() wie folgt verwenden:

rust
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(())
}
Blueshift © 2025Commit: e573eab