Choose phases, profiles, and account permissions
Request only the callbacks and state access your hook implements.
Prerequisite: identify your phase in the callback catalog. The public contract is programs/grapl-amm/HOOK_FRAMEWORK.md; implementation types live in src/hooks/abi/, with permission and account checks in src/hooks/{permissions,accounts}.rs.
Select callbacks and reply behavior
Each phase has a bit from HookPhase::bit(). The complete mask is 1023; BeforeSwap alone is 64, and both swap phases are 192. These are grapl values, not Uniswap address flags.
This configuration fragment enables policy-only swap callbacks with the default read-only account policy:
use grapl_amm::hooks::abi::{HookCapabilities, HookPhase, HookProfile};
let capabilities = HookCapabilities {
bits: HookPhase::BeforeSwap.bit() | HookPhase::AfterSwap.bit(),
profile: HookProfile::PolicyOnly,
..HookCapabilities::default()
};
assert!(capabilities.is_supported());
assert_eq!(capabilities.bits, 192);is_supported checks callback/profile compatibility; it is not the complete validation of account permissions. The fee, window, and allowlist examples require exactly BeforeSwap. A composed stateful example can select a broader set, but each selected phase needs a valid implementation.
| Profile | Requirement | Successful reply |
|---|---|---|
PolicyOnly | Any supported callback subset | allow: true, fee_ppm: None |
DynamicFee | Must include BeforeSwap | BeforeSwap returns Some(fee); other phases return None |
CustomAccounting | Unsupported | Rejected |
The core accepts a configured pool cap up to 990000 ppm (99%). That protocol ceiling does not raise an existing pool's immutable cap or default fee. A hook cannot exceed the cap chosen for its pool. An error, allow: false, missing return data, wrong producer/version/phase, or incompatible fee rejects the operation.
Declare the pool's access ceiling
HookCapabilities.permissions bounds what callers may request. Its default allows at most four read-only hook-owned extras, zero writable extras, and no external programs. A callback bit alone grants no write access.
| Permission field | Grants within enabled phases |
|---|---|
max_output_fee_ppm | Independent output-fee ceiling, default 0; at most 990000 and requires BeforeSwap |
hook_write_phases | Writes to hook-owned policy/state accounts |
external_read_phases | Read-only external state |
external_write_phases | Writable external state |
external_program_phases | Access to declared executable programs |
max_extra_accounts | Configured total, at most 16 |
max_writable_accounts | Configured writable total per callback, at most 8 |
external_program_count / external_programs | Declared prefix of at most four program IDs |
Masks must be subsets of enabled lifecycle phases. The program-ID prefix must be unique; unused entries must be zero. Core and attached-hook IDs cannot appear in that prefix. The System Program's zero ID is valid only as an explicitly included prefix entry. An external ID authorizes a program/owner class, not arbitrary role-specific addresses.
Request exact accounts for each operation
The hookable instruction's hook_accounts vector must describe every remaining account in order. Each descriptor selects a class and writable_phases. An empty vector supplies no accounts; it does not request a permissive default.
| Class | Core requirement | Additional hook responsibility |
|---|---|---|
PolicyPda { index } | Hook-owned, non-executable, canonical [b"policy", pool, index], index 0–3 | Decode policy and validate its pool/configuration |
HookState | Hook-owned, non-executable | Bind its PDA, discriminator, actor and other relationships |
ExternalState | Non-executable; owner is a declared external program | Bind the exact mint/account/authority and supported state |
ExternalProgram | Executable; key is a declared program | Use only the intended CPI interface |
CreatableHookState | Hook-owned state, or empty System-owned account with System explicitly authorized | Bind the canonical PDA before allocating or assigning; writes need hook-write permission |
PoolMintReadOnly | Exact pool reserve mint; zero write mask and no signer | Use only as a readonly mint view for token CPI |
ActorReadOnly | Exact authenticated actor; zero write mask; no CPI signing privilege | Use as an identity/ATA-owner view, never as a payer or signer |
InstructionsReadOnly | Canonical Instructions sysvar key/owner; readonly, nonsigner, non-executable; explicit external-read phase | Verify the signed top-level envelope instead of trusting CPI signer flags |
Writes need both pool permission and the descriptor's phase bit. An external state account also needs read permission in any enabled phase where it is passed read-only. External programs are always read-only. Transaction-level writability is an upper bound; the core rebuilds CPI metas separately for each phase and can downgrade an account to read-only.
All enabled phases of the operation are validated before the first callback. A missing AfterSwap permission therefore cannot let a BeforeSwap side effect execute first. Duplicate extras, wrong owners/programs, excess counts, and protected-account aliases reject. Signer extras reject except for ActorReadOnly: it may refer to the outer actor signer, but the callback receives it strictly read-only and without signing privilege.
Preserve the authority boundary
The core hook PDA remains a read-only signer, and the pool remains read-only. Actor signer privileges and vault authority are never forwarded. State permissions do not grant arbitrary settlement. PoolMintReadOnly explicitly permits a reserve mint view. A nonzero output-fee ceiling allows a bounded BeforeSwap request; core performs the token transfer and checks net slippage without forwarding vault authority.
A hook may sign for its own PDA in a nested CPI using its own program seeds. For example, a reward hook may control a separate Token-2022 mint. Its program/state accounts must be declared and validated; that authority cannot spend the AMM's vaults. See authentication and reward credits.
Apply the permissions
Check per-phase privilege downgrades and rollback for every stateful operation. Serialized permission masks alone do not establish runtime behavior. Continue with pool binding and hook validation.