Răsfoiți Sursa

anchor transfer-fee example

John 1 an în urmă
părinte
comite
bd9b2bad90

+ 7 - 0
tokens/token-2022/transfer-fees/anchor/.gitignore

@@ -0,0 +1,7 @@
+.anchor
+.DS_Store
+target
+**/*.rs.bk
+node_modules
+test-ledger
+.yarn

+ 7 - 0
tokens/token-2022/transfer-fees/anchor/.prettierignore

@@ -0,0 +1,7 @@
+.anchor
+.DS_Store
+target
+node_modules
+dist
+build
+test-ledger

+ 18 - 0
tokens/token-2022/transfer-fees/anchor/Anchor.toml

@@ -0,0 +1,18 @@
+[toolchain]
+
+[features]
+resolution = true
+skip-lint = false
+
+[programs.localnet]
+transfer_fee = "4evptdGtALCNT8uTxJhbWBRZpBE8w5oNtmgfSyfQu7td"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Localnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 14 - 0
tokens/token-2022/transfer-fees/anchor/Cargo.toml

@@ -0,0 +1,14 @@
+[workspace]
+members = [
+    "programs/*"
+]
+resolver = "2"
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1

+ 12 - 0
tokens/token-2022/transfer-fees/anchor/migrations/deploy.ts

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@coral-xyz/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+};

+ 20 - 0
tokens/token-2022/transfer-fees/anchor/package.json

@@ -0,0 +1,20 @@
+{
+  "scripts": {
+    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.30.0",
+    "@solana/spl-token": "^0.4.6"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.0",
+    "@types/chai": "^4.3.0",
+    "@types/mocha": "^9.0.0",
+    "chai": "^4.3.4",
+    "mocha": "^9.0.3",
+    "prettier": "^2.6.2",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^4.3.5"
+  }
+}

+ 21 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "transfer-fee"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "transfer_fee"
+
+[features]
+default = []
+cpi = ["no-entrypoint"]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
+
+[dependencies]
+anchor-lang = { version = "0.30.0", features = ["init-if-needed"] }
+anchor-spl = "0.30.0"

+ 2 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 40 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/harvest.rs

@@ -0,0 +1,40 @@
+use anchor_lang::prelude::*;
+use anchor_spl::token_interface::{
+    harvest_withheld_tokens_to_mint, HarvestWithheldTokensToMint, Mint, Token2022, TokenAccount,
+};
+
+#[derive(Accounts)]
+pub struct Harvest<'info> {
+    #[account(mut)]
+    pub mint_account: InterfaceAccount<'info, Mint>,
+    pub token_program: Program<'info, Token2022>,
+}
+
+// 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
+pub fn process_harvest<'info>(ctx: Context<'_, '_, 'info, 'info, Harvest<'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<_>>();
+
+    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
+    )?;
+    Ok(())
+}

+ 117 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/initialize.rs

@@ -0,0 +1,117 @@
+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>,
+}
+
+// There is currently not an anchor constraint to automatically initialize the TransferFeeConfig extension
+// We can manually create and initialize the mint account via CPIs in the instruction handler
+pub fn process_initialize(
+    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])
+            .unwrap();
+
+    // 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
+    )?;
+
+    Initialize::check_mint_data(&ctx.accounts)?;
+
+    Ok(())
+}
+
+// helper to demonstrate how to read mint extension data within a program
+impl<'info> Initialize<'info> {
+    pub fn check_mint_data(&self) -> Result<()> {
+        let mint = &self.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>()?;
+
+        assert_eq!(
+            extension_data.transfer_fee_config_authority,
+            OptionalNonZeroPubkey::try_from(Some(self.payer.key()))?
+        );
+
+        assert_eq!(
+            extension_data.withdraw_withheld_authority,
+            OptionalNonZeroPubkey::try_from(Some(self.payer.key()))?
+        );
+
+        msg!("{:?}", extension_data);
+        Ok(())
+    }
+}

+ 10 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/mod.rs

@@ -0,0 +1,10 @@
+pub mod transfer;
+pub use transfer::*;
+pub mod initialize;
+pub use initialize::*;
+pub mod harvest;
+pub use harvest::*;
+pub mod withdraw;
+pub use withdraw::*;
+pub mod update_fee;
+pub use update_fee::*;

+ 80 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/transfer.rs

@@ -0,0 +1,80 @@
+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 process_transfer(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
+    )?;
+
+    msg!("transfer amount {}", amount);
+    msg!("fee amount {}", fee);
+
+    Ok(())
+}

+ 34 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/update_fee.rs

@@ -0,0 +1,34 @@
+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 process_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(())
+}

+ 31 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/instructions/withdraw.rs

@@ -0,0 +1,31 @@
+use anchor_lang::prelude::*;
+use anchor_spl::token_interface::{
+    withdraw_withheld_tokens_from_mint, Mint, Token2022, TokenAccount,
+    WithdrawWithheldTokensFromMint,
+};
+
+#[derive(Accounts)]
+pub struct Withdraw<'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>,
+}
+
+// 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
+pub fn process_withdraw(ctx: Context<Withdraw>) -> Result<()> {
+    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(())
+}

+ 38 - 0
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/lib.rs

@@ -0,0 +1,38 @@
+use anchor_lang::prelude::*;
+pub mod instructions;
+pub use instructions::*;
+
+declare_id!("4evptdGtALCNT8uTxJhbWBRZpBE8w5oNtmgfSyfQu7td");
+
+#[program]
+pub mod transfer_fee {
+    use super::*;
+
+    pub fn initialize(
+        ctx: Context<Initialize>,
+        transfer_fee_basis_points: u16,
+        maximum_fee: u64,
+    ) -> Result<()> {
+        process_initialize(ctx, transfer_fee_basis_points, maximum_fee)
+    }
+
+    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
+        process_transfer(ctx, amount)
+    }
+
+    pub fn harvest<'info>(ctx: Context<'_, '_, 'info, 'info, Harvest<'info>>) -> Result<()> {
+        process_harvest(ctx)
+    }
+
+    pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
+        process_withdraw(ctx)
+    }
+
+    pub fn update_fee(
+        ctx: Context<UpdateFee>,
+        transfer_fee_basis_points: u16,
+        maximum_fee: u64,
+    ) -> Result<()> {
+        process_update_fee(ctx, transfer_fee_basis_points, maximum_fee)
+    }
+}

+ 144 - 0
tokens/token-2022/transfer-fees/anchor/tests/transfer-fee.ts

@@ -0,0 +1,144 @@
+import * as anchor from "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { TransferFee } from "../target/types/transfer_fee";
+import {
+  TOKEN_2022_PROGRAM_ID,
+  getAssociatedTokenAddressSync,
+  getOrCreateAssociatedTokenAccount,
+  mintTo,
+} from "@solana/spl-token";
+import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
+
+describe("transfer-fee", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  const wallet = provider.wallet as anchor.Wallet;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.TransferFee as Program<TransferFee>;
+
+  const mintKeypair = new anchor.web3.Keypair();
+  const recipient = new anchor.web3.Keypair();
+
+  const senderTokenAccountAddress = getAssociatedTokenAddressSync(
+    mintKeypair.publicKey,
+    wallet.publicKey,
+    false,
+    TOKEN_2022_PROGRAM_ID
+  );
+
+  const recipientTokenAccountAddress = getAssociatedTokenAddressSync(
+    mintKeypair.publicKey,
+    recipient.publicKey,
+    false,
+    TOKEN_2022_PROGRAM_ID
+  );
+
+  it("Create Mint with Transfer Fee", async () => {
+    const transferFeeBasisPoints = 100;
+    const maximumFee = 1;
+
+    const transactionSignature = await program.methods
+      .initialize(transferFeeBasisPoints, new anchor.BN(maximumFee))
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .signers([mintKeypair])
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+
+  it("Mint Tokens", async () => {
+    await getOrCreateAssociatedTokenAccount(
+      connection,
+      wallet.payer,
+      mintKeypair.publicKey,
+      wallet.publicKey,
+      false,
+      null,
+      null,
+      TOKEN_2022_PROGRAM_ID,
+      ASSOCIATED_PROGRAM_ID
+    );
+
+    await mintTo(
+      connection,
+      wallet.payer,
+      mintKeypair.publicKey,
+      senderTokenAccountAddress,
+      wallet.payer,
+      300,
+      [],
+      null,
+      TOKEN_2022_PROGRAM_ID
+    );
+  });
+
+  it("Transfer", async () => {
+    const transactionSignature = await program.methods
+      .transfer(new anchor.BN(100))
+      .accounts({
+        sender: wallet.publicKey,
+        recipient: recipient.publicKey,
+        mintAccount: mintKeypair.publicKey,
+        senderTokenAccount: senderTokenAccountAddress,
+        recipientTokenAccount: recipientTokenAccountAddress,
+      })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+
+  it("Transfer Again, fee limit by maximumFee", async () => {
+    const transactionSignature = await program.methods
+      .transfer(new anchor.BN(200))
+      .accounts({
+        sender: wallet.publicKey,
+        recipient: recipient.publicKey,
+        mintAccount: mintKeypair.publicKey,
+        senderTokenAccount: senderTokenAccountAddress,
+        recipientTokenAccount: recipientTokenAccountAddress,
+      })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+
+  it("Harvest Transfer Fees to Mint Account", async () => {
+    const transactionSignature = await program.methods
+      .harvest()
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .remainingAccounts([
+        {
+          pubkey: recipientTokenAccountAddress,
+          isSigner: false,
+          isWritable: true,
+        },
+        {
+          pubkey: new anchor.web3.Keypair().publicKey,
+          isSigner: false,
+          isWritable: true,
+        },
+      ])
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+
+  it("Withdraw Transfer Fees from Mint Account", async () => {
+    const transactionSignature = await program.methods
+      .withdraw()
+      .accounts({
+        mintAccount: mintKeypair.publicKey,
+        tokenAccount: senderTokenAccountAddress,
+      })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+
+  it("Update Transfer Fee", async () => {
+    const transferFeeBasisPoints = 0;
+    const maximumFee = 0;
+
+    const transactionSignature = await program.methods
+      .updateFee(transferFeeBasisPoints, new anchor.BN(maximumFee))
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", transactionSignature);
+  });
+});

+ 10 - 0
tokens/token-2022/transfer-fees/anchor/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}