Protocol
bordrless-programsThe 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_transferafter_transferbefore_mintafter_mintbefore_burnafter_burn | The token program | hook_signer, mint, source, destination, authority |
| Pool hook | before_initializeafter_initializebefore_add_liquidityafter_add_liquiditybefore_remove_liquidityafter_remove_liquiditybefore_swapafter_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.
Mint.hook_flags · u16, 8 bits used
BEFORE_TRANSFER1calls before_transferAFTER_TRANSFER2calls after_transferBEFORE_MINT4calls before_mintAFTER_MINT8calls after_mintBEFORE_BURN16calls before_burnAFTER_BURN32calls after_burnTRANSFER_RETURNS_DELTA64before_transfer may answer up to three cutsWRITES_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.
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],
}
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).
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]>,
}
| 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), andmin_amount_outis 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
- The
before_swapanswer, from the input: each cut a transfer from the trader’s input holding, then the burn. - The rest reaches the quote vault. The LP and protocol fees are charged on it; the curve runs on what remains.
- The
after_swapanswer, from the curve’s output: each cut from the output vault (the pool signs), then the burn. - The rest goes to the recipient’s holding.
A sell base in, quote out
- The
before_swapanswer, from the input. On a launch pool: the burn. - The rest reaches the base vault. The LP fee is charged on it; the curve gives the output in the quote token.
- The protocol fee is taken from that output, kept apart from the reserves.
- The
after_swapanswer, from what is left. On a launch pool: the creator and holder fees. - 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. | Bordrless’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.