Skip to content

Build your first hook

Start from the Rust BaseHook template, implement a callback, and verify authenticated CPI execution.

Use the custom hook template to build a program that participates in a grapl pool's lifecycle. The starter exposes all ten callbacks and authenticates calls before your logic runs. It does not impose an allowlist, trading window, fee policy, or rewards policy.

Prerequisites

You need a protocol checkout containing examples/hooks/template, the repository's pinned Rust toolchain, and the Agave SBF build tools. The program uses Rust 2021 and Anchor 1.1.2. You do not need a wallet, API key, or public RPC for the included tests.

BaseHook is currently a module of the grapl-amm crate. There is no separate published grapl-base-hook package to install.

Start with the template

Open these files in the protocol checkout:

FilePurpose
examples/hooks/template/src/lib.rsProgram identity and authenticated entrypoint
examples/hooks/template/src/callbacks.rsCapability contract and callback implementations
examples/hooks/template/tests/host_cases/Authentication and dispatch tests
examples/hooks/template/tests/svm_cases/Executable SBF lifecycle tests

Keep the entrypoint routed through process_hook:

use grapl_amm::hooks::base::process_hook;

// Inside process_instruction, with your program's ID and CustomHook in scope:
process_hook(program_id, &ID, accounts, encoded, &CustomHook)

This calls your implementation only after checking the invocation and pool binding. Rust uses a trait implementation here, rather than Solidity inheritance.

Implement the callbacks you need

The template's CustomHook implements BaseHook. Each callback receives a HookContext containing the authenticated invocation, decoded pool and declared extra accounts.

For example, inside your impl BaseHook for CustomHook, replace before_swap with your rule:

fn before_swap(&self, context: &HookContext<'_, '_>) -> Result<HookReply> {
    let swap = match &context.invocation.details {
        HookOperationDetails::Swap(swap) => swap,
        _ => return err!(CoreError::InvalidHookContext),
    };
    require!(swap.requested_input <= 1_000_000, CoreError::InvalidHookContext);
    Ok(context.acknowledge())
}

Import HookOperationDetails from grapl_amm::hooks::abi and CoreError from grapl_amm::errors. This illustrative rule limits requested input to one million base units, not one million human-readable tokens. Choose bounds and error types appropriate to your own policy, and test both sides of the limit.

Use AfterSwap and its settled amounts when calculating rewards. Requested input can differ from consumed input or the amount delivered to the recipient. See callback context.

Declare the capability contract

The starter's template_capabilities() enables all ten callbacks with the PolicyOnly profile and default account permissions. Each callback explicitly acknowledges the invocation. Disable the bits you do not use; a method inherited without an implementation rejects.

When creating a pool, set its hook_program account to your executable program and its configuration.hook_capabilities to the exact contract returned by your hook's capabilities(). Bitmap, profile and permissions must match. The BaseHook dispatcher rejects mismatches.

For dynamic trading fees, use the DynamicFee profile, enable BeforeSwap, and return fee_ppm within the pool's cap. Changing the reply alone does not enable that capability. See dynamic fees and attach a hook.

Add state deliberately

The default validate_accounts accepts only an empty payload and no extra accounts. Override it when your hook needs state or input. Validate each account's owner, seeds, discriminator, pool relationship and expected privileges; validate the payload's encoding and bounds.

Declare extras through HookAccountAccess and the pool permission contract. The core validates and narrows their privileges before CPI. Your validation binds those accounts to your own policy. Neither the actor's signing authority nor core vault authority is forwarded to your hook.

A single program can serve many pools with separate configuration PDAs. Callback handling does not create those PDAs for you. If you add setup instructions, authenticate those instructions separately and keep them distinct from the callback ABI. See authentication and permissions.

Run the validation gate

From the protocol repository root:

make test-hook-template

The gate checks formatting and Clippy, builds executable SBF programs, and runs:

  • Host tests for every callback, invalid account identities and privileges, context mismatches, missing implementations and invalid replies.
  • LiteSVM tests that invoke all ten template callbacks through the real core program.
  • A direct-call rejection test without a core PDA signature.
  • The lifecycle probe's deliberate before/after failures, including rollback after settlement.

Expect every suite to report zero failures. Host tests can construct signer flags; the SBF tests exercise actual runtime CPI signing. Both are needed.

Move into your own program

The template's program ID is a fixture, not a deployed custom hook. Replace it with your program identity before deployment. If you move the source outside this repository, replace the relative grapl-amm dependency with a reviewed, pinned source dependency and port the tests' shared fixture imports.

Adding a hook program does not automatically provide a quote adapter. SDK and Metis integrations must evaluate your policy with matching state, time and identity inputs. Start with BaseHook reference, then validate hook behavior and hook quote adapters.