All articles
Solana·June 27, 2026·7 min read

Token-2022 Transfer Hooks: The Trap That Breaks Sniper Bots

A Token-2022 transfer hooks bot can be silently taxed or honeypotted by malicious extensions. Here's how the traps work and how to detect them before you buy.

A Token-2022 mint can charge your bot a 10% fee on every buy and sell, silently, with the token creator collecting the difference — and your sniper won't notice until it tries to exit and the numbers don't add up. This is not a bug in your code. It's a feature of the SPL Token-2022 program, and the people minting honeypots on Solana know exactly how to weaponize it.

If your bot only understands the legacy SPL Token program, you're flying blind on a growing share of new launches. Token-2022 (program ID TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) is a separate program with a plugin system called extensions. Some of those extensions are harmless. A few of them are precision tools for extracting money from automated buyers.

The three extensions that actually hurt bots

Most of the 15-plus extensions are irrelevant to trading (metadata pointers, interest-bearing config, memo requirements). Three matter, and they matter a lot.

Transfer Fee. The mint can attach a fee, expressed in basis points, that gets skimmed off every transfer. The creator sets a maximumFee cap and a transferFeeBasisPoints value up to 10000 (100%). When you buy, the fee is withheld inside the recipient token account; when you sell, it's skimmed again. A 500 bps fee means you eat 5% on entry and 5% on exit before slippage or the actual pool spread. The nasty part: the fee is stored in the mint's TransferFeeConfig and can have a pending "newer" value that activates at a future epoch. A mint can look clean at buy time and flip to 100% fee two epochs later, right as liquidity peaks.

Transfer Hook. This is the real trap. The mint points at an external program via the TransferHook extension, and that program's Execute instruction runs on every transfer. The token program does a CPI into arbitrary code that the mint authority wrote. That code can return Err(...) under conditions the author chooses — for example, "allow transfers from these three whitelisted accounts, revert for everyone else." You buy fine because the deployer's routing account is whitelisted. You try to sell and the hook program reverts your transfer. Your tokens are real, your balance is real, and they are completely frozen for you specifically. Classic honeypot, dressed up as a compliance feature.

Non-Transferable / Default Account State. NonTransferable is the blunt version — the token literally cannot move after minting, so it can't honeypot a DEX pool (no pool forms), but it will trap a naive airdrop-farming bot. More dangerous is DefaultAccountState set to Frozen: every new token account starts frozen, and only the freeze authority can thaw it. Your account gets created frozen, the deployer thaws their own, and you're holding an unsellable position.

Why the standard checks miss this

The usual honeypot heuristics — simulate a buy, simulate a sell, compare — mostly work, but they have blind spots on Token-2022:

  • A time-delayed fee simulates clean now and taxes you later. Simulation at block N tells you nothing about the TransferFeeConfig scheduled for epoch N+2.
  • A stateful hook can whitelist your simulation account (if you always simulate from the same signer, sophisticated deployers detect and allow it) then revert real transfers from fresh wallets.
  • Compute budget blowups: a hook that runs an expensive loop can push your sell transaction over the CU limit so it never lands, functioning as a soft honeypot without ever returning an error.

Simulation is necessary but not sufficient. You need to read the mint's extension data directly and reason about it. This is the same discipline that separates bots that survive from bots that get farmed, and it pairs closely with the transaction-landing work covered in our writeup on stake-weighted QoS and transaction priority — landing a sell fast is useless if the token was never sellable.

Detecting it pre-buy: read the mint account

Before you send a lamport, fetch the mint account and check its owner program and its TLV-encoded extensions. Here's the shape of the check in TypeScript using @solana/spl-token:

import {
  getMint,
  getTransferFeeConfig,
  getTransferHook,
  getDefaultAccountState,
  ExtensionType,
  getExtensionTypes,
  TOKEN_2022_PROGRAM_ID,
} from "@solana/spl-token";
import { AccountState } from "@solana/spl-token";

