Skip to content

Token-2022 Extensions

The last three pages were about making state smaller: state compression and ZK compression both shrink the on-chain footprint of accounts you already understand. This page goes the other way. It asks: given the account model, how do you make a token do more — charge a fee on every transfer, hide its amounts, run custom logic when it moves — without inventing a new contract for each behavior?

The answer is Token-2022, also called Token Extensions. It is the second official token program, and it extends the mint/token-account model you met in The SPL Token Program by letting a mint declare capabilities at creation time as extra data on the account. That last phrase is the whole point, and it is the same up-front-declaration principle that makes Sealevel work — so this page is really about what happens when you push “capabilities are visible on the account” all the way into the token standard.

Recall the honest footnote from the SPL Token page: there are now two token programs, not one.

  • SPL Token — the original, deployed at TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA. Simple, battle-tested, universally supported.
  • Token-2022 (Token Extensions) — a separate program at TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb, deployed later, that keeps the original’s data layout as a prefix and appends optional extensions after it.

The critical word is successor, not replacement. Token-2022 was deliberately built to be backward-compatible in layout: a Token-2022 mint or token account begins with exactly the same fields as a classic one (supply, decimals, mint, owner, amount, …), so code that reads the base fields still works. But it is a different program at a different address, and that is not a cosmetic detail:

A mint declares which program owns it — and that choice is permanent.
USDC mint ── owned by ─► SPL Token program (classic)
PYUSD mint ── owned by ─► Token-2022 program (extensions)
A token account is ALWAYS owned by whichever program its mint uses.
You cannot "upgrade" a classic mint to Token-2022 in place — the
owner field is fixed at creation. New tokens opt in; old ones don't.

Because the owner of a mint account is set when the account is created and never changes, mints choose their program up front. There is no migration switch. This is exactly the account model from Everything is an account doing its job: the owning program is a property of the account, and only the owner may write its data. Two token programs simply means two possible owners, and a mint picks one for life.

The extension model: capabilities as account data

Section titled “The extension model: capabilities as account data”

Here is the mechanism, and it falls straight out of the account model. In classic SPL Token, a mint account is a fixed-size struct and a token account is another fixed-size struct. Token-2022 keeps those structs as the base, then appends a TLV (type-length-value) region of extensions after them:

Token-2022 mint account
┌────────────────────────────────────────────────┐
│ BASE (identical to classic SPL Token mint) │
│ supply, decimals, mint_authority, ... │
├────────────────────────────────────────────────┤
│ EXTENSIONS (TLV: type, length, value ...) │
│ [TransferFeeConfig | len | fee bps, cap, ...] │
│ [TransferHook | len | program id ] │
│ [InterestBearing | len | rate, ts ] │
│ [TokenMetadata | len | name, symbol, uri] │
└────────────────────────────────────────────────┘

Some extensions live on the mint (they describe the token: a fee rate, an interest rate, a hook program, metadata). Others live on the token account (they describe one holder’s balance: whether it is confidential, how much fee it has withheld). An extension is chosen when the account is created — you allocate a mint large enough to hold the extensions you want, then initialize each one before initializing the mint itself. After that the set of extensions is largely fixed.

That “declare at creation, then it’s fixed” shape is the important part. A token does not gain a transfer fee by having someone deploy a wrapper contract around it later. It is born with a transfer fee, and that fact is readable directly off the mint account by anyone, offline, before they ever transact:

Wrapper-contract model (Ethereum-style):
token behavior = whatever some *other* contract does when it calls transfer.
To know if a token has a fee, you must find and read that wrapper's code.
Token-2022 model:
token behavior = flags and config in the mint's own extension data.
To know if a token has a fee, you read one account. No code archaeology.

This is the same trade the whole book keeps making: move information from “hidden in code you must execute” to “declared in named account data you can inspect.” Declared footprints did it for scheduling; extensions do it for token semantics.

Token-2022 ships a couple dozen extensions. Five carry the weight of the design, so understand these and the rest are variations.

A TransferFeeConfig extension on the mint says: on every transfer of this token, withhold a fee (in basis points, up to a per-transfer cap). The fee is not sent anywhere immediately — it is withheld on the recipient’s token account, and a designated withdraw-withheld authority later harvests the accumulated fees. Crucially, the fee is enforced by the token program itself, not by a wrapper the sender might route around.

Alice transfers 1,000 tokens, fee = 100 bps (1%), cap = 50
→ Bob's token account receives 990 (post-fee)
→ 10 tokens are withheld ON Bob's account
→ later, the fee authority withdraws the withheld 10

