The idea
The estimator lives inFrugalMedianLibrary 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. stepis not fixed. It grows while the moves keep going the same direction (roughly 1% of the value per iteration, viastepIncrement), so the estimate speeds up when it needs to catch up.- As soon as the direction flips,
stepis 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 tonewNumberand the leftoverstepis corrected (see// line 6and// line 18in the code).
Why convergence is quadratic
Becausestep 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 resetsstep 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 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:
(direction: “up” means
positive = true after this step, “down” means positive = false)
What this table shows
- Fast start (tx 1-7).
approxMedianstarts 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 growingstep. - Outlier at tx 8 (300 gwei). The real median barely moves (11 → 11.5) and
approxMedianonly moves by its currentstepof 5, from 12 to 17. It does not jump to 300. - Quick correction (tx 9-11). As soon as the next value (11) is below the estimate again,
stepresets to 1, direction flips, and the estimate settles back into the 9-13 range. - Steady state (tx 12-20). After the transient,
approxMedianstays 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
approxMedianis copied intoblockMedianSnapshots, a circular buffer withSNAPSHOT_WINDOW = 15slots (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 liveapproxMedian. Shifting it takes several blocks of sustained effort, not a single burst of swaps.
Median updates require real price movement
- The live
approxMedianis 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.
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.
referenceMedian in MPFHook, especially with the snapshotting and update-gating layered on top (see above).