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:
- Enforcing NFT royalties on secondary sales
- Whitelisting or blacklisting wallet addresses
- Charging dynamic transfer fees
- Tracking transfer analytics
- Triggering custom events
To implement a transfer hook, your program must comply with the official interface, which defines three core instructions:
Execute
: Called on every transferInitializeExtraAccountMetaList
(optional): Sets up additional required accountsUpdateExtraAccountMetaList
(optional): Updates the list of extra accounts
👉 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:
initialize_extra_account_meta_list
: Initializes metadata for additional accounts (empty in this case).transfer_hook
: Executes custom logic during transfer.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
:
- wSOL mint (
NATIVE_MINT
) - Token and associated token programs
- Delegate PDA
- Delegate’s wSOL token account
- Sender’s wSOL token account
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.
- Import the project
- Run
build
to generate your program ID - Run
deploy
to publish to devnet - 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)