> For the complete documentation index, see [llms.txt](https://docs.aegis.markets/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aegis.markets/part-5-dfm-dynamic-fee-mechanism/how-dynamic-fees-work.md).

# 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:

1. Base fee should track realized volatility — high when the pool is volatile, low when calm.
2. 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:

&#x20; dfm.prepareSwap(poolId, swapParams):

&#x20;   quote totalFeePPM = baseFeePPM + surgeFeePPM

&#x20; hookRuntime store (tick, fee, sender)

Uniswap v4 executes swap at quoted fee

afterSwap:

&#x20; oracle.pushObservation(poolId, tickAfter, liquidity)

&#x20; capped = oracle check

&#x20; dfm.finalizeSwap(poolId, capped):

&#x20;   if capped: set inCap, capStart, surgeFeePPM₀

&#x20;   if base-fee step due: adjust baseFeePPM

&#x20; spot.handleFeeSplit(pendingFees):

&#x20;   split between LP / protocol / POL per policy

&#x20; spot.maybeReinvest(poolId):

&#x20;   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.
