CAsoon

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#

Callbacks, who calls them and the accounts that come first
HookCallbacksCalled byFirst accounts
Token hookbefore_transferafter_transferbefore_mintafter_mintbefore_burnafter_burnThe token programhook_signer, mint, source, destination, authority
Pool hookbefore_initializeafter_initializebefore_add_liquidityafter_add_liquiditybefore_remove_liquidityafter_remove_liquiditybefore_swapafter_swapThe DEXhook_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.

Mint.hook_flags · u16, 8 bits used

  • BEFORE_TRANSFER1calls before_transfer
  • AFTER_TRANSFER2calls after_transfer
  • BEFORE_MINT4calls before_mint
  • AFTER_MINT8calls after_mint
  • BEFORE_BURN16calls before_burn
  • AFTER_BURN32calls after_burn
  • TRANSFER_RETURNS_DELTA64before_transfer may answer up to three cuts
  • WRITES_HOOK_DATA128before callbacks may answer hook data; write_hook_data is allowed

Arguments#

A callback is told everything the caller knows about the operation. Balances and hook data are those before it in the before phase, after it in the after phase.

rust
pub struct TokenHookArgs {
    pub op: TokenOp,                     // Transfer, Mint or Burn
    pub phase: Phase,                    // Before or After
    pub mint: Pubkey,
    pub source: Pubkey,                  // the mint, for a mint
    pub destination: Pubkey,             // the mint, for a burn
    pub source_owner: Pubkey,
    pub destination_owner: Pubkey,
    pub authority: Pubkey,               // who signed
    pub authority_is_delegate: bool,
    pub amount: u64,
    pub delta: u64,                      // what the cuts took (known after)
    pub source_balance: u64,
    pub destination_balance: u64,
    pub decimals: u8,
    pub supply: u64,
    pub source_hook_data: [u8; 64],      // the holdings' 64 bytes
    pub destination_hook_data: [u8; 64],
}
mizo-hook/src/lib.rs

Answers#

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).

rustmizo-hook/src/lib.rs · the answer
pub const MAX_DELTAS: usize = 3;
pub const HOOK_DATA_LEN: usize = 64;

pub struct Delta {
    pub amount: u64,                     // above zero
    pub account: u8,                     // index in the callback's accounts; must be an extra
}

pub struct HookReturn {
    pub deltas: Vec<Delta>,                              // at most MAX_DELTAS
    pub burn: u64,                                       // pool swap callbacks only
    pub lp_fee_bps: Option<u16>,                         // before_swap only
    pub source_hook_data: Option<[u8; HOOK_DATA_LEN]>,   // token callbacks only
    pub destination_hook_data: Option<[u8; HOOK_DATA_LEN]>,
}
Which callback may answer which field, and the flag it needs
FieldCallbackNeeds
deltas (up to 3)token before_transferTRANSFER_RETURNS_DELTA
deltas, burnpool before_swapBEFORE_SWAP_RETURNS_DELTA
deltas, burnpool after_swapAFTER_SWAP_RETURNS_DELTA
lp_fee_bpspool before_swapBEFORE_SWAP_OVERRIDES_FEE
source_hook_datatoken before_transfer, before_burnWRITES_HOOK_DATA
destination_hook_datatoken before_transfer, before_mintWRITES_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 registryAddress(hook_program, mint-or-pool): 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. 1The before_swap answer, from the input: each cut a transfer from the trader’s input holding, then the burn.
  2. 2The rest reaches the quote vault. The LP and protocol fees are charged on it; the curve runs on what remains.
  3. 3The after_swap answer, from the curve’s output: each cut from the output vault (the pool signs), then the burn.
  4. 4The rest goes to the recipient’s holding.

A sell base in, quote out

  1. 1The before_swap answer, from the input. On a launch pool: the burn.
  2. 2The rest reaches the base vault. The LP fee is charged on it; the curve gives the output in the quote token.
  3. 3The protocol fee is taken from that output, kept apart from the reserves.
  4. 4The after_swap answer, from what is left. On a launch pool: the creator and holder fees.
  5. 5The 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.

What the protocol takes, by pool
PoolProtocol feeLP fee
Ordinary pool1% of the quote, flatStays with the pool’s liquidity
Launch pool25% 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.The protocol’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.