This is why “protocol-level” matters: with an ERC-20 wrapper, a savvy user can often transfer the underlying token directly and skip the wrapper’s fee. With a Token-2022 transfer fee, there is no underlying token to reach — the fee is a property of the only token that exists.

Transfer hooks — run custom logic on transfer

Section titled “Transfer hooks — run custom logic on transfer”

A TransferHook extension on the mint names a separate program that the token program will call into on every transfer (via cross-program invocation). This is the escape hatch for behavior the fixed extensions can’t express: enforce a whitelist, update a royalty ledger, block transfers during a lockup, record analytics.

The hook does not get to silently mutate arbitrary state. It runs under the same rules as any program: it can only touch the accounts passed to the instruction, and — this is the design tie-back — those extra accounts must be declared so the runtime still knows the transaction’s full footprint up front.

transfer(Token-2022) ──CPI──► hook program's `execute`
│ │
│ passes: source, dest, │ may read/write only the
│ mint, + extra accounts │ declared extra accounts
│ resolved from an ▼
│ "extra account metas" PDA e.g. a whitelist PDA, a royalty vault

Because the extra accounts a hook needs are resolved from a PDA the mint publishes, the transaction can still name every account it will touch before it runs — preserving the Sealevel-style predictability from The conflict rule. The hook is powerful, but it is powerful within the declared-footprint model, not an exception to it.

Confidential transfers — amounts hidden with ZK

Section titled “Confidential transfers — amounts hidden with ZK”

The ConfidentialTransfer extension lets a token account hold an encrypted balance and move amounts without revealing them on-chain. It uses ElGamal encryption plus zero-knowledge proofs (range proofs and equality proofs) so the runtime can verify a transfer is valid — no negative balances, inputs equal outputs — without learning the amount.

The account model is what makes this legible. A confidential balance is still a named token account you declare; what changes is that its amount field is ciphertext, and the transfer instruction carries proofs the program checks. Ownership, which mint, and that a transfer happened stay public; only the number is hidden. This is a close cousin of the machinery in ZK compression — proofs standing in for data the chain would rather not store or reveal — applied to balances instead of account existence.

Interest-bearing tokens — a display that accrues

Section titled “Interest-bearing tokens — a display that accrues”

The InterestBearingConfig extension stores an interest rate and a timestamp on the mint. The token’s raw amount does not change, but the program exposes a function that computes the UI amount by applying continuous interest since the timestamp. It is a display transformation — the ledger balance is unchanged, but wallets show a value that grows over time — useful for rebasing-style or yield-bearing representations without rewriting every holder’s balance on a schedule.

Metadata — name, symbol, and URI on the mint itself

Section titled “Metadata — name, symbol, and URI on the mint itself”

Classic SPL tokens carry no name or symbol; that data lived in a separate account maintained by Metaplex. The TokenMetadata extension (with MetadataPointer) lets a mint store its name, symbol, and URI in the mint account’s own extension data. The token’s identity and its human-readable metadata become one account instead of two — fewer accounts to derive, fetch, and keep consistent.

Under the hood — why “declare at creation” is load-bearing

Section titled “Under the hood — why “declare at creation” is load-bearing”

It is worth being precise about why extensions are fixed at creation rather than toggleable later, because it is the same reasoning that runs through Solana’s whole design.

If a token could gain a transfer fee or a transfer hook after wallets and DEXes already integrated it, every integrator would face a moving target: code that was correct yesterday silently starts mishandling the token today. By forcing capabilities to be declared when the mint is created — and encoded in inspectable account data — Token-2022 makes a token’s behavior a stable, readable contract. An integrator can look at a mint once, see its full extension set, and decide whether it can support it.

Integrator's question, answered by reading ONE account:
"Does this mint have a transfer fee? A transfer hook? Confidential
transfers? Is it non-transferable?"
→ all answered from the mint's extension TLV, before any transfer.

This is the account model’s promise — capabilities are visible on the account — extended from “which program owns it” to “what does it do.” It is the same up-front-declaration discipline that lets the scheduler run transfers in parallel, now applied to what a token means.

The trade-offs: richer tokens, harder integration

Section titled “The trade-offs: richer tokens, harder integration”

Nothing is free, and Token-2022’s cost lands squarely on the ecosystem, not the chain.

