Inspiration
It started with a number that didn't make sense. Model a simple ETH/USDC pool: someone buys 10 ETH, moves the price, and for one block the pool is sitting on money it hasn't given away yet — worth more than it started, marked at the true price. Then an arbitrageur takes it, and a block builder takes most of that. Nobody stole anything. The pool just had no way to know it was mispriced, and no mechanism to charge for the privilege of correcting it.
That's the whole idea: the pool was never robbed, it was refunded — and it refunded 100% every time. We wanted a Uniswap v4 hook that keeps some of that value in the loop, split between the trader who moved the price and the LPs who funded the pool that got drained.
The harder question came from a second case. Sometimes nobody trades at all — the market just moves, the pool sits stale, and an arbitrageur drains it anyway. That's loss-versus-rebalancing (LVR), formalized by Milionis, Moallemi, Roughgarden & Zhang (2022), Automated Market Making and Loss-Versus-Rebalancing. It has a different villain: there is no trader to blame, only LPs losing money to the world changing around them. We got interested in whether one mechanism could handle both cases without special-casing either.
What it does
Backdraft is a Uniswap v4 hook that watches the gap between a pool's price and an external reference. When the gap exceeds a threshold, it opens a contribution ledger: every swap that widens the gap is credited proportionally, every swap that narrows it pays a surcharge, escrowed inside the hook.
The math
Detecting the gap. On the pre-swap tick against a reference tick:
$$ \text{gap}{\text{before}} = \text{tick}{\text{pool}} - \text{tick}_{\text{ref}} $$
A gap is open once $|\text{gap}| > \tau$, where $\tau = 65$ ticks — a threshold derived from measured reference error, not chosen by taste (see Research below).
Pricing a closing swap. beforeSwap cannot see the post-swap tick without simulating the trade, so the surcharge is priced on direction and the peak gap reached during the gap's life, not the prevailing one — otherwise splitting one large closing swap into many small ones buys a discount:
$$ \text{surchargeBps} = \min\left(\rho \cdot |\text{gap}_{\max}|,\ \kappa\right), \qquad \text{surcharge} = \text{notional} \times \text{surchargeBps} $$
where $\rho$ is captureRateBps and $\kappa$ is surchargeCapBps.
The contribution ledger. Per swap, the change in absolute distance from the reference:
$$ \Delta_{\text{gap}} = |\text{gap}{\text{after}}| - |\text{gap}{\text{before}}| $$
- $\Delta_{\text{gap}} > 0$ (widening):
contribution[tx.origin] += Δgap,totalContribution += Δgap - $\Delta_{\text{gap}} < 0$ (narrowing): surcharged, per the formula above
No single address is ever named "the" originator — contribution accrues additively across however many trades built the gap.
Settlement — one formula, two regimes. When a gap closes, the escrow splits by how much of it the ledger actually explains:
$$ \text{explained} = \min\left(\frac{\text{totalContribution}}{\text{maxAbsGap}},\ 1\right) $$
$$ \text{traderPot} = \text{escrowed} \times \alpha \times \text{explained}, \qquad \text{lpPot} = \text{escrowed} - \text{traderPot} $$
where $\alpha$ is traderShareBps. If trades caused the gap, the ledger explains nearly all of it ($\text{explained} \approx 1$) and traders collect their share. If the gap opened because the market moved — nobody in the pool traded — the ledger is empty, $\text{explained} = 0$, and 100% goes to LPs as LVR compensation. There is no if (exogenous) branch anywhere in the contract. An empty ledger is the attribution result; it falls out of the same arithmetic as the endogenous case.
The reference price itself is a weighted signal, not a single read. A divergence check guards against a manipulated reference:
$$ \text{divergence} = \max\Big(\,|\text{spot}{\text{deep}} - \text{TWAP}{\text{deep}}|,\ |\text{spot}{\text{fast}} - \text{spot}{\text{deep}}|\,\Big) $$
Taking the max of both disagreements closes the route where an attacker splits a push across both pools to hide it. Below the guard threshold the surcharge multiplier is $1.00\times$; above it, the multiplier rises linearly rather than freezing the hook outright — freezing was our first design, and it turned out to be the wrong call (see Challenges).
How we built it
We split the work: hook core (beforeSwap / afterSwap, the ledger, escrow, settlement) and the reference-price reader on one side; LP eligibility, checkpointing, and the invariant/fuzz suite on the other. Eight build days, Foundry, Solidity — and, before any of that Solidity depended on it, a full Python measurement pipeline run against real mainnet data.
The reference price was the part that needed research, not just engineering. Our first instinct — a damped EMA of the pool's own tick history, no external dependency — is structurally broken: a pool cannot detect its own staleness from its own history, because during an exogenous move the EMA is the stale price. Worse, the arbitrageur correcting the pool moves its tick away from the EMA, so the ledger would have credited — and rebated — the very bot draining the LPs.
So we pulled Uniswap v3 Swap events across all four ETH/USDC fee tiers over three separate mainnet windows (14 Jul, 24 Jul, and 19–21 Aug 2026) and measured every candidate reference against Binance spot as ground truth. The result was not what standard oracle design would predict: the freshest reference wasn't the deepest pool, it was the thinnest one, because it's the arbitrageurs' first stop and gets corrected constantly.
| Source | Mean error vs Binance | Liquidity share |
|---|---|---|
| v3 0.01% | 3.48 bps | 10% |
| v3 0.05% | 4.18 bps | 78% |
| v3 0.30% | 13.82 bps | 12% |
| v3 1.00% | 50.90 bps | 0.2% |
This ordering — cheap tier leads, deep tier lags — independently reproduces a finding from Alexander, Chen, Deng & Fu (2025), Price Discovery and Efficiency in Uniswap Liquidity Pools, Journal of Futures Markets 45(8), 1023–1048, who report the same lead/lag structure from three years of minute-level data. We didn't know that paper's result going in; we measured it, then found the literature agreed.
Standard oracle practice — weight sources by liquidity, take a weighted median — actually selects the staler price on this pair: every combination containing the deep pool returned exactly the deep pool's error, to three decimals, because a liquidity-weighted median just returns whichever source holds over 50% of the weight. Multi-source aggregation bought 0.000 bps of accuracy for 3–4x the read cost. So we split the job instead: price from the fast, thin pool; sanity-check against the deep pool's own spot-vs-TWAP divergence.
By the time Solidity started, every constant in the contract — the 65-tick gap threshold, the 50-tick guard deviation — traced back to a measured sample-maximum error from that dataset, not a guess.
Challenges we ran into
A ledger-poisoning attack we found in our own design. The first settlement rule was a clean binary: empty ledger → LPs get everything, non-empty → traders get their cut. It's exploitable almost for free. An attacker opens an exogenous gap (the market moved, ledger legitimately empty), sends one dust swap from address A that nudges the gap by two ticks, then closes it from address B and claims a share meant for LPs — having "explained" a 200-tick gap with two ticks of noise. Fixing it meant scaling the trader pot by how much of the gap the ledger actually explains, $\text{totalContribution} / \text{maxAbsGap}$, not merely whether it is nonzero. That dropped the attacker's payoff on a 200-tick gap poisoned by 2 ticks from $250 to $2.50 — below the cost of the dust swap itself.
Freezing the hook was the wrong instinct. Our first divergence guard froze pricing entirely above 50 ticks of spot-vs-TWAP disagreement — and on paper it looked great, cutting worst-case reference error 75% for only ~10% downtime. The problem: the blocks where spot and TWAP disagree are exactly the blocks where a real exogenous move is happening. A freeze there switches the hook off during the events it exists to capture. Bucketing dislocation value by dollar-weighted, not block-weighted, share showed the freeze was invisibilizing 37–91% of capturable value depending on the pool. We replaced the freeze with a graduated multiplier that prices divergence instead of hiding from it, and reserved an outright freeze for an absurdity backstop far beyond normal volatility.
JIT liquidity gaming the LP payout. A bot minting a position one block before a gap closes shouldn't collect LP compensation it did nothing to earn. We added an age filter tied to the block a gap opened, and — the part that took real iteration — made any liquidity increase reset that position's age, so a bot can't top up a seasoned position with fresh capital and inherit its seniority.
Deciding a surcharge's direction before we can know its size. beforeSwap can't see the post-swap tick without simulating the trade, so the exact $\Delta_{\text{gap}}$ isn't available at charge time — only the pre-swap gap and direction are. We price off the peak gap reached, which slightly overcharges swaps that only partially close a gap; we bounded that with the surcharge cap rather than pay for a second delta path in afterSwap.
Admitting the mechanism has a blind spot. The hook literally cannot see gaps under the 65-tick threshold, by construction. We measured, rather than assumed, where that hurts: on a pool that's already the market's price-setter, most dislocation value never clears the threshold at all. Backdraft is economically meaningful on thinner pools bootstrapping against a deep incumbent, and close to irrelevant on a pool that already sets the price. That's a positioning statement, and we'd rather publish it than hide it.
What we learned
That an elegant piece of logic — an empty ledger meaning "exogenous," no branch required — can hide a security hole precisely because it looks complete. We trusted the emptiness before we tried to poison it. That oracle-design intuition ("weight by depth, trust the deepest pool") can be actively wrong for a specific mechanism, and the only way to know is to pull real swap data against a real ground truth and check, the way Seoev et al. (2026) and Ghasemlu (2026) approach MEV and AMM fee design empirically rather than by assumption. And that the honest version of a project — the one with a "what it does not do" section listing measured limitations, not a hidden asterisk — is more convincing than the one that omits them, because every number in it is something a reviewer can reproduce.
Research cited
- Milionis, J., Moallemi, C., Roughgarden, T., & Zhang, A. L. (2022). Automated Market Making and Loss-Versus-Rebalancing. arXiv:2208.06046.
- Alexander, C., Chen, D., Deng, X., & Fu, S. (2025). Price Discovery and Efficiency in Uniswap Liquidity Pools. Journal of Futures Markets, 45(8), 1023–1048.
- Seoev, A. et al. (2026). The Origins of MEV. arXiv:2604.27979.
- Ghasemlu, K. (2026). Optimal Dynamic Fees for AMMs. arXiv:2606.21769.
- Backdraft's own reference-price measurement, run across three mainnet windows against Binance ground truth:
backdraft-python-scripts, full write-up inAPPENDIX.md.
Log in or sign up for Devpost to join the conversation.