Token Extensions: Transfer Hook

·

The Transfer Hook extension and Transfer Hook Interface unlock a new layer of programmability on Solana by enabling custom logic execution during every token transfer. This powerful feature allows developers to build dynamic, rule-based token behaviors—opening doors to innovative use cases in DeFi, NFTs, and tokenized ecosystems.

By integrating the Transfer Hook Interface into a custom program, developers can enforce conditions, track activity, or collect fees each time tokens are moved. Whether you're building royalty-enforcing NFTs, transfer-restricted utility tokens, or fee-based governance systems, this guide walks you through implementation with clarity and precision.


Transfer Hook Interface Overview

The Transfer Hook Interface allows developers to define custom logic that executes on every transfer of tokens from a specific Mint Account. When enabled, the Token 2022 program invokes your designated program via Cross Program Invocation (CPI) during each transfer.

This enables real-time control over transfers, such as:

To implement a transfer hook, your program must comply with the official interface, which defines three core instructions:

👉 Discover how to build powerful token logic with Solana’s latest extensions.

A key security design: during CPI, all original transfer accounts become read-only. This prevents malicious escalation of sender privileges within the hook program.

While native programs can implement this interface, most developers use the Anchor framework for faster, safer development. We'll use Anchor throughout this guide.


Hello World: Basic Transfer Hook

Let’s start with a simple "Hello World" example—a transfer hook that logs a message every time tokens are transferred.

Core Components

This minimal setup includes three instructions:

  1. initialize_extra_account_meta_list: Initializes metadata for additional accounts (empty in this case).
  2. transfer_hook: Executes custom logic during transfer.
  3. fallback: Ensures compatibility between Anchor and the Transfer Hook Interface discriminators.

Here’s the core logic:

pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
    msg!("Hello Transfer Hook!");
    Ok(())
}

Each time a token moves, this function runs. You can extend it—for example, to reject transfers above a threshold:

#[error_code]
pub enum MyError {
    #[msg("The amount is too big")]
    AmountTooBig,
}

pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
    msg!("Hello Transfer Hook!");
    if amount > 50 {
        return err!(MyError::AmountTooBig);
    }
    Ok(())
}

This demonstrates how easily you can inject conditional logic into token behavior.

To test it, deploy the example using Solana Playground. After building and deploying, run the test script to see output like:

✔ Create Mint Account with Transfer Hook Extension
✔ Transfer Hook with Extra Account Meta
4 passing (3s)

You can also create the token via CLI:

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook yourProgramId

Counter Transfer Hook

Now let’s build something more functional: a counter that increments every time tokens are transferred.

Adding State with PDAs

We need a Program Derived Address (PDA) to store the counter:

#[account]
pub struct CounterAccount {
    counter: u64,
}

During initialization, we register this PDA in the ExtraAccountMetaList:

let account_metas = vec![
    ExtraAccountMeta::new_with_seeds(
        &[Seed::Literal { bytes: b"counter".to_vec() }],
        false,
        true,
    )?,
];

Then, in the transfer_hook function:

pub fn transfer_hook(ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
    ctx.accounts.counter_account.counter = ctx
        .accounts
        .counter_account
        .counter
        .checked_add(1)
        .unwrap();
    msg!("This token has been transferred {} times", ctx.accounts.counter_account.counter);
    Ok(())
}

Security Check: Prevent Direct Calls

To ensure the hook only runs during actual transfers, add a guard:

fn assert_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
    let source_token_info = ctx.accounts.source_token.to_account_info();
    let account_data = source_token_info.try_borrow_data()?;
    let account = PodStateWithExtensionsMut::<PodAccount>::unpack(&account_data)?;
    let extension = account.get_extension_mut::<TransferHookAccount>()?;
    if !bool::from(extension.transferring) {
        return err!(TransferError::IsNotCurrentlyTransferring);
    }
    Ok(())
}

Call this at the start of transfer_hook to block unauthorized invocations.

Test results will show messages like:

"This token has been transferred 1 times"

