Skip to main content

Overview

Token-2022, also known as the Token Extensions Program, extends the functionality of the original SPL Token Program with additional features called “extensions”. These extensions enable advanced token functionality while maintaining backward compatibility with the core token operations. Anchor’s anchor-spl crate provides full support for Token-2022 extensions through:
  • The token_interface module for basic operations
  • Extension-specific constraint syntax
  • Helper functions for reading extension data
  • CPI functions for extension-specific operations

What are Token Extensions?

Token extensions are optional features that can be added to mints and token accounts. Each extension adds specific functionality:
  • Metadata Pointer - Points to on-chain or off-chain metadata
  • Token Metadata - Stores name, symbol, and URI directly in the mint
  • Transfer Hook - Execute custom logic on every transfer
  • Permanent Delegate - An authority that can always transfer tokens
  • Transfer Fee - Charge fees on token transfers
  • Interest Bearing - Tokens that accrue interest
  • Non-Transferable - Soulbound tokens that cannot be transferred
  • Mint Close Authority - Allows closing mint accounts
  • Group Pointer & Member Pointer - Create token groups and members
  • Confidential Transfers - Privacy-preserving transfers
  • And more…

Using Token-2022 in Anchor

The token_interface module works seamlessly with both token programs. To specifically use Token-2022:
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
use anchor_spl::token_2022::Token2022;

#[derive(Accounts)]
pub struct MyInstruction<'info> {
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    // Use Interface for either program
    pub token_program: Interface<'info, TokenInterface>,
    // Or explicitly require Token-2022
    // pub token_program: Program<'info, Token2022>,
}

Enabling Extensions on Mints

Extensions are enabled when creating a mint using the extensions constraint syntax:
#[account(
    init,
    payer = payer,
    mint::decimals = 0,
    mint::authority = authority,
    mint::token_program = token_program,
    // Extensions
    extensions::metadata_pointer::authority = authority,
    extensions::metadata_pointer::metadata_address = mint,
    extensions::transfer_hook::authority = authority,
    extensions::transfer_hook::program_id = crate::ID,
)]
pub mint: InterfaceAccount<'info, Mint>,

Common Extensions

Metadata Pointer

The metadata pointer extension tells wallets and apps where to find token metadata.
#[account(
    init,
    payer = payer,
    mint::decimals = 9,
    mint::authority = authority,
    mint::token_program = token_program,
    // Point to metadata stored in the mint itself
    extensions::metadata_pointer::authority = authority,
    extensions::metadata_pointer::metadata_address = mint,
)]
pub mint: InterfaceAccount<'info, Mint>,
Key fields:
  • authority - Who can update the pointer
  • metadata_address - Where the metadata is stored (often the mint itself)

Token Metadata

Store token metadata (name, symbol, URI) directly in the mint account.
use anchor_spl::token_interface::{
    token_metadata_initialize,
    TokenMetadataInitialize,
};

pub fn initialize_metadata(
    ctx: Context<InitializeMetadata>,
    name: String,
    symbol: String,
    uri: String,
) -> Result<()> {
    let cpi_accounts = TokenMetadataInitialize {
        program_id: ctx.accounts.token_program.to_account_info(),
        metadata: ctx.accounts.mint.to_account_info(),
        update_authority: ctx.accounts.authority.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        mint_authority: ctx.accounts.authority.to_account_info(),
    };

    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts,
    );

    token_metadata_initialize(cpi_ctx, name, symbol, uri)?;
    Ok(())
}

#[derive(Accounts)]
pub struct InitializeMetadata<'info> {
    pub authority: Signer<'info>,
    #[account(
        mut,
        extensions::metadata_pointer::metadata_address = mint,
    )]
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
}

Transfer Hook

Execute custom program logic on every token transfer.
#[account(
    init,
    payer = payer,
    mint::decimals = 0,
    mint::authority = authority,
    mint::token_program = token_program,
    extensions::transfer_hook::authority = authority,
    extensions::transfer_hook::program_id = crate::ID,
)]
pub mint: InterfaceAccount<'info, Mint>,
Use cases:
  • Enforce transfer restrictions
  • Charge custom fees
  • Update on-chain state on transfers
  • Implement loyalty programs

Permanent Delegate

An authority that can always transfer tokens from any account, useful for:
  • Asset recovery
  • Regulatory compliance
  • Game mechanics
#[account(
    init,
    payer = payer,
    mint::decimals = 9,
    mint::authority = authority,
    mint::token_program = token_program,
    extensions::permanent_delegate::delegate = authority,
)]
pub mint: InterfaceAccount<'info, Mint>,

Mint Close Authority

Allows closing a mint account to recover rent, useful for temporary tokens.
#[account(
    init,
    payer = payer,
    mint::decimals = 6,
    mint::authority = authority,
    mint::token_program = token_program,
    extensions::close_authority::authority = authority,
)]
pub mint: InterfaceAccount<'info, Mint>,

Transfer Fee

Charge a fee on every transfer, with fees accumulating in token accounts.
#[account(
    init,
    payer = payer,
    mint::decimals = 9,
    mint::authority = authority,
    mint::token_program = token_program,
    extensions::transfer_fee_config::transfer_fee_config_authority = authority,
    extensions::transfer_fee_config::withdraw_withheld_authority = authority,
    extensions::transfer_fee_config::transfer_fee_basis_points = 100, // 1%
    extensions::transfer_fee_config::maximum_fee = 1_000_000,
)]
pub mint: InterfaceAccount<'info, Mint>,

Group Pointer & Member Pointer

