Jacky Gu

Solana开发笔记: Token2022 - 使用Transfer Fee扩展

01 Jan 2025 Share to

Token2022 - 使用Transfer fee扩展

本文在之前创建的包含Metadata Pointer ExtensionToken Metadata Extension的代币的基础上,再添加一个扩展:Transfer Fee Extension

因为Anchor框架尚未开发这个扩展,所以需要手动创建这个扩展,然后在指令中添加这个扩展,最后在指令中调用这个扩展,就可以实现Transfer fee的功能了。

创建指令

instructions目录下创建一个新的指令: initialize_token_with_transfer_fee.rs,并将以下代码粘贴进去:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke_signed;
use anchor_lang::system_program::{create_account, CreateAccount};
use anchor_spl::{
    associated_token::{AssociatedToken, create, Create},
    token_interface::{Token2022, spl_pod::optional_keys::OptionalNonZeroPubkey},
    token_2022::spl_token_2022::{
        self,
        instruction::AuthorityType,
        extension::{
            ExtensionType,
            metadata_pointer::instruction::initialize as initialize_metadata_pointer,
            transfer_fee::instruction::initialize_transfer_fee_config
        },
        instruction::initialize_mint2,
        pod::PodMint
    },
    token_2022::{mint_to, MintTo, set_authority, SetAuthority}
};
use spl_token_metadata_interface::{
    state::TokenMetadata as SplTokenMetadata,
    instruction::initialize as initialize_metadata,
};
use crate::constants::MINT_SEEDS;
use crate::errors::ErrorCode;
use crate::states::TokenMetadata;

#[derive(Accounts)]
#[instruction(metadata_params: TokenMetadata)]
pub struct InitializeTokenWithTransferFee<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  /// CHECK: token account of payer
  #[account(mut)]
  pub payer_ata: UncheckedAccount<'info>,
  /// CHECK: token mint
  #[account(
    mut,
    seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()],
    bump,
  )]
  pub mint: AccountInfo<'info>,
  pub rent: Sysvar<'info, Rent>,
  pub system_program: Program<'info, System>,
  pub token_program: Program<'info, Token2022>,
  pub associated_token_program: Program<'info, AssociatedToken>,
}

