Tempo Mainnet is now live on Chainstack! Get Tempo RPC endpoint for free!    Learn more
  • Pricing
  • Docs

Solana Token-2022: transfer hooks and fee-on-transfer

Created Feb 19, 2026 Updated Mar 19, 2026

Solana Token-2022 adds extensibility to SPL tokens, but not all extensibility lives at the same layer.

Two features are often discussed together: the fee-on-transfer extension and transfer hooks. Both are triggered during a token transfer, and both influence what happens when value moves between accounts.

This similarity is misleading.

These features exist at different layers of the system, solve different classes of problems, and are subject to very different execution constraints. Treating them as interchangeable leads to designs that work in theory, pass simulation, and then fail at runtime.

In this post, we’ll start with the fee-on-transfer extension, how it works and why it exists. Then move to transfer hooks, and finally compare what is and is not possible with each.

Fee-on-transfer extension: Defining token economics

When people hear “fee on transfer,” they usually imagine a separate program that runs during a transfer and skims a percentage to a treasury.

Token-2022 does it differently.

The Fee-on-Transfer extension makes the fee part of the token program’s native transfer logic. In other words, it changes what a “transfer” means for that mint, at the protocol level.

What it is

A Token-2022 mint can be configured with:

  • fee rate (basis points), and
  • maximum fee cap (so big transfers don’t create absurd fees).

Once enabled, every transfer of that mint automatically applies the fee inside the Token-2022 program.

What actually happens during a transfer

Suppose Alice sends Bob 100 tokens and the mint charges a fee.

At execution time, Token-2022:

  1. calculates the fee from the mint’s fee config,
  2. deducts it from the transferred amount, and
  3. withholds the fee rather than immediately paying it out to an arbitrary address.

That “withheld” detail matters a lot.

Where the fees go

With transfer fees enabled, the fee amount is tracked in Solana Token-2022 extension state (commonly recorded on the recipient token account via the transfer-fee amount extension). The recipient can’t spend those withheld tokens as part of their normal balance.

Later, a withdraw authority (configured on the mint) can withdraw accumulated withheld fees.

There’s also a practical mechanism to harvest withheld fees from many token accounts into the mint before withdrawing, which matters when a token has lots of holders.

Why this extension exists

This extension exists because “transfer tax” is not just extra logic it is token economics:

  • It changes how much value moves in the transfer.
  • It must be deterministic (same inputs → same result).
  • It must be enforced atomically by the token program, without relying on external programs, CPIs, wallets, or simulation quirks.

So you can think of it like this:

Fee-on-transfer is not a hook.
It’s a rule baked into the token program’s definition of a transfer.

Example

lets set the environment

solana config set --url devnet

Create two wallets (sender + recipient) and fund them

solana-keygen new --no-bip39-passphrase -o ./alice.json
solana-keygen new --no-bip39-passphrase -o ./bob.json
solana airdrop 1 -k ./alice.json
solana airdrop 1 -k ./bob.json

Create a Solana Token-2022 mint with transfer fees:

Important: spl-token flag names can vary by version. First, confirm what your CLI supports:

spl-token create-token --help | grep -i fee

Most recent CLIs support enabling fees at mint creation using the flags for:

  • --transfer-fee-basis-points
  • --transfer-fee-maximum-fee

set the primary private key

solana config set --keypair ./alice.json

Example: 1% fee, max fee 5 tokens0 decimals (so it’s easy to see):

MINT=$(spl-token create-token \
  --program-2022 \
  --decimals 0 \
  --transfer-fee-basis-points 100 \
  --transfer-fee-maximum-fee 5 \
  | awk '/Creating token/ {print $3}')
echo "MINT=$MINT"
# Output:
# MINT=9ncw8asA7P79w7vJ3XbUFWW4EKy7cSYRVdhp9xjXb9cf

On explorer it will look like this:

Note: Max fee is set to 5 means that the fee will never take more than 5 tokens

create Alice’s token account:

ALICE_ATA=$(spl-token create-account $MINT --program-2022 | awk '/Creating account/ {print $3}')
echo "ALICE_ATA=$ALICE_ATA"
# Output:
# ALICE_ATA=J9V83VXc4H1LcCHu6DjxKLn6tkem5Se4ZYiybLnqoG35

On explorer it will look like this:

create Bob’s token account

