Protocol
The hook protocol
A hook is an ordinary program whose instructions are named after the callbacks. The token program calls token hooks, the DEX calls pool hooks; both sign every call and apply what the hook answers.
How a hook is called
By CPI, with an Anchor-style discriminator and Borsh arguments, signed by the caller’s ["hook-authority", hook_program] PDA, always the first account. There is one signer per hook program, so a hook accepts only its own: a signer another hook passes on vouches for nothing. A hook never moves funds itself and never gets the user’s signature; it answers, and the caller checks and applies the answer.
Callbacks
| Hook | Callbacks | Called by | First accounts |
|---|---|---|---|
| Token hook | before_transfer after_transfer before_mint after_mint before_burn after_burn | The token program | hook_signer, mint, source, destination, authority |
| Pool hook | before_initialize after_initialize before_add_liquidity after_add_liquidity before_remove_liquidity after_remove_liquidity before_swap after_swap | The DEX | hook_signer, pool, base_mint, quote_mint, actor |
For a mint or a burn, the mint stands in for the side the operation lacks. Pool callbacks also carry the caller’s opaque hook data (Uniswap v4’s hookData), up to 256 bytes.
Flags
Mint.hook_flags and Pool.hook_flags subscribe a hook. A callback whose flag is off is never called, and an answer is read only when a flag allows one.
| Token hook flag (u16, 8 bits used) | Calls |
|---|---|
BEFORE_TRANSFER (1) | before_transfer |
AFTER_TRANSFER (2) | after_transfer |
BEFORE_MINT (4) | before_mint |
AFTER_MINT (8) | after_mint |
BEFORE_BURN (16) | before_burn |
AFTER_BURN (32) | after_burn |
Arguments
A before_* callback, and a pool’s after_swap, may answer in its return data. A field it may not fill must be empty, zero or None, or the whole answer is refused (UnsupportedHookReturn).
pub const MAX_DELTAS: usize = 3; pub const HOOK_DATA_LEN: usize = 64;
| Field | Callback | Needs |
|---|---|---|
deltas (up to 3) | token before_transfer | TRANSFER_RETURNS_DELTA |
deltas, burn | pool before_swap | BEFORE_SWAP_RETURNS_DELTA |
deltas, burn | pool after_swap | AFTER_SWAP_RETURNS_DELTA |
lp_fee_bps | pool before_swap | BEFORE_SWAP_OVERRIDES_FEE |
source_hook_data | token before_transfer, before_burn | WRITES_HOOK_DATA |
destination_hook_data | token before_transfer, before_mint | WRITES_HOOK_DATA |
The caller reads an answer only when the flags allow it and the return data is the hook’s own, then checks:
at most three cuts, each above zero, no account named twice, the sums in checked arithmetic (DeltaTooLarge); a transfer: the cuts add up to at most the amount, each to a writable, unfrozen holding of the mint that is neither the source nor the destination - the destination gains the amount less the cuts; a swap: the cuts and the burn stay below the side’s amount, never to a vault or the trader’s own holdings - a burn needs the mint writable (MintNotWritable), and min_amount_out is checked against what the recipient actually gained; a cut credits a holding without calling the hook for it, so a hook that keeps per-holder state names only holdings it leaves out of that state.
Extra accounts
A hook that needs accounts of its own publishes them at PDA(["bordrless-hook-accounts", mint-or-pool], hook_program): literal keys, or PDAs whose seeds are literals or accounts already in the callback’s list. Clients resolve the list and append the accounts after the fixed prefix; the calling programs pass them through untouched.
A swap, in order
A pool hook’s answer is taken at two points, drawn on ink: from the input before the curve, and from the output after it. The LP fee stays on the input side; the protocol fee is always taken in the quote token.
A buy (quote in, base out)
1. The before_swap answer, from the input: each cut a transfer from the trader’s input holding, then the burn. 2. The rest reaches the quote vault; the LP and protocol fees are charged on it; the curve runs on what remains. 3. The after_swap answer, from the curve’s output: each cut from the output vault (the pool signs), then the burn. 4. The rest goes to the recipient’s holding.
A sell (base in, quote out)
1. The before_swap answer, from the input. On a launch pool: the burn. 2. The rest reaches the base vault; the LP fee is charged on it; the curve gives the output in the quote token. 3. The protocol fee is taken from that output, kept apart from the reserves. 4. The after_swap answer, from what is left. On a launch pool: the creator and holder fees. 5. The rest goes to the recipient’s holding.
Who is paid
Each pool copies its fee model (fee_model) from the DEX config when it is created and keeps it for life. A launch pool is a curve the launchpad creates as its own hook; a curve any other program opens pays the flat rate.
| Pool | Protocol fee | LP fee |
|---|---|---|
| Ordinary pool | 1% of the quote, flat | Stays with the pool’s liquidity |
| Launch pool | 25% of what the pool’s hooks cut (creator fee, holder rewards, a creator’s own hook), in SOL. A base-side cut is valued at the swap’s price; burns are not shared. | Mintvelocity’s, all of it, in SOL |
A launch whose rules collect nothing pays the LP fee alone. Anyone may send collect_protocol_fees_sol: it unwraps a launch pool’s fees through the bridge and pays them to the fee collector as SOL.