> ## Documentation Index
> Fetch the complete documentation index at: https://pulse-hook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Priority Fee Median

> Frugal2U median approach used in the Hook

The hook needs a "typical" priority fee to compare each new swap against. Storing every past fee and sorting them on-chain would be far too expensive, so instead we use a **streaming estimator** that keeps only three numbers in storage and updates in constant time.

## The idea

The estimator lives in `FrugalMedianLibrary` and follows the **Frugal-1U / Frugal-2U** algorithm (Ma, Muthukrishnan, Sandler, 2013), adapted to Solidity integer math. It tracks three state variables: `approxMedian`, `step`, and `positive`.

Every time a new priority fee comes in:

* If the new value is **bigger** than the current estimate, the estimate moves **up** by `step`.
* If it is **smaller**, the estimate moves **down** by `step`.
* `step` is not fixed. It **grows** while the moves keep going the same direction (roughly 1% of the value per iteration, via `stepIncrement`), so the estimate speeds up when it needs to catch up.
* As soon as the direction **flips**, `step` is reset to 1. That's a brake against overshooting and bouncing back and forth.
* If a move would jump past `newNumber`, the estimate is clamped to `newNumber` and the leftover `step` is corrected (see `// line 6` and `// line 18` in the code).

## Why convergence is quadratic

Because `step` accumulates instead of resetting each time, a sustained run in one direction makes the per-step move grow like 1%, 2%, 3%, 4%... of the value. Total distance covered therefore grows like **n²**. Catching up to a big level jump takes about `sqrt(2 × 100) ≈ 14` consecutive same-direction steps (with `STEP_DIVISOR = 100`), not \~100 as a flat 1% step would need.

## Why it resists outliers

Because each step is small (\~1% of the value) and the direction-flip resets `step` back to 1, **one huge spike barely moves the estimate**. It creeps toward the outlier instead of jumping to it. This is what makes `approxMedian` resistant to attacks like "spam a batch of high-tip swaps to yank the median." `MPFHook` adds two more layers on top of this — see [Snapshots and update gating](#snapshots-and-update-gating) below.

## Example with 20 transactions

Below is a sequence of priority fees (in gwei). Most values sit around **10-13**, but transactions #8 and #16 are clear outliers (300 and 250), simulating a manipulation attempt:

```text theme={null}
10, 12, 9, 11, 13, 10, 12, 300, 11, 10, 9, 13, 12, 10, 11, 250, 9, 10, 12, 11
```

| #  | tx (gwei) | approxMedian | step | direction | real median (of all tx so far) |
| -- | --------- | ------------ | ---- | --------- | ------------------------------ |
| 1  | 10        | 1            | -1   | up        | 10                             |
| 2  | 12        | 2            | 0    | up        | 11.0                           |
| 3  | 9         | 3            | 1    | up        | 10                             |
| 4  | 11        | 5            | 2    | up        | 10.5                           |
| 5  | 13        | 8            | 3    | up        | 11                             |
| 6  | 10        | 10           | 2    | up        | 10.5                           |
| 7  | 12        | 12           | 2    | up        | 11                             |
| 8  | **300**   | 17           | 5    | up        | 11.5                           |
| 9  | 11        | 13           | 1    | down      | 11                             |
| 10 | 10        | 11           | 2    | down      | 11.0                           |
| 11 | 9         | 9            | 2    | down      | 11                             |
| 12 | 13        | 10           | 1    | up        | 11.0                           |
| 13 | 12        | 12           | 2    | up        | 11                             |
| 14 | 10        | 11           | 1    | down      | 11.0                           |
| 15 | 11        | 11           | 1    | down      | 11                             |
| 16 | **250**   | 12           | -1   | up        | 11.0                           |
| 17 | 9         | 11           | -2   | down      | 11                             |
| 18 | 10        | 10           | -1   | down      | 11.0                           |
| 19 | 12        | 11           | -2   | up        | 11                             |
| 20 | 11        | 11           | -2   | up        | 11.0                           |

*(direction: "up" means `positive = true` after this step, "down" means `positive = false`)*

### What this table shows

1. **Fast start (tx 1-7).** `approxMedian` starts at 0 and catches up to the real level (\~10-11) in about 5-6 same-direction steps. That's the quadratic convergence from the growing `step`.
2. **Outlier at tx 8 (300 gwei).** The real median barely moves (11 → 11.5) and `approxMedian` only moves by its current `step` of 5, from 12 to 17. It does **not** jump to 300.
3. **Quick correction (tx 9-11).** As soon as the next value (11) is below the estimate again, `step` resets to 1, direction flips, and the estimate settles back into the 9-13 range.
4. **Steady state (tx 12-20).** After the transient, `approxMedian` stays in a tight 10-12 band, matching the real rolling median (\~11), even through a second outlier (250 at tx 16).

## Snapshots and update gating

`MPFHook` doesn't feed the fee formula from the live `approxMedian` directly. Two extra layers sit on top, both meant to stop an attacker from moving the reference value within a single block.

### One snapshot per block

* On the first swap that touches a registered pool in a new block, the current `approxMedian` is copied into `blockMedianSnapshots`, a circular buffer with `SNAPSHOT_WINDOW = 15` slots (about 15 blocks, similar to Uniswap's Truncated Oracle hook).
* Every other swap in that same block is a no-op here. No matter how many swaps happen in one block, at most one snapshot gets written.
* The value the fee formula actually uses (`referenceMedian`, i.e. `M`) is the average of the last 15 snapshots, not the live `approxMedian`. Shifting it takes several blocks of sustained effort, not a single burst of swaps.

### Median updates require real price movement

* The live `approxMedian` is only updated when a swap moves the pool's price (its tick) far enough from the tick recorded at the last accepted update.
* How far is "far enough" scales with the pool's liquidity, roughly `~sqrt(REFERENCE_LIQUIDITY / liquidity)`: a shallow pool needs a bigger tick move (a small trade can swing it cheaply), a deep pool needs a smaller one (moving its price is already expensive).
* This keeps a burst of same-price or dust swaps from nudging the median with no real price movement behind them.

Together, these two layers mean an attacker has to actually move the pool's price *and* sustain that over multiple blocks just to shift the value the dynamic fee reacts to.

## Key takeaway

`approxMedian` is not a live snapshot of the true median. It's a **smoothed, noise-resistant approximation** that:

* converges to the real value in about 10-15 same-direction updates, and
* mostly ignores single outliers thanks to the small step size and the direction-flip reset.

That's what makes it a good basis for `referenceMedian` in `MPFHook`, especially with the snapshotting and update-gating layered on top (see above).