Every extension is a new behavior a wallet, DEX, or lending market must explicitly support. A DEX that assumes “amount sent equals amount received” is silently wrong for a fee-bearing token. A pool that calls transfer without carrying the hook’s extra accounts will simply fail. A UI that reads the base amount and ignores the interest-bearing config shows a stale number. So the standard can ship a capability years before the ecosystem safely handles it — feature support lags feature existence, and the lag is the real cost.

  • Why does it exist? To let tokens carry native, composable behaviors — fees, hooks, privacy, metadata — as account data declared at creation, instead of forcing every project to wrap a token in bespoke contracts that the ecosystem then can’t read uniformly.
  • What problem does it solve? The wrapper-contract problem: on a wrapper model, a token’s real behavior is hidden in whatever code calls transfer, so it’s invisible until executed and easy to route around. Extensions put behavior in the mint’s own inspectable data, enforced by the token program itself.
  • What are the trade-offs? You gain standard, inspectable, protocol-enforced capabilities. You pay with integration complexity — every wallet and DEX must support each extension — and new footguns (fees and hooks breaking code that assumed classic semantics), so ecosystem support lags the feature set.
  • When should I avoid it? When your token needs nothing beyond a plain balance and you want the widest possible support today: the classic SPL Token program is universally integrated, and reaching for Token-2022 without needing an extension only narrows where your token works.
  • What breaks if I remove it? You lose protocol-level, inspectable token behavior. Fees, confidential amounts, on-transfer logic, and interest would each have to be re-implemented as custom wrapper contracts — fragmenting the token standard, hiding behavior in code again, and forcing every integrator to read that code rather than one account.
  1. Token-2022 is described as a successor, not a replacement. What is backward-compatible about it, what is not interchangeable, and why can’t you upgrade a classic mint to Token-2022 in place?
  2. Explain the extension model in terms of the account model: where do extensions live, when are they chosen, and why does that let anyone learn a token’s behavior without executing it?
  3. Pick two extensions from {transfer fees, transfer hooks, confidential transfers, interest-bearing, metadata} and describe concretely what each stores and where (mint vs. token account).
  4. A transfer hook runs custom code on every transfer, yet the page says it preserves Sealevel-style predictability. How, given the runtime must still know the transaction’s footprint up front?
  5. State one concrete way a naive DEX or wallet can break when it meets a Token-2022 token, and give the one-line rule that prevents it.
Show answers
  1. Backward-compatible in data layout: a Token-2022 mint or token account begins with the same base fields as a classic one, so code reading those base fields still works. What is not interchangeable is the owning program — Token-2022 lives at a different address, and a mint/token account is always owned by whichever token program its mint chose. Because an account’s owner is fixed at creation and only the owner may write it, there is no in-place upgrade; a classic mint stays classic, and new tokens opt into Token-2022 at creation.
  2. Extensions are stored as extra (TLV) data appended after the base struct — on the mint for token-wide behavior (fees, hooks, interest, metadata) and on the token account for per-holder state (confidential balance, withheld fees). They are chosen when the account is created and are largely fixed thereafter. Because the behavior is inspectable account data rather than hidden code, anyone can read the mint once and know the token’s full capability set before transacting — the account-model promise “capabilities are visible on the account” extended from who owns it to what it does.
  3. Examples: Transfer fees — a TransferFeeConfig on the mint stores a fee in basis points and a per-transfer cap; fees are withheld on the recipient’s token account and later harvested by the withdraw-withheld authority. Metadata — a TokenMetadata extension on the mint stores name, symbol, and URI directly, replacing a separate Metaplex account. (Also valid: transfer hook stores a hook program id on the mint; confidential transfer stores an encrypted balance on the token account; interest-bearing stores a rate + timestamp on the mint.)
  4. The token program invokes the hook via CPI, and the hook can only touch accounts passed to the instruction. Those extra accounts are resolved from an “extra account metas” PDA the mint publishes, so the client can assemble — and the transaction can declare — every account the hook will touch before execution. The footprint stays known up front, so the conflict rule still applies and scheduling is unaffected; the hook is powerful within the declared-footprint model, not an exception to it.
  5. Concrete break: a DEX that assumes “amount sent equals amount received” mishandles a transfer-fee token (1,000 in, 990 out) and leaves a pool short; or one that calls transfer without the hook’s required extra accounts gets a failed instruction; or code hardcoding the classic program id rejects the token outright. The rule: check the token program and read the mint’s extensions before integrating a mint — don’t assume classic ownership, lossless transfers, or that transfer needs no extra accounts.