Create relationships between tokens, useful for NFT collections.
#[account(
    init,
    payer = payer,
    mint::decimals = 0,
    mint::authority = authority,
    mint::token_program = token_program,
    extensions::group_pointer::authority = authority,
    extensions::group_pointer::group_address = mint,
    extensions::group_member_pointer::authority = authority,
    extensions::group_member_pointer::member_address = mint,
)]
pub mint: InterfaceAccount<'info, Mint>,

Reading Extension Data

Access extension data from mint accounts using helper functions:
use anchor_spl::token_2022::spl_token_2022::extension::{
    metadata_pointer::MetadataPointer,
    permanent_delegate::PermanentDelegate,
};
use anchor_spl::token_interface::get_mint_extension_data;

pub fn check_extensions(ctx: Context<CheckExtensions>) -> Result<()> {
    let mint_info = ctx.accounts.mint.to_account_info();

    // Read metadata pointer extension
    let metadata_pointer = get_mint_extension_data::<MetadataPointer>(&mint_info)?;
    msg!("Metadata address: {:?}", metadata_pointer.metadata_address);

    // Read permanent delegate extension
    let permanent_delegate = get_mint_extension_data::<PermanentDelegate>(&mint_info)?;
    msg!("Permanent delegate: {:?}", permanent_delegate.delegate);

    Ok(())
}

#[derive(Accounts)]
pub struct CheckExtensions<'info> {
    pub mint: InterfaceAccount<'info, Mint>,
}

Complete Example: NFT with Metadata

Here’s a complete example creating an NFT with metadata using Token-2022:
lib.rs
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_2022::Token2022,
    token_interface::{
        self, token_metadata_initialize, Mint, TokenAccount, TokenInterface,
        TokenMetadataInitialize,
    },
};

declare_id!("YourProgramIDHere");

#[program]
pub mod nft_with_metadata {
    use super::*;

    pub fn create_nft(
        ctx: Context<CreateNFT>,
        name: String,
        symbol: String,
        uri: String,
    ) -> Result<()> {
        // Initialize token metadata
        let cpi_accounts = TokenMetadataInitialize {
            program_id: ctx.accounts.token_program.to_account_info(),
            metadata: ctx.accounts.mint.to_account_info(),
            update_authority: ctx.accounts.payer.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            mint_authority: ctx.accounts.payer.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );

        token_metadata_initialize(cpi_ctx, name, symbol, uri)?;

        // Mint one token to the recipient
        let cpi_accounts = token_interface::MintTo {
            mint: ctx.accounts.mint.to_account_info(),
            to: ctx.accounts.token_account.to_account_info(),
            authority: ctx.accounts.payer.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );

        token_interface::mint_to(cpi_ctx, 1)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct CreateNFT<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    /// The NFT mint with metadata extension
    #[account(
        init,
        payer = payer,
        mint::decimals = 0,
        mint::authority = payer,
        mint::freeze_authority = payer,
        mint::token_program = token_program,
        extensions::metadata_pointer::authority = payer,
        extensions::metadata_pointer::metadata_address = mint,
    )]
    pub mint: InterfaceAccount<'info, Mint>,

    /// The recipient's token account
    #[account(
        init,
        payer = payer,
        associated_token::mint = mint,
        associated_token::authority = recipient,
        associated_token::token_program = token_program,
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,

    /// CHECK: Can be any account
    pub recipient: UncheckedAccount<'info>,

    pub token_program: Program<'info, Token2022>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

Validating Extension Constraints

You can validate extension configurations in your accounts:
#[derive(Accounts)]
pub struct ValidateExtensions<'info> {
    #[account(
        // Validate metadata pointer
        extensions::metadata_pointer::authority = authority,
        extensions::metadata_pointer::metadata_address = mint,
        // Validate permanent delegate
        extensions::permanent_delegate::delegate = authority,
        // Validate transfer hook
        extensions::transfer_hook::authority = authority,
        extensions::transfer_hook::program_id = crate::ID,
    )]
    pub mint: InterfaceAccount<'info, Mint>,
    pub authority: Signer<'info>,
}

Calculating Mint Size with Extensions

Mints with extensions require more space. Use the helper function:
use anchor_spl::token_interface::{ExtensionsVec, find_mint_account_size};
use spl_token_2022::extension::ExtensionType;

// Calculate required space
let extensions = vec![
    ExtensionType::MetadataPointer,
    ExtensionType::TransferHook,
];

let space = find_mint_account_size(Some(&extensions))?;

Best Practices

  1. Use Token-2022 for new projects - More features, same core operations
  2. Initialize extensions before metadata - Some extensions must be set during mint creation
  3. Store metadata in the mint - Saves accounts and transaction complexity
  4. Validate extension configurations - Use constraints to ensure extensions are configured correctly
  5. Handle extension size - Mint accounts with extensions need more space
  6. Test extension combinations - Some extensions work well together, others may have interactions

Available Extensions

Full list of supported extensions:
  • metadata_pointer - Points to metadata location
  • group_pointer - Points to token group
  • group_member_pointer - Points to group member
  • transfer_hook - Custom transfer logic
  • permanent_delegate - Always-authorized delegate
  • close_authority - Mint close authority
  • transfer_fee_config - Transfer fees configuration
  • transfer_fee_amount - Per-account fee tracking
  • mint_close_authority - Close mint authority (legacy)
  • confidential_transfer - Privacy features
  • default_account_state - New accounts default state
  • immutable_owner - Cannot change owner
  • memo_transfer - Require memo on transfers
  • non_transferable - Soulbound tokens
  • interest_bearing_config - Interest-bearing tokens
  • cpi_guard - Restrict CPI usage

Resources

Next Steps

Build docs developers (and LLMs) love