Advanced Example: wSOL Transfer Fee

Let’s build a fee-enforcing transfer hook where users pay in wrapped SOL (wSOL) for every transaction.

Why a Delegate PDA?

Since the sender’s signature isn’t available in the CPI context, we use a delegate PDA—a program-controlled signer—to handle wSOL transfers.

Step 1: Initialize Extra Accounts

We store these in ExtraAccountMetaList:

Using new_external_pda_with_seeds, we derive PDAs dynamically based on account indices.

Step 2: Implement Fee Logic

In transfer_hook, we transfer wSOL from sender to delegate:

transfer_checked(
    CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        TransferChecked {
            from: ctx.accounts.sender_wsol_token_account.to_account_info(),
            mint: ctx.accounts.wsol_mint.to_account_info(),
            to: ctx.accounts.delegate_wsol_token_account.to_account_info(),
            authority: ctx.accounts.delegate.to_account_info(),
        },
        signer_seeds,
    ),
    amount,
    ctx.accounts.wsol_mint.decimals,
)?;

The sender must first approve the delegate for the fee amount.


Getting Started with Solana Playground

Use this starter project to follow along.

  1. Import the project
  2. Run build to generate your program ID
  3. Run deploy to publish to devnet
  4. Execute test to validate functionality

Ensure your wallet has enough devnet SOL. Use the faucet if needed.


Using Token Account Data in Hooks

You can derive PDAs using on-chain data, such as a token account’s owner.

For example, to track per-user transfer counts:

ExtraAccountMeta::new_with_seeds(
    &[
        Seed::Literal { bytes: b"counter".to_vec() },
        Seed::AccountData { account_index: 0, data_index: 32, length: 32 },
    ],
    false,
    true,
)?;

This uses bytes 32–64 of the first account (the source token), which contains the owner’s public key.

On-chain structure:

pub struct Account {
    pub mint: Pubkey,
    pub owner: Pubkey, // starts at byte 32
    pub amount: u64,
    // ...
}

In Anchor:

#[account(seeds = [b"counter", owner.key().as_ref()], bump)]
pub counter_account: Account<'info, CounterAccount>,

The client auto-resolves this using helper functions like createTransferCheckedWithTransferHookInstruction.


Conclusion

The Transfer Hook extension transforms static tokens into dynamic assets capable of enforcing rules, collecting fees, and responding to behavior. From simple logging to complex fee models, the possibilities are vast.

By combining Anchor’s developer-friendly framework with Solana’s high-performance architecture, you can build secure, scalable token logic that evolves with your ecosystem.

Whether you're launching a community token with built-in governance fees or an NFT collection with perpetual royalties, Transfer Hooks give you the tools to innovate.

👉 Start building your next-generation token economy today.


Frequently Asked Questions

Q: What is a Transfer Hook in Solana?
A: A Transfer Hook is a program that executes custom logic during every token transfer from a specific mint. It enables features like fees, access control, and analytics.

Q: Can I charge fees in SOL using Transfer Hooks?
A: Not directly in native SOL, but you can use wrapped SOL (wSOL). The hook transfers wSOL from sender to a designated account using a delegate PDA.

Q: How do I prevent someone from bypassing the hook?
A: Use the transferring flag in the token account extension to verify the call originates from an actual transfer CPI.

Q: Are Transfer Hooks compatible with all wallets?
A: Most modern wallets support Token 2022 and Transfer Hooks, but always test with your target audience. Some older interfaces may not handle extra accounts correctly.

Q: Can I update or disable a Transfer Hook after deployment?
A: You can update the hook program logic if designed for upgrades, but you cannot disable the extension without recreating the mint.

Q: What are ExtraAccountMetas?
A: They define additional accounts required by the transfer hook. These are stored in a PDA and automatically added to transfer instructions by SDK helpers.


Core Keywords: Transfer Hook, Solana Token 2022, Token Extensions, Custom Token Logic, Anchor Framework, wSOL Fee, Program Derived Address (PDA), Cross Program Invocation (CPI)