# switch to bob
solana config set --keypair ./bob.json
BOB_ATA=$(spl-token create-account $MINT --program-2022 | awk '/Creating account/ {print $3}')
echo "BOB_ATA=$BOB_ATA"
# Output:
# BOB_ATA=G5DjAMaTSrTajDK3Y9EW1dwSsXz6SUWQdvpu9Ft3CNif
# Then switch back to Alice for minting/transferring:
solana config set --keypair ./alice.json 

On explorer it will look like this:

After you have both ATAs lets mint and transfer:

# Alice mints to herself
spl-token mint $MINT 1000 $ALICE_ATA --program-2022
# Alice transfers to Bob
spl-token transfer $MINT 100 $BOB_PUBKEY \
  --from $ALICE_ATA \
  --fund-recipient \
  --program-2022

lets check the balance of Bob to see how much he got

spl-token balance $MINT \
  --owner $BOB_PUBKEY \
  --program-2022
# Output:
# 99

On explorer it will look like this:

Transfer hooks

If Fee-on-Transfer is a built-in “tax law” of the token program, a Transfer Hook is a “custom security checkpoint.” It allows a token creator to mandate that an external, custom program must approve every single transfer before it can finalize.

What it is

The Transfer Hook extension allows a Mint to specify a Program ID that must be called during any transfer. This essentially turns a standard token into a programmable asset.

When a transfer is initiated:

  1. Solana Token-2022 sees the hook extension on the Mint.
  2. It pauses the transfer logic and sends a Cross-Program Invocation (CPI) to your custom program.
  3. Your hook program runs its logic and returns either a “Success” or an “Error”.

Why this is a game-changer

Before Token-2022, if you wanted to enforce custom logic you had to “wrap” the token in a custom contract or “freeze” it and only “unfreeze” it via a proxy program. This destroyed composability, wallets and DEXs didn’t know how to talk to your proxy.

Hooks fix this by putting the logic directly into the standard transfer flow.

Execution-Time Side Effects

Transfer Hook is Solana Token-2022’s way to run your program during a token transfer. But the key detail is who initiates it:

The user does not call your hook program. The Token-2022 program calls it as part of processing a transfer.

So instead of “a token transfer plus my callback”, the mental model is:

A token transfer is a pipeline, and one stage of that pipeline can invoke your program.

What “hooked” actually means

When a mint has the transfer-hook extension enabled, Token-2022 stores:

  • the hook program id
  • and a configuration describing extra accounts the hook will need

Then, whenever someone transfers that mint, Token-2022 will:

  1. validate the transfer
  2. perform its internal extension logic
  3. invoke the hook program (CPI) with the transfer context

This invocation is automatic and deterministic. If the hook fails, the transfer fails.

Why hooks exist

Transfer hooks are not meant to redefine the economics of the token (that’s what fee-on-transfer is for). Hooks exist for things that are inherently conditional and policy-driven, like:

Gating / compliance:

  • allowlist / blocklist
  • KYC / region gating
  • “only transfers to approved destinations”
  • “only transfers above/below a threshold”

Runtime enforcement

  • pause transfers based on some on-chain flag
  • rate-limit transfers
  • require “membership” / “badge” / “NFT ownership” to receive

The common theme:

Note: Hooks let you add policy and stateful side effects to transfers.

The most important constraint

When your hook runs, it’s not a top-level instruction. It’s running inside the Token-2022 transfer instruction. That means two consequences that matter a lot:

1) No privilege escalation

Accounts passed into your hook have fixed privileges (read-only / writable, signer / non-signer). Your program cannot “upgrade” an account’s permissions mid-flight.
If Solana Token-2022 passed an account as read-only, your hook cannot treat it as writable.

2) The hook cannot change what the transfer is

A hook can:

  • validate
  • reject
  • record
  • perform side effects on explicitly provided accounts

But it cannot retroactively redefine the transfer semantics the way a token-program-level extension can. This is exactly why “take a fee in the same token inside the hook” fails in practice.

Why hooks cannot implement token-level fees

After setting up a hook, the first instinct for many developers is to add a “tax” or “royalty” logic. However, if you try to make your hook move tokens to a treasury during a transfer, you will hit two fundamental architectural walls.

1. The “Read-Only” Wall

