Skip to content

Swap hooks and dynamic fees

Use a clock-based fee schedule without replacing core pricing or settlement.

Prerequisites: the callback contract, understand BeforeSwap/AfterSwap, and use the policy setup order. The example is examples/hooks/dynamic-fee; its pure calculation is examples/hooks/shared/src/fee.rs.

Example hook: dynamic fee. The core provides the bounded fee-reply capability; this particular time-decay schedule is optional example code. Pools do not adopt it automatically.

1. Configure the fee schedule

Consider a schedule that decreases from 1,000 ppm to 100 ppm:

HookPolicy::DynamicFee(FeeSchedule {
    peak_ppm: 1_000,
    floor_ppm: 100,
    starts_at: 100,
    ends_at: 200,
})

Pass this policy value to the configuration instruction. The timestamps are illustrative Unix seconds. Production schedules would need explicitly chosen timestamps and clock-aware quotes; changing a browser clock cannot change execution time.

Use the DynamicFee profile with exactly HookPhase::BeforeSwap.bit() for this example. The authenticated pool cap must allow the peak. Empty/reversed intervals, floor above peak, or peak above cap reject configuration.

2. Calculate the rate with integer rounding

For time clamped into the configured interval:

remaining = ends_at - clamped_time
duration = ends_at - starts_at
fee_ppm = floor_ppm + ceil((peak_ppm - floor_ppm) × remaining / duration)
Runtime timeRate for the example
Before or at 1001000 ppm = 0.10%
150550 ppm = 0.055%
At or after 200100 ppm = 0.01%

The shared runtime passes call.clock.unix_timestamp, captured and authenticated once by the core, to the policy evaluator. It no longer reads a second Clock sysvar. Callers cannot replace that timestamp with a browser timestamp or payload value. The client must pass an empty hook payload; it cannot override the schedule by submitting a fee value. Ending the decay leaves the floor fee active. It does not close trading.

3. Apply the reply in the core

BeforeSwap returns HookReply with Some(rate). The core validates the phase, profile, responder, and cap, then uses the rate for traversal. This overrides the rate for that swap; it does not rewrite the pool's static fee configuration or fee-share beneficiaries.

The core computes input fees and performs transfers. AfterSwap sees actual consumed input/output and the resulting price, and must return no fee even if the pool uses DynamicFee. An after hook cannot retroactively replace the fee or return custom deltas.

This differs from the signed deltas and return types discussed in Uniswap's swap-hook guide. Use grapl's typed swap context when interpreting requested input, consumed input, actual output, and unspent input.

Follow one amount through the schedule

For 10,000 input atoms, the example charges 10 atoms at time 100, 6 at time 150, and 1 at time 200. The midpoint rounds the token fee up from 5.5 to 6. Keep rate rounding and token-amount rounding distinct.

Next: preserve quote/execution agreement

A compatible quote adapter needs the same schedule, configuration, timestamp semantics, cap, rounding, and execution eligibility. Quoting the static fee or using process wall-clock time would not reproduce this policy. Continue with Metis compatibility.

For a 99%-to-floor preset of this same linear curve, continue with Decaying launch fee. That demonstration accepts starts_at plus duration_seconds, has its own program/configuration format, and defaults its convenience constructor to 990,000 ppm. It preserves the same upward rate rounding and endpoint clamps. The initial rate still requires a matching pool cap; the temporal rule remains external to the core.