Skip to content

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

ConstantCurrent value
HOOK_ABI_VERSION4
Call / reply tagsGRAPLHK4 / GRAPLOK4
POOL_VERSION5
Application payloadAt most 512 bytes
Extra accountsConfigured bound, at most 16
Writable extrasConfigured bound, at most 8 per callback
External program IDsAt most 4
Canonical policy PDA indices0..=3
Callback maskTen 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:

FieldRust type
versionu8
phaseHookPhase, variants 0–9 in lifecycle order
pool, mint_a, mint_b, actorPubkey, 32 bytes each
operation_sequenceu64
clockHookClock { slot: u64, unix_timestamp: i64 }
beforeHookPoolSnapshot
afterOption<HookPoolSnapshot>
detailsHookOperationDetails
account_accessVec<HookAccountAccess>
payloadVec<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

VariantContents
InitializeNo 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 fieldMeaning
consumed_input: u64Actual input debit, including the AMM fee
net_input: u64Consumed input less that fee
actual_output: u64Net output transferred to the actor's output account
output_fee_amount: u64Separate deduction in output-token base units; gross output equals this plus actual output
unspent_input: u64Requested input that traversal did not consume
fee_amount: u64AMM fee in input-token base units
fee_ppm: u32Applied 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.