Evidence

Everything the measurement found

Named hooks, the detectors scored against them, and who routes users through them. Every figure is computed in this repo from a snapshot you can re-derive.

The hooks that charge more than they quote

A uniform random sample of Base fills, each re-quoted against the state immediately before it. Of the 69,242 hooks on Base, 25 had enough fills to classify at all, and these are the ones whose charged fills beat their own measurement noise.

4 of 25 hooks with enough fills to classify
HookFillsChargedOver-deliveredNet rateMedian excessIn hooklist
0x1f91c998…e02acc7821839911%400 bpsyes
0x985c14ba…ca2acc7511261053%142 bpsyes
0xa5c4a1be…5a41453611322%99 bpsno
0x0d5d83c5…aba8cc419217%45 bpsno

“Over-delivered” counts fills that came out better than quoted. A hook cannot do that, so those are measurement error, and because the error is symmetric, their count estimates the false positives in the column beside them. A hook only appears here if its charged fills beat its own over-delivered tail. A hook nobody has measured is reported as unmeasured, never as clean.

“In hooklist” means the hook is in Uniswap’s public hooklist with verified source. Anyone can open a pull request to add theirs, it records who deployed the hook and what it is permitted to do, and it says nothing about what the hook charges. It is not the private allowlist Uniswap’s routing API uses, which is not published and cannot be read from here.

Measured from Base blocks 25,350,988 to 51,779,491, 28,165,155 indexed rows. Computed in this repo, from data you can re-derive.
census-base
a5379268608d7698… · 15,309,659 rows · blocks 25,350,988–51,778,292
fills-base
ed5147b030554d38… · 12,855,496 rows · blocks 50,483,491–51,779,491
built at
f1e4dcb-dirty
threshold
beats its own noise floor

Three ways to ask whether a hook can spoof

Each looks at something different, and each fails differently. Reporting where they disagree is more useful than merging them into one verdict.

MethodWhat it asksFlagsBlind to
StaticDoes the bytecode contain a distinguishing opcode?4whether it ever runs
DifferentialDoes the quote move when only the environment changes?0dice rolls and state-keyed behaviour
TraceDoes the hook execute one while pricing?1hooks that read state instead
Settled tradesWhat did users actually receive?2nothing, but only after the fact
Measured from Base blocks 25,350,988 to 51,778,292, 15,378,901 indexed rows. Computed in this repo, from data you can re-derive.
census-base
a5379268608d7698… · 15,309,659 rows · blocks 25,350,988–51,778,292
hooks-base
24516667778bdc50… · 69,242 rows
built at
4e932f0
probe amount
1e15 wei

None of them found a single one

2 hooks in this set were independently measured, from settled trades, as charging more than they quote. That is the ground truth. Every method an integrator could run before a trade was scored against it.

of the 2 hooks that were charging
MethodRunsFoundMissedFalse alarms
Static bytecode scanbefore the trade023
Differential eth_callbefore the trade020
Trace of a priced callbefore the trade020
All three togetherbefore the trade023
Re-quoting settled tradesafter the trade200

The only method with perfect recall is the one that reads a trade that has already settled. By then the user has been paid less than they were quoted.

2 is a small ground truth. It is the overlap between the hooks this repo probed and the hooks it measured from settled trades, and it is too small to claim a detection rate. What it does show is that the checks available before a trade found none of the hooks that were demonstrably charging, and that the bytecode scan raised 3 alarms on hooks that were not.

Measured from Base blocks 25,350,988 to 51,779,491, 28,165,155 indexed rows. Computed in this repo, from data you can re-derive.
census-base
a5379268608d7698… · 15,309,659 rows · blocks 25,350,988–51,778,292
fills-base
ed5147b030554d38… · 12,855,496 rows · blocks 50,483,491–51,779,491
built at
a087fdb-dirty

Everyone is routing into them

849,451
swaps on Base went into the 4 hooks measured as charging more than they quote
who sent them
ProductSwaps into those hooksShare of its v4 swaps
Uniswap233,4314.8%
0x104,64312.9%
unknown-aggregator13,6652.9%
routers nobody has identified497,71250.2% of all fills

This is not an accusation. These products are doing the normal thing, which is to trust a quote. That is the whole point: the gap is invisible from where a router stands, so avoiding it cannot be a matter of diligence.

Every v4 swap logs the contract that called the PoolManager, never the person swapping. Mapping those contracts to the products that run them is what this table does, and 50.2% of them are contracts nobody has mapped. That share is published rather than dropped.

Measured from Base blocks 25,350,988 to 51,779,491, 28,165,155 indexed rows. Computed in this repo, from data you can re-derive.
census-base
a5379268608d7698… · 15,309,659 rows · blocks 25,350,988–51,778,292
fills-base
ed5147b030554d38… · 12,855,496 rows · blocks 50,483,491–51,779,491
built at
ccb5b9a
mapping
analysis/data/routers.csv