Skip to content

Liquidity, donations, and fee claims

Place policies around principal changes and donations without inventing collection callbacks.

Prerequisites: LP lifecycle, callback context, and the callback contract. Use the lifecycle probe to explore these phases; the fee, window, and allowlist examples enable only BeforeSwap.

Follow a liquidity change

programs/grapl-amm/src/instructions/positions/liquidity.rs follows this order:

  1. Calculate token obligations from the position range, current price, and signed liquidity delta.
  2. Check deposit maxima or withdrawal minima.
  3. Mark the pool entered and call the enabled before phase.
  4. Crystallize prior position fees, update position/ticks/active liquidity, and settle tokens.
  5. Serialize the pool for the enabled after phase, then clear the entered flag.

The operation's A/B amounts are computed before the hook and reused afterward. They are deposit or withdrawal principal, not a change to fee claims. Hooks do not replace the range calculation, rounding rule, or ownership check.

The liquidity instruction uses this argument shape:

let args = ModifyLiquidityArgs {
    liquidity_delta: delta,
    limit_a: if delta > 0 { u64::MAX } else { 0 },
    limit_b: if delta > 0 { u64::MAX } else { 0 },
    hook_payload: payload,
    hook_accounts: Vec::new(),
};

This excerpt shows the argument shape. Replace permissive example limits with meaningful maxima/minima derived from the user’s amounts and slippage.

Know which policies this context can express

A hook can inspect authenticated actor, position key, delta sign/magnitude, token amounts, active pool liquidity, and pool configuration. It cannot read the position's tick range from the key alone: the position and tick book are not forwarded.

A hook may maintain its own reward accounting or counters through explicitly permitted writable state. Its account declarations and lifecycle ordering must match the operation; a callback bit alone does not grant writes. Core position/tick/vault accounts remain protected, and custom settlement deltas are unsupported. Compare the account model before adapting Uniswap's liquidity-hook example.

A removal policy can reject an LP's exit. The core has no emergency escape instruction that bypasses it. A fixed hook binding also does not establish immutable code if the program has an upgrade authority.

Follow a donation

instructions/fees/donate.rs validates a nonzero A/B contribution, runs BeforeDonate, credits active LP fee growth, deposits the tokens, and runs AfterDonate. Donation does not change price, active L, or the beneficiary split. It credits LP fee growth directly and requires active liquidity for a nonzero credit.

The donation argument shape is:

let args = DonateArgs {
    amount_a: 10,
    amount_b: 10,
    hook_payload: payload,
    hook_accounts: Vec::new(),
};

The same HookDonationDetails amounts reach both phases. Before/after snapshots include global fee growth, so AfterDonate can compare the credit while price and active liquidity remain unchanged. A rejection restores the contribution, growth, and provisional hook effects.

Fee claims are a different operation

collect_position_fees crystallizes and pays fees without changing liquidity. collect_beneficiary_fees pays the configured recipient's debt. Neither dispatches hooks; neither should be described as a liquidity-change or donation callback. Opening and closing the position record also have no callbacks.

For reporting, a donation may increase LP entitlement, but it is not organic swap volume. A collection moves an existing claim and must not count as another earning event. See fee accounting and LP performance.

Preserve atomic settlement

A rejection in either callback must restore the position, ticks, pool, and affected token balances. Check after-phase failures as well as early policy rejection. Use hook validation when combining liquidity rules with external effects.