async function screenMint(conn, mintPubkey) {
  const info = await conn.getAccountInfo(mintPubkey);
  if (!info) throw new Error("mint not found");

  // Legacy SPL mints can't carry these extensions — safe on this axis.
  if (!info.owner.equals(TOKEN_2022_PROGRAM_ID)) return { safe: true };

  const mint = await getMint(conn, mintPubkey, "confirmed", TOKEN_2022_PROGRAM_ID);
  const exts = getExtensionTypes(mint.tlvData);
  const flags = [];

  if (exts.includes(ExtensionType.TransferHook)) {
    const hook = getTransferHook(mint);
    if (hook?.programId && !hook.programId.equals(PublicKey.default)) {
      flags.push({ kind: "transfer_hook", program: hook.programId.toBase58() });
    }
  }

  const feeCfg = getTransferFeeConfig(mint);
  if (feeCfg) {
    const bps = feeCfg.newerTransferFee.transferFeeBasisPoints;
    flags.push({ kind: "transfer_fee", bps, maxFee: feeCfg.newerTransferFee.maximumFee.toString() });
  }

  if (exts.includes(ExtensionType.NonTransferable)) flags.push({ kind: "non_transferable" });

  const das = getDefaultAccountState(mint);
  if (das?.state === AccountState.Frozen) flags.push({ kind: "default_frozen" });

  return { safe: flags.length === 0, flags };
}

The rules I'd actually ship:

  • Any transfer hook → skip by default. Unless the hook program is on a known-good allowlist (e.g. a legitimate royalty enforcer whose code you've read), treat a non-default hook program as hostile. You cannot statically prove arbitrary hook code is safe.
  • Transfer fee → cap it. Reject anything above your threshold, say 300 bps, and always read newerTransferFee (the pending value), not olderTransferFee. Compare newerTransferFee.epoch against the current epoch so you know whether a fee hike is armed and waiting.
  • NonTransferable or default-frozen → hard skip. No exceptions.

For an MEV or arbitrage path, the fee math also has to flow into your profit calc, not just a boolean gate — a 2% transfer fee turns a lot of "profitable" atomic arbs into losers once it hits both legs. If you're building that kind of engine, the accounting belongs in the same layer as your slippage model, which is part of why we treat Token-2022 screening as core infrastructure in our MEV and arbitrage bot work.

The hook whitelist attack, concretely

Here's the pattern I've seen farm the most bots. Deployer ships a token with a transfer hook whose Execute does roughly this:

// pseudo-Rust hook logic
if source_account == deployer_routing_account
   || destination_account == deployer_routing_account {
    Ok(())          // deployer and the pool-seeding tx pass
} else {
    Err(HookError::TransferBlocked.into())  // everyone else reverts
}

Buys route through an aggregator that touches the whitelisted account, so your simulation and your first buy succeed. The moment you try to sell from your own wallet, the CPI into the hook reverts and the whole transfer fails. On-chain it looks like a random failed transaction, so bots that don't parse the revert reason just retry into a wall. The only reliable defense is refusing to buy anything with a live, non-allowlisted hook in the first place — detection at buy time, not exit time.

This kind of pre-trade screening is exactly the sort of thing that separates a Solana sniper bot that compounds from one that donates. It's also why fast, reliable data access matters: your screen has to run on a fresh view of the mint account with low latency, which ties into the low-latency data infrastructure your bot reads from. If you're pulling shreds directly for speed, the same freshness concern shows up in our notes on Jito ShredStream feeds, and connection-level drops that can starve your screener are covered in the QUIC throttling breakdown.

Practical checklist

  • Detect the mint program first. Legacy SPL Token mints can't carry these extensions.
  • Parse the TLV extension list, don't rely on simulation alone.
  • Treat any non-default transfer hook as hostile until proven otherwise.
  • Read newerTransferFee, check its activation epoch, cap the bps.
  • Hard-reject NonTransferable and default-frozen mints.
  • Fold transfer fees into your profit math on every leg, both entry and exit.

None of this is exotic once you've been burned once. The mistake is assuming a token behaves like legacy SPL because that's what your parser was written for. Token-2022 is a different program with a plugin surface, and on Solana, plugin surfaces get abused within hours of a new pattern working.

If you want a sniper that reads the mint before it reads the pool, that's the kind of screening we build into every Solana sniper bot we ship.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Token-2022#honeypot detection#sniper bots#transfer hooks