Skip to content

Gate swaps with a Merkle proof

Bind membership to the authenticated actor, exact pool domain, and expiry.

Prerequisites: callback authentication, policy setup, and the callback contract. Use examples/hooks/allowlist; pure hashing and verification live in examples/hooks/shared/src/{allowlist,hashing}.rs.

Example hook: allowlist. This is an optional program built for this repository, not native or default policy in the protocol/library. The core does not depend on Merkle membership or grapl-hook-policies. A pool or launch does not require an allowlist.

1. Choose the domain and root

The immutable AllowlistPolicy stores chain_domain, one pool mint, root, and root_version. Setup checks that the mint belongs to the pair. The example requires PolicyOnly and exactly BeforeSwap, so it gates both swap directions and does not lock LP exits.

Membership binds chain namespace, core protocol, pool, mint, root version, trader, and expiry. Changing any field changes the leaf. The runtime gets trader from authenticated HookInvocation.actor; it never accepts an identity override in the proof payload.

The chain namespace is a creator-selected 32-byte value. Choosing the cluster genesis hash is a convention; the program does not query or verify the actual cluster genesis hash. Document and consistently apply the selected domain.

The exact SHA-256 tags, field order, little-endian integers, and sorted-node rule are maintained in policy mechanics. Do not hash JSON, decimal strings, base58 text, or an extra array-length prefix in their place.

2. Construct the leaf and proof

Encode the configured domain, root version, authenticated trader, and expiry in the exact byte order specified by policy mechanics. Hash the domain-separated leaf, then combine it with each sibling using the sorted-node rule.

The proof contains sibling hashes, not a list of wallet addresses. Each sibling adds 32 bytes. Validate your client encoding against the canonical examples/hooks/shared/fixtures/merkle-v1.json vector before using it to authorize a swap.

3. Encode membership for a swap

The runtime helper membership_payload(expires_at, &proof) in examples/hooks/shared/src/runtime/evaluate.rs encodes only version, expiry, sibling count, and siblings. The payload contains no trader field. The maximum is 12 siblings and 394 bytes, within the core's 512-byte bound. An empty proof is valid for a single-leaf tree.

Pass the bytes as SwapArgs.hook_payload and the configured policy as the first read-only remaining account. Derive the root using the intended pool and authenticated actor domain.

At execution, the hook reconstructs the leaf with the authenticated actor and configured domain, folds the proof from leaf to root, checks the root, and rejects when now >= expires_at. Extra bytes, an unknown payload version, or an inconsistent sibling count reject.

Membership is reusable authorization

The proof can be reused by the same actor in the same domain before expiry. There is no consumed nonce, one-buy limit, quota, or per-wallet spend tracking. This example declares its policy read-only and does not request participant state. The generic framework can support stateful rules, but this example does not implement them. Replay tests reject changes of domain/actor; they do not turn valid membership into a single-use ticket.

There is no root-rotation or individual revocation instruction in the example. Expiration is the implemented time boundary. A copied proof also does not confer the actor's signing authority, and the policy applies only to this pool.

Validate identity and expiry

Reject an altered domain, another actor, a bad root, an expired grant, or an inconsistent proof length. A copied proof cannot supply the actor’s signing privilege. Continue with hook validation and router identity.