How Dynamic Fees Work?
This page is the mechanism. If What is AEGIS DFM? is the executive summary, this is the engineering summary: how base fee is computed, how surge triggers and decays, and what the CAP event mechanism does.
The fee per swap, at a glance
When a swap arrives at an AEGIS-enabled pool, the hook quotes a total fee in PPM (parts-per-million):
totalFeePPM = baseFeePPM + surgeFeePPM
The fee is then applied by Uniswap v4 as it would apply any v4 dynamic-fee quote. LPs and POL receive their configured shares of the fee; the rest goes to the hook fee account.
All math is in PPM. Conversions: 1 PPM = 0.0001% = 0.000001. The familiar 30 bp fee tier is 3,000 PPM in DFM terms.
Base fee
The base fee tracks realized pool volatility via the max-ticks-per-block (MTB) metric:
baseFeePPM = maxTicksPerBlock × baseFeeFactorPpm
Default baseFeeFactorPpm is 28 — meaning 28 PPM per tick, or ~0.0028% per MTB tick. In a calm pool where MTB drifts to a small value like 3, base fee is 3 × 28 = 84 PPM ≈ 0.0084%. In a volatile pool where MTB rises to 100, base fee is 100 × 28 = 2,800 PPM ≈ 0.28%.
Where MTB comes from
TruncGeoOracleMulti self-tunes maxTicksPerBlock to hit a configured targetCapsPerDay. The mechanism:
Every block, the oracle measures the actual tick move of the pool.
When the move exceeds the current MTB cap, the oracle records a CAP event — a moment where volatility breached the expected envelope — and emits MaxTicksPerBlockUpdated after the auto-tune step.
The auto-tune loop adjusts MTB up (if CAP events are too frequent) or down (if they're too rare), asymptoting toward the target frequency.
Bounds are enforced: minBaseFeePpm ≤ baseFeePPM ≤ maxBaseFeePpm.
Default bounds are 10 PPM to 100,000 PPM (0.001% to 10%), tunable per-pool via PoolPolicyManager.
Step cadence
The oracle does not continuously recompute base fee. It steps on a cadence configured by baseFeeUpdateIntervalSeconds (default: every few minutes, varies by pool). At each step, baseFeePPM moves at most baseFeeStepPpm toward the target. This ensures the fee doesn't whipsaw on short-lived outliers.
Surge fee
The surge fee is designed to catch sudden price moves — moves big enough to likely reflect information the pool hasn't priced yet.
Initial surge
When a CAP event fires (tick move exceeds current MTB), the oracle notifies DynamicFeeManager, which sets:
surgeFeePPM₀ = baseFeePPM × surgeFeeMultiplierPpm / 1_000_000
With default surgeFeeMultiplierPpm = 3_000_000 (300%), the initial surge is 3 × baseFeePPM. The multiplier is capped at 300% by policy — a hard cap that can't be raised beyond sensibility.
Linear decay
Once set, the surge fee decays linearly to zero over surgeDecayPeriodSeconds (default 6 hours = 21,600 seconds):
t = current time − capStart
if t ≥ surgeDecayPeriodSeconds: surgeFeePPM = 0
else: surgeFeePPM = surgeFeePPM₀ × (1 − t / surgeDecayPeriodSeconds)
The decay starts only after the CAP event. If another CAP event fires during the decay, the surge fee re-arms to surgeFeePPM₀ at the new event time. This means during a sustained volatility episode, surge can stay elevated; during a single-shock event, it decays smoothly.
Why linear and not exponential
Linear decay is simpler to reason about, gas-cheap on-chain, and produces predictable behavior for integrators and routers. Exponential decay tends to have a long tail that adds fee to swaps long after the volatility is gone; linear cuts off cleanly.
CAP events and the policy budget
CAP events are not free. Each CAP event ticks against a per-pool budget tracked by DynamicFeeManager. The budget:
Has a daily cap (capFrequency).
Tracks inCap state (whether surge is currently active).
Blocks excessive CAP events if the budget is exhausted.
This prevents pathological pools (ones that would fire CAP continuously) from permanently elevating the surge fee. The budget refreshes on a policy-configured cadence.
The auto-tune loop
The MTB auto-tune loop balances two objectives:
Base fee should track realized volatility — high when the pool is volatile, low when calm.
CAP events should fire at roughly targetCapsPerDay — enough to capture genuine spikes, not so many that surge is constant.
Both are governed by targetCapsPerDay, a per-pool policy parameter. The oracle adjusts MTB in both directions over time to hit this target. On cold pools (few swaps per day), the loop converges slowly; on busy pools, it converges quickly.
What this looks like in practice
Cold pool, stable pair: MTB drifts low, base fee is near minBaseFeePpm, surge rarely fires. Swap fees are cheap.
Active pool, volatile pair: MTB sits high, base fee is elevated, occasional CAP events pump surge. Swap fees are meaningfully higher than a static low tier would charge.
Active pool, calm pair: MTB is moderate, base fee is moderate, surge rare. Behaves similarly to a well-chosen static tier.
Swap-time mechanics
Inside a single swap, the hook flow is:
beforeSwap:
dfm.prepareSwap(poolId, swapParams):
quote totalFeePPM = baseFeePPM + surgeFeePPM
hookRuntime store (tick, fee, sender)
Uniswap v4 executes swap at quoted fee
afterSwap:
oracle.pushObservation(poolId, tickAfter, liquidity)
capped = oracle check
dfm.finalizeSwap(poolId, capped):
if capped: set inCap, capStart, surgeFeePPM₀
if base-fee step due: adjust baseFeePPM
spot.handleFeeSplit(pendingFees):
split between LP / protocol / POL per policy
spot.maybeReinvest(poolId):
if thresholds met, reinvest POL fees
A sophisticated trader could, in principle, observe that surge is active and choose not to trade. This is expected and fine — it is exactly what the surge fee is supposed to do: make trading less attractive when the market is in the middle of a price discovery event, so LPs aren't run over by adverse selection.
Parameter catalog
Key parameters exposed via PoolPolicyManager (one per pool, all governable):
Parameter
Default
Description
minBaseFeePpm
10
Lower bound on base fee.
maxBaseFeePpm
100,000
Upper bound on base fee.
baseFeeFactorPpm
28
PPM of base fee per MTB tick.
baseFeeStepPpm
TODO
Max change to base fee per step.
baseFeeUpdateIntervalSeconds
TODO
Seconds between base fee steps.
surgeFeeMultiplierPpm
3,000,000
Surge fee as multiple of base. Capped at 300%.
surgeDecayPeriodSeconds
21,600
Surge decay duration (6 hours).
targetCapsPerDay
TODO per pool
MTB auto-tune target frequency.
capFrequency
TODO
Daily CAP event budget.
polSharePpm
TODO per pool
Fraction of fees that go to POL.
The full parameter inventory lives in PoolPolicyManager and is enumerated in the spec. Refer to the DFM repo's docs/one_pagers/PoolPolicyManager.md for authoritative defaults and bounds.
The integrator view
For aggregators and routers integrating with AEGIS-enabled pools:
Quote at swap time, not ahead. The fee can change between blocks if a CAP event fires or base fee steps. Don't stale-cache quotes.
Use DynamicFeeManager.getLatestFeeQuote(poolId) for a consistent simulation view.
Surge is information. If surge is active, you can choose to skip routing, accept the higher fee, or wait. AEGIS doesn't have an opinion — surge exists to let LPs capture adverse selection.
POL is an LP. If you're computing pool depth for a route, include the POL position. It's a full-range position that looks like any other full-range LP to v4.
Last updated