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.
| Hook | Fills | Charged | Over-delivered | Net rate | Median excess | In hooklist |
|---|---|---|---|---|---|---|
| 0x1f91c998…e02acc | 782 | 183 | 99 | 11% | 400 bps | yes |
| 0x985c14ba…ca2acc | 751 | 126 | 105 | 3% | 142 bps | yes |
| 0xa5c4a1be…5a4145 | 36 | 11 | 3 | 22% | 99 bps | no |
| 0x0d5d83c5…aba8cc | 41 | 9 | 2 | 17% | 45 bps | no |
“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.
| Method | What it asks | Flags | Blind to |
|---|---|---|---|
| Static | Does the bytecode contain a distinguishing opcode? | 4 | whether it ever runs |
| Differential | Does the quote move when only the environment changes? | 0 | dice rolls and state-keyed behaviour |
| Trace | Does the hook execute one while pricing? | 1 | hooks that read state instead |
| Settled trades | What did users actually receive? | 2 | nothing, 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.
| Method | Runs | Found | Missed | False alarms |
|---|---|---|---|---|
| Static bytecode scan | before the trade | 0 | 2 | 3 |
| Differential eth_call | before the trade | 0 | 2 | 0 |
| Trace of a priced call | before the trade | 0 | 2 | 0 |
| All three together | before the trade | 0 | 2 | 3 |
| Re-quoting settled trades | after the trade | 2 | 0 | 0 |
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
| Product | Swaps into those hooks | Share of its v4 swaps |
|---|---|---|
| Uniswap | 233,431 | 4.8% |
| 0x | 104,643 | 12.9% |
| unknown-aggregator | 13,665 | 2.9% |
| routers nobody has identified | 497,712 | 50.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