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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aegis.markets/part-5-dfm-dynamic-fee-mechanism/how-dynamic-fees-work.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
