Skip to main content
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 . 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 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

  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).
Last modified on August 10, 2026