Search projects, hackathons, sponsors and tech

    ← All projects

    Sworn

    A Uniswap v4 router that makes the quote and the trade the same transaction, so hooks can't lie.

    Demo

    About

    A Uniswap v4 hook is arbitrary code that runs inside PoolManager.swap. It sees tx.gasprice, tx.origin and block.coinbase, all of which differ between an eth_call quote and a real transaction. So a hook can be honest to every quoting engine that exists and dishonest to the person paying, and the cost to build one is a single modifier. In September 2026, 0x published "Uniswap v4 hooks were a mistake": 84,163 hooks analysed, 54.2% malicious. Hayden Adams replied "skill issue, don't route to bad hooks". He is right. The question that leaves open is the one an integrator actually faces: how do you know which ones are bad, at the moment you route? Sworn answers it by measuring first and then closing the gap. I indexed every v4 pool on four chains from Initialize logs, 15.7 million pools, then re-quoted a uniform random sample of 10,000 Base fills against the state immediately before each one. 4 of the 25 hooks with enough fills to classify charge more than they quote, and 849,451 swaps reached them, through UniversalRouter, through 0x, through everything. Roughly half of all charged fills are measurement error, estimated from the fills that measured as over-delivered, which a hook cannot do. That noise floor is subtracted before any hook is named. Two findings came out of that. PoolManager emits Swap before afterSwap, so the event that every v4 indexer and hook-scoring tool is built on structurally under-reports the hooks that take the most. And detection does not work: static bytecode analysis, differential eth_call and trace analysis each scored zero recall against ground truth. Only re-quoting settled trades caught anything, and that runs after someone has already been underpaid. So Sworn does not detect. SwornRouter takes the v4 lock, runs every candidate route for real, reverts each probe so state and transient storage roll back, executes the best, and asserts executedDelta == probed[chosen]. A hook that lies makes those two disagree and the trade does not happen. Against a live Base hook at block 51,247,545 that prices two callers 707 bps apart for the identical swap, Sworn probed, saw the real offer, and settled elsewhere for 672 bps more. That hook was one this repo's own offline pipeline had not flagged, which is the strongest argument in the project: a score is retrospective and lossy, a probe is neither.

    How it's made

    Contracts. Solidity 0.8.26 under Foundry, via-IR, optimizer_runs = 44444444 and evm_version = cancun to match v4-core exactly; at lower settings the Yul optimizer cannot clear v4's stack pressure. SwornRouter is IUnlockCallback, so it is periphery, not a hook, with no mined permission bits. The probe isolation is the part worth calling out. For a hook to cheat Sworn it would have to answer the probe honestly and the execution dishonestly, which means telling them apart. Probe and execution go through the same external self-call, and the probing flag is only read after the last externally observable call, so msg.sender, call depth and calldata shape are identical. Probes are forced to revert (ProbeMustRevert), which is what rolls their state back, including EIP-1153 transient storage, which does not survive a revert either. Both get an identical gas stipend, enforced against EIP-150's 63/64 rule, so gasleft() leaks nothing. tx.gasprice and block.* are free because it is all one transaction. Measurement. Python over debug_traceTransaction, with pandas and pyarrow parquet compaction across 28 million indexed rows. Recovering what a swap actually did means decoding the swap() calldata out of the trace rather than reading the Swap event, because of the emission-order defect. Amounts are stored as strings: v4 deltas exceed 2^63 and parquet has no int128. Provenance. Every result file carries meta.snapshots[] with a sha256 of its input and the commit that produced it. The README is generated from a template where {{result:divergence.json:totals.divergent_hooks}} resolves at build time, and verify_readme_numbers.py fails CI on any hand-typed digit. The diagrams, the banner and the social image are SVG generated from the same result files. Testing. Twelve attacker capabilities from the threat model, each with a working hook fixture in ToxicHooks.sol that tries the attack and fails, plus fork tests pinned to real Base and BNB blocks against hooks nobody in this repo wrote. Rest of the stack. A Next.js 15 static export that reads the committed result files at build time, so the dashboard and the README cannot disagree. A viem SDK, buildSwornCall, that turns an existing quote into router calldata, so an integrator adopts this by changing the call they already make rather than rewriting their router. And EIP-712-signed hook scores written on-chain to HookBook on Base Sepolia by a scheduled attestor: signed rather than sent, so anyone can relay an attestation and pay the gas for it. HookBook returns INSUFFICIENT_DATA for a hook nobody has measured, never a clean zero, because a registry that reports absence as safety is worse than no registry.