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 time | Rate for the example |
|---|---|
| Before or at 100 | 1000 ppm = 0.10% |
| 150 | 550 ppm = 0.055% |
| At or after 200 | 100 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.