When the Token-2022 program invokes your hook, it provides the source and destination token accounts. However, for security reasons, these accounts are passed to your hook as read-only.

  • The Constraint: You cannot modify the data or the balance of a read-only account.
  • The Result: Since your hook cannot “write” to the balances, it cannot deduct a fee.

2. The Re-entrancy Lock

You might think: “I’ll just issue a CPI (Cross-Program Invocation) from my hook back to Token-2022 to move the 1% fee.”

  • The Constraint: Solana prohibits “re-entrancy.” You cannot call the Token-2022 program to move a token while that same Token-2022 program is currently suspended, waiting for your hook to finish.
  • The Result: The transaction will fail immediately.

Note: In next blog post we will write code and see this in practice

What is possible with each mechanism

The core difference lies in state mutation (writing) versus state validation (reading).

Modifying the transfer amount

  • Fee-on-Transfer: Yes. Because this logic lives inside the Token-2022 program, it has the authority to split the transfer amount. If Alice sends 100, the program physically writes 99 to Bob and 1 to a withheld bucket.
  • Transfer Hooks: No. Your hook program is a guest. The accounts it receives are restricted by the caller (Token-2022). To prevent a hook from “stealing” tokens, the runtime passes the source and destination accounts as Read-Only. You can see the 100 tokens, but you cannot change that number to 99.

Gating by identity or metadata

  • Fee-on-Transfer: No. This extension is “blind.” It applies the same math to every transfer regardless of who is involved. You cannot say “charge 1% except for these five wallets” using only this extension.
  • Transfer Hooks: Yes. This is the hook’s superpower. Because the hook is a custom program, it can look at a MemberAccount PDA or an external Registry to see if Alice or Bob are authorized. If they aren’t, it returns an error and the whole transfer is aborted.

Side-effects in external programs

  • Fee-on-Transfer: No. It does one thing: math. It cannot ping a different program to update a “Points” balance or log a trade on an external leaderboard.
  • Transfer Hooks: Yes. Your hook can be passed writable extra accounts. For example, you could pass a UserStats PDA. Every time a user transfers tokens, your hook could increment a “Total Volume” counter on that PDA.

Choosing the correct layer

The decision tree is simple:

  • Does it change the math of the transfer? Use Fee-on-Transfer. It is the only way to ensure the fee is deducted atomically and safely at the protocol level.
  • Does it change the permission of the transfer? Use Transfer Hooks. It is the only way to check external state (like a denylist PDA or an NFT) to decide if a transfer should be allowed to happen at all.

Summary

Solana Token-2022 isn’t just about “more features”, it’s about putting those features at the correct layer of the stack.

  • Fee-on-Transfer handles the the math. It ensures your token economics are immutable, deterministic, and enforced natively.
  • Transfer Hooks handle the the policy. They turn your token into a programmable asset that can react to the world around it.

Coming up next: In the next blog post, we’re going to get our hands dirty. We’ll write a real Transfer Hook in Anchor, deploy it, and see exactly how to pair it with a Fee-on-Transfer mint to build a fully regulated token economy.

Learn more about Solana architecture from our articles

Reliable Solana RPC infrastructure

Getting started with Solana on Chainstack is fast and straightforward. Developers can deploy a reliable Solana node within seconds through an intuitive Console — no complex setup or hardware management required. 

Chainstack provides low-latency Solana RPC access and real-time gRPC data streaming via Yellowstone Geyser Plugin, ensuring seamless connectivity for building, testing, and scaling DeFi, analytics, and trading applications. With Solana low-latency endpoints powered by global infrastructure, you can achieve lightning-fast response times and consistent performance across regions. 

Start for free, connect your app to a reliable Solana RPC endpoint, and experience how easy it is to build and scale on Solana with Chainstack – one of the best RPC providers.

SHARE THIS ARTICLE

Andrey Obruchkov

Senior Software Engineer and blockchain specialist with hands-on experience building across EVM, Solana, Bitcoin, Cosmos, Aptos, and Sui ecosystems from smart contracts and DeFi infrastructure to cross-chain integrations and developer tooling.

Customer Stories

Kenshi Oracle Network

Contributing towards a swifter Web3 for all with secure, efficient, and robust oracle infrastructure.

Lootex

Leveraging robust infrastructure in obtaining stable performance for a seamless user experience.

UniWhales

Growing and strengthening the analytics community with impeccable infrastructure.