Hook ABI
Typed operation context, bounded replies, and explicit account permissions.
Source: programs/grapl-amm/HOOK_FRAMEWORK.md, src/hooks/abi/, and the core dispatcher. For a runnable introduction, use Hooks. Callback timing explains the phase semantics; permissions explains account access.
Wire and state versions
| Constant | Current value |
|---|---|
HOOK_ABI_VERSION | 4 |
| Call / reply tags | GRAPLHK4 / GRAPLOK4 |
POOL_VERSION | 5 |
| Application payload | At most 512 bytes |
| Extra accounts | Configured bound, at most 16 |
| Writable extras | Configured bound, at most 8 per callback |
| External program IDs | At most 4 |
| Canonical policy PDA indices | 0..=3 |
| Callback mask | Ten lifecycle bits, 1023 |
Match the program, IDL, hook, and client versions. Old wire tags and pool versions reject; there is no parallel legacy decoder.
Invocation layout
After the eight-byte call tag, Anchor/Borsh serializes these fields in order:
| Field | Rust type |
|---|---|
version | u8 |
phase | HookPhase, variants 0–9 in lifecycle order |
pool, mint_a, mint_b, actor | Pubkey, 32 bytes each |
operation_sequence | u64 |
clock | HookClock { slot: u64, unix_timestamp: i64 } |
before | HookPoolSnapshot |
after | Option<HookPoolSnapshot> |
details | HookOperationDetails |
account_access | Vec<HookAccountAccess> |
payload | Vec<u8> |
Integers are little-endian; vectors carry their own length prefix. Enum and option discriminants are part of the encoding. The outer application-payload vector is separate from any proof or rule format inside it. Decoding validates the current tag/version, bounds, and exact serialization; use the exported types instead of assembling offsets manually.
HookPoolSnapshot contains pool_mode: u8 (0 for CLMM, 1 for curve), sqrt_price_x64: u128, current_tick: i32, liquidity: u128, and fee_growth_global_a_x64 / fee_growth_global_b_x64: u128. Liquidity is the active pool amount, not a position balance. Before callbacks have after: None; after callbacks include the resulting snapshot.
Typed operation details
| Variant | Contents |
|---|---|
Initialize | No additional operation fields |
ModifyLiquidity(HookLiquidityDetails) | Position key, signed i128 liquidity delta, unsigned u64 A/B principal amounts |
Donate(HookDonationDetails) | Unsigned u64 A/B contributions |
Swap(Box<HookSwapDetails>) | Direction, account/authority identities, user limits, and optional settled result |
HookSwapDetails contains exact_output: bool; direction: HookSwapDirection (AtoB or BtoA); input/output mints and token accounts; input_authority and output_owner; requested_input and minimum_output: u64; sqrt_price_limit_x64: u128; and settled: Option<HookSettledSwap>.
| Settled field | Meaning |
|---|---|
consumed_input: u64 | Actual input debit, including the AMM fee |
net_input: u64 | Consumed input less that fee |
actual_output: u64 | Net output transferred to the actor's output account |
output_fee_amount: u64 | Separate deduction in output-token base units; gross output equals this plus actual output |
unspent_input: u64 | Requested input that traversal did not consume |
fee_amount: u64 | AMM fee in input-token base units |
fee_ppm: u32 | Applied fee rate |
For ExactOut, requested_input is the maximum input and minimum_output is the exact net output target. For ExactIn, those fields retain their input-budget and minimum-output meanings.
BeforeSwap has no settled result. AfterSwap runs after classic SPL settlement and includes it. requested_input = consumed_input + unspent_input; consumed_input = net_input + fee_amount. Reward effects remain separate from these AMM amounts.
Account descriptors and permissions
Each hookable instruction includes hook_accounts: Vec<HookAccountAccess> in exact remaining-account order. HookAccountAccess has class: HookAccountClass and writable_phases: u16; its eight classes are PolicyPda { index }, HookState, ExternalState, ExternalProgram, CreatableHookState, ActorReadOnly, PoolMintReadOnly, and InstructionsReadOnly. The latter accepts only the canonical readonly Instructions sysvar and requires explicit external-read permission; it grants no CPI signing or execution authority. Reserve mint views are restricted to either authenticated pool mint, with no signing or writing privilege. See permissions for ownership and phase requirements.
HookCapabilities contains bits, profile, and permissions: HookAccountPermissions. Permissions declare hook writes, external reads/writes/program access by phase, extra/write count limits, and a bounded external-program ID list. Defaults allow four read-only hook-owned extras and no external access. Descriptors do not override that configuration; an empty descriptor vector means zero extras.
The first CPI account is the read-only core hook PDA signer. The second is the read-only pool. Extras never receive upstream signer privileges and cannot alias protected settlement/core accounts. The core validates classes, owners, declared programs, bounds and per-phase privileges; the hook additionally validates role-specific addresses, discriminators, and relationships. See authentication.
Replies and identity
HookReply { version, phase, allow, fee_ppm, output_fee } contains output_fee: Option<HookOutputFee> (introduced in ABI 3). A successful reply must come from the configured hook, match the enabled phase/version/profile, and have allow: true. Only DynamicFee BeforeSwap supplies Some(fee) within the immutable cap; every other successful reply requires None. output_fee is independent: BeforeSwap can request { fee_ppm, destination } under permissions.max_output_fee_ppm, which defaults to zero. Other phases must return None. Core floor-rounds the deduction, enforces minimum output on the net amount, and transfers the fee itself to a declared writable ExternalState token account matching the output mint. The destination cannot alias a protected account. General custom accounting and return deltas remain unsupported.
The core reads Clock once per hookable operation. Both phases share that Clock and operation_sequence, which increments once for initialize, liquidity modification, swap, or donate, including no-hook operations. A failed operation rolls the increment back. (core program, pool, sequence) identifies the operation context; it is not randomness, a transaction signature, or an address that clients should predict for state preparation.
Actor is the authenticated local token authority and may be a PDA. Caller payload bytes and a quoted actor are not signature evidence. All ten phases belong to this framework; opening/closing positions and collecting fees have no dedicated callbacks. Graduation uses the add-liquidity callback pair; it has no dedicated callback.
Optional examples
Fee schedules, windows, allowlists, and rewards belong under examples/hooks. They do not become native pool rules or dependencies of the core program. A single attached hook can compose its own rules. The lifecycle probe exercises callbacks without an economic policy; reward credits demonstrate stateful effects under explicit permissions.
Use policy mechanics for the earlier pure examples and Metis compatibility for quote/execution obligations. Routing availability depends on a compatible evaluator and the routing provider’s integration.