Github: https://github.com/shreyas-sovani/Markout Slides: Project Link: https://markout-nine.vercel.app/ Demo Video: https://youtu.be/fvdTjGUC8CY
Volatile AMM pairs cannot advertise a tight fee without bleeding LPs to one-shot arbitrage: a toxic trader swaps once, snaps the pool to the external price, and leaves. There is no continuation trade, so any hook that classifies toxicity by “what happened next” waves the worst flow through. The other option is a wide fee, which taxes organic volume and sends it to a tighter venue.
Markout treats the pool’s own later price as the signal. Informed flow moves the AMM to the global price and it stays. Uninformed flow moves it away and natural arbitrage pushes it back within seconds. A 24-second, hook-local window is enough to tell those apart — including a 1:1 reversion that lands in the next 12-second block, which sits on a 50% frontier and refunds. We wanted that judgment on canonical v4 infrastructure, payable through any router, with LPs credited at settle rather than in a side pot.
The unusual part is one memory used twice, not another rebate bolted onto beforeSwap.
Spot still fills immediately at 3 bps. The hook charges a live premium onto the swap caller’s own PoolManager delta, so Universal Router or any integrator pays it with no allowlist. If ≥50% of that swap’s own impact reverted, the premium returns in settle. If it held, in-range LPs are credited in that same transaction via v4 donate whenever L > 0.
Batch reuses the same clock: explicit custody, permissionless clearBatch, epoch TWAP from an append-only accumulator. Two-sided size never touches the curve. Residual execution goes through an immutable child router because v4 skips hook callbacks when the hook is msg.sender — so leftover still pays the premium lane instead of getting a privileged free swap.
It is live on the canonical Sepolia PoolManager, source-verified, with an official PositionManager LP path, 56 Foundry tests (invariants + canonical fork), and on-chain proofs for refund-at-settle, donate-credited-in-settle, and a two-sided batch clear at one TWAP. Honest limits stay named: atomic same-block spot sandwiches, unmatched batch leftover is still a spot swap, the premium is not an LVR hedge.
v4 will not run hook callbacks when msg.sender is the hook, so a hook cannot residual-swap through its own lane. A premium-free clearer was off the table. The fix is an immutable hook-owned MarkoutBatchRouter child — easy to say, easy to get wrong on escrow and uniform-rate accounting.
The two-lane hook also blew EIP-170 at optimizer_runs = 44M (compile printed addresses, broadcast refused). 20000 runs fits and still lets v4-core Pool.swap compile.
The operator EOA is EIP-7702-delegated, so forge nonce fan-out fails; deploys and proofs had to go sequential (--slow). PositionManager DECREASE takes uint256 and negates internally — a negative int256 overflows SafeCast. Publicnode silently prunes ~day-old logs and viem does not fail over on empty arrays, which made the hosted history look dead until RPC order became env → tenderly → publicnode.
forge coverage disables optimizer/via_IR; --ir-minimum stack-overflowed clearBatch until it was split into helpers. None of that changed the product thesis; it was all “ship on the canonical stack or don’t claim it.”