impl<'info> InitializeTokenWithTransferFee<'info> {
  pub fn initialize_token_with_transfer_fee(ctx: Context<InitializeTokenWithTransferFee>, metadata_params: Box<TokenMetadata>, transfer_fee_basis_points: u16, maximum_fee: u64) -> Result<()> {
    require!(spl_token_2022::ID == ctx.accounts.token_program.key(), ErrorCode::WrongTokenProgram);
    let seeds = &[MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes(), &[ctx.bumps.mint]];
    let signer = [&seeds[..]];

    // Create account and allocate space
    let mint_size = ExtensionType::try_calculate_account_len::<PodMint>(&[
      ExtensionType::TransferFeeConfig,
      ExtensionType::MetadataPointer
    ])?;
    let spl_token_metadata = SplTokenMetadata {
      update_authority: OptionalNonZeroPubkey(ctx.accounts.mint.key()),
      mint: ctx.accounts.mint.key(),
      name: metadata_params.name.clone(),
      symbol: metadata_params.symbol.clone(),
      uri: metadata_params.uri.clone(),
      additional_metadata: vec![],
    };

    let metadata_size = SplTokenMetadata::tlv_size_of(&spl_token_metadata)?;
    let lamports = (Rent::get()?).minimum_balance(mint_size + metadata_size);
    create_account(
      CpiContext::new_with_signer(
        ctx.accounts.system_program.to_account_info(),
        CreateAccount {
          from: ctx.accounts.payer.to_account_info(),
          to: ctx.accounts.mint.to_account_info(),
        },
        &signer,
      ),
      lamports,
      mint_size as u64,
      &ctx.accounts.token_program.key(),
    )?;
    
    // Initialize transfer fee config
    invoke_signed(
        &initialize_transfer_fee_config(
          &ctx.accounts.token_program.key(), // SPL Token program ID
          &ctx.accounts.mint.key(),          // Mint account
          Some(&ctx.accounts.mint.key()),
          Some(&ctx.accounts.mint.key()),
          transfer_fee_basis_points, // Basis points for the transfer fee
          maximum_fee,// Maximum fee
      ).unwrap(),
        &[
          ctx.accounts.mint.to_account_info().clone(),
        ],
        &signer,
    )?;

    // Initialize metadata point
    invoke_signed(
      &initialize_metadata_pointer(
        &ctx.accounts.token_program.key(),
        &ctx.accounts.mint.key(),
        Some(ctx.accounts.mint.key()),  // authority = mint
        Some(ctx.accounts.mint.key()),  // metadata address = mint
      ).unwrap(),
      &[
          ctx.accounts.mint.to_account_info().clone(),
      ],
      &signer,
    )?;

    // Initialize Mint Account
    invoke_signed(
      &initialize_mint2(
          &ctx.accounts.token_program.key(),
          &ctx.accounts.mint.key(),
          &ctx.accounts.mint.key(),
          Some(&ctx.accounts.mint.key()),
          9,
      )?,
      &[
        ctx.accounts.mint.to_account_info().clone(),
        ctx.accounts.rent.to_account_info(),
      ],
      &signer,
    )?;

    // Initialize token metadata
    invoke_signed(
      &initialize_metadata(
        &ctx.accounts.token_program.key(),
        &ctx.accounts.mint.key(),
        &ctx.accounts.mint.key(),
        &ctx.accounts.mint.key(),
        &ctx.accounts.mint.key(),
        metadata_params.name.clone(),
        metadata_params.symbol.clone(),
        metadata_params.uri.clone(),
      ),
      &[
        ctx.accounts.mint.to_account_info().clone(),
        ctx.accounts.mint.to_account_info().clone(),
      ],
      &signer,
    )?;

    // Create payer's ATA if not exists
    if ctx.accounts.payer_ata.to_account_info().data_is_empty() {
      create(
        CpiContext::new(
          ctx.accounts.associated_token_program.to_account_info(), 
          Create {
            payer: ctx.accounts.payer.to_account_info(),
            associated_token: ctx.accounts.payer_ata.to_account_info(),
            authority: ctx.accounts.payer.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
        })
      )?;
    }

    // Mint 10000000 to deployer
    mint_to(
      CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        MintTo {
          mint: ctx.accounts.mint.to_account_info(),
          to: ctx.accounts.payer_ata.to_account_info(),
          authority: ctx.accounts.mint.to_account_info(),
        },
        &signer,
      ),
      10000000 * 10u64.pow(metadata_params.decimals as u32),
    )?;

    // Revoke the freeze authority
    set_authority(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            SetAuthority {
                current_authority: ctx.accounts.mint.to_account_info().clone(),
                account_or_mint: ctx.accounts.mint.to_account_info().clone(),
            },
            &signer,
        ),
        AuthorityType::FreezeAccount,
        None,
    )?;

    // Revoke the mint authority
    set_authority(
      CpiContext::new_with_signer(
          ctx.accounts.token_program.to_account_info(),
          SetAuthority {
              current_authority: ctx.accounts.mint.to_account_info().clone(),
              account_or_mint: ctx.accounts.mint.to_account_info().clone(),
          },
          &signer,
      ),
      AuthorityType::MintTokens,
      None,
    )?;

    Ok(())
  }
}

以上指令创建了一个新的Mint,按顺序执行了一下操作:

  • 1- 计算一个包含Transfer fee扩展和Metadata Pointer扩展的mint需要的空间,以及需要的lamports,并创建账户;
  • 2- 初始化Transfer fee config账户,并设置Transfer fee费率和最大费用;
  • 3- 初始化Metadata Pointer账户,并设置Metadata Pointer账户的authoritymint账户,metadata addressmint账户;
  • 4- 使用initialize_mint2指令创建mint账户;
  • 5- 使用initialize_metadata指令创建metadata
  • 6- 创建部署人的TokenAccount;
  • 7- 向部署人的token account账户铸造10000000个代币,用于之后的转账测试;
  • 8- 取消freeze代币的权限;
  • 9- 取消mint代币的权限;

