Callback timing and context
Use typed before/after state, authenticated identities, and actual settled amounts.
Use this catalog to choose where a rule belongs. Prerequisites: hook model and position lifecycle. Types and serialization are listed in the ABI reference.
Follow one authenticated operation
HookInvocation identifies phase, pool, mints, and actor. It carries operation_sequence, a frozen HookClock, before and optional after pool snapshots, typed details, ordered account-access descriptors, and application payload. The payload is copied between the phase pair; it is not trusted identity.
The core reads Clock once. Both phases share that slot/timestamp and sequence. Sequence increments once per initialize, modify-liquidity, swap, or donate even when callbacks are disabled, and rolls back with a failed transaction. It is not random, a transaction signature, or a client reservation. Prepare state using the hook's stable identities rather than predicting the next pool sequence.
Each snapshot contains price, signed current tick, active pool liquidity, and global fee growth for both assets. A before callback has after: None; an after callback preserves before and adds the resulting state. The serialized pool is read-only inside CPI and has entered = true. Position/tick/vault/actor settlement accounts remain protected; a key in the context does not grant access to its account bytes.
Initialization — phases 0 and 1
| Phase | Context | Response |
|---|---|---|
BeforeInitialize (0) | Validated provisional pool; creator actor; configured price and zero liquidity | Allow without a fee, or reject |
AfterInitialize (1) | Same candidate after initialization work, with an after snapshot | Allow without a fee, or reject |
Details are HookOperationDetails::Initialize. Anchor creates accounts before the instruction body, and the candidate is assigned before the callbacks. An after snapshot is still provisional until transaction success. Rejection rolls back pool, tick book, and vault creation. A policy configured only after the pool exists cannot already be required by its initialization callback; see bootstrap.
Liquidity — phases 2 through 5
| Phase | Snapshot timing | HookLiquidityDetails |
|---|---|---|
BeforeAddLiquidity (2) | Before accounting/deposit | Position, positive signed delta, calculated A/B principal |
AfterAddLiquidity (3) | After accounting/deposit | Same position, delta and amounts |
BeforeRemoveLiquidity (4) | Before accounting/withdrawal | Position, negative signed delta, calculated A/B principal |
AfterRemoveLiquidity (5) | After accounting/withdrawal | Same position, delta and amounts |
The actor owns the position. Deposits round up; withdrawals round down. Amounts and user limits are checked before the callback, so an invalid request may fail without invoking the hook. The context's active pool liquidity is not position liquidity: an out-of-range position can change without changing active L.
The position key does not include its range ticks or the user's limit_a/b. All four phases require a fee-free acknowledgement. Liquidity and donations follows the accounting sequence.
Swaps — phases 6 and 7
HookSwapDetails explicitly identifies A-to-B or B-to-A, input/output mints and token accounts, input authority, output owner, requested input, minimum output, and price limit. Do not infer direction by guessing which amount is positive.
| Phase | Timing | settled |
|---|---|---|
BeforeSwap (6) | Before traversal and transfers | None |
AfterSwap (7) | After traversal and exact classic SPL settlement | Actual input, output, unspent input, fee amount/rate |
The settled result distinguishes consumed_input including fees from net_input excluding fees. actual_output is the amount transferred to the output account. requested_input = consumed_input + unspent_input; partial execution can leave part of the maximum input unused. All token amounts are unsigned base units, while liquidity deltas are signed.
An output-based reward uses settled.actual_output and output_mint in AfterSwap. Requested input is not an output receipt. A later callback, nested CPI, or transaction failure rolls back both settlement and hook effects. Actor is the authenticated local authority and may be a router PDA; output_owner is the local output-account owner, not proof of a route's final beneficiary.
Only DynamicFee BeforeSwap returns Some(fee_ppm). Every AfterSwap reply requires None, including a hook that writes reward state. See dynamic fees and reward credits.
Donations — phases 8 and 9
| Phase | Timing | HookDonationDetails |
|---|---|---|
BeforeDonate (8) | Before LP growth credit and token deposit | A/B contribution amounts |
AfterDonate (9) | After LP growth credit and token deposit | Same amounts |
Donation requires active liquidity for a nonzero credit, updates LP fee growth, and leaves price and active L unchanged. Before/after snapshots expose the growth difference. Neither phase may override the fee. The authenticated actor is the donor.
Operations without callbacks
Opening/closing positions and collecting LP, beneficiary, or protocol fees have no dedicated phases. Collection does not synthesize a zero-liquidity callback, and there are no dedicated graduation phases. graduate_curve uses BeforeAddLiquidity with curve mode and AfterAddLiquidity with CLMM mode. Zaps compose the existing swap and liquidity callbacks for their respective legs. External reward claims or consumption belong to their own hook program; they are not new core instructions.
Build against the callback contract
The dispatch code lives in programs/grapl-amm/src/hooks/; operation orchestration lives under instructions/{pool,positions,trading,fees}. Match the typed context to the operation and phase before evaluating a rule. Use hook validation to check sequence, Clock, partial fills, and settlement.