在lib.rs中调用上面的指令

pub fn initialize_token_with_transfer_fee(ctx: Context<InitializeTokenWithTransferFee>, metadata_params: Box<TokenMetadata>, transfer_fee_basis_points: u16, maximum_fee: u64) -> Result<()> {
    InitializeTokenWithTransferFee::initialize_token_with_transfer_fee(ctx, metadata_params, transfer_fee_basis_points, maximum_fee)
}

编译部署

anchor build && anchor deploy

编写测试代码

describe("Test Transfer fee extension", async () => {
  it("Create token mint with transfer fee extension", async () => {
    const payerAta = getAssociatedTokenAddressSync(mintAccount, deployerAccount.publicKey, false, TOKEN_2022_PROGRAM_ID);
    const context = { 
      payer: deployerAccount.publicKey,
      mint: mintAccount,
      payerAta: payerAta,
      tokenProgram: TOKEN_2022_PROGRAM_ID,
    }
    const tranferFeeBasisPoints = 100;
    const maximumFee = new anchor.BN(100000).mul(new anchor.BN(1000000000));
    const tx = new anchor.web3.Transaction();
    const ix = await program.methods
      .initializeTokenWithTransferFee(metadataParams, tranferFeeBasisPoints, maximumFee)
      .accounts(context)
      .instruction();
    tx.add(ix);
    const signature = await provider.sendAndConfirm(tx, [deployerAccount]);
    console.log("Initialized token with transfer fee extension tx:", signature);
  })

  it("Transfer token with transfer fee extension (from deployer to user1)", async () => {
    // Get the source and destination token accounts
    const sourceAta = await getAssociatedTokenAddress(mintAccount, deployerAccount.publicKey, false, TOKEN_2022_PROGRAM_ID);
    const destinationAta = await getAssociatedTokenAddress(mintAccount, user1Account.publicKey, false, TOKEN_2022_PROGRAM_ID);

    // Create destination ATA if it doesn't exist
    const tx = new anchor.web3.Transaction();
    try {
      await provider.connection.getTokenAccountBalance(destinationAta);
    } catch {
      // if token accont the destination doesn't exist, create it
      const ix = createAssociatedTokenAccountInstruction(
        deployerAccount.publicKey,
        destinationAta,
        user1Account.publicKey,
        mintAccount,
        TOKEN_2022_PROGRAM_ID,
      );
      tx.add(ix);
      // await provider.sendAndConfirm(tx, [user1Account]);
      console.log("Created destination ATA:", destinationAta.toBase58());
    }

    // Transfer amount: 100 tokens (considering 9 decimals)
    const amount = new anchor.BN(1000).mul(new anchor.BN(1000000000)); // 1000 tokens
    const fee = amount.mul(new anchor.BN(100)).div(new anchor.BN(10000));

    const transferIx = createTransferCheckedWithFeeInstruction(
      sourceAta,
      mintAccount,
      destinationAta,
      deployerAccount.publicKey,
      BigInt(amount.toString()),
      9,
      BigInt(fee.toString()),
      [],
      TOKEN_2022_PROGRAM_ID,
    )

    // Send transaction
    tx.add(transferIx);
    const signature = await provider.sendAndConfirm(tx, [deployerAccount]);
    console.log("Transfer transaction signature:", signature);

    // Log balances
    const sourceBalance = await provider.connection.getTokenAccountBalance(sourceAta);
    const destBalance = await provider.connection.getTokenAccountBalance(destinationAta);
    console.log("Source balance after transfer:", sourceBalance.value.uiAmount);
    console.log("Destination balance after transfer:", destBalance.value.uiAmount);
  })
})

上面执行了两个测试程序:

  • 1- 部署一个新的Token,执行后,将交易hash复制到区块链浏览器,可以看到以下交易指令的记录:

  • 2- 执行转账操作 注意当时用Transfer fee extension转账时,需要使用createTransferCheckedWithFeeInstruction()方法创建指令。

执行后,交易记录见下图:

转账了1000个代币,费用为1%,有10个代币进到了收款人的token account中,实际收到990个。

进到收款人的token account中,可以通过Solana Explorer浏览器查看到: