Every automated market maker charges the same fee to two people doing completely
different things. This one holds a bond, waits, reads its own price, and then
works out which of the two you were.
§1 Same fee, two different people
Exhibit A
The uninformed trade
Somebody saw a price on a screen, decided they wanted the token, and bought it.
They have no idea where the price is going. When they are gone the price is
roughly where it was.
The provider who filled them is straightforwardly better off. They collected a
fee for standing still.
Exhibit B
The informed trade
Somebody knows something. A listing, a print on another venue, a liquidation
about to hit. They buy, the price moves, and it stays moved, because
what moved it was true.
The provider is worse off by almost exactly the distance the price travelled,
and no fee at the rates anybody charges comes close to covering it.
This is not a marginal effect. It is the dominant term in what a liquidity provider
actually earns. It has a name — loss-versus-rebalancing — and every
dashboard in this industry reports fee APR as though it did not exist. Fee APR is a
gross number. Nobody publishes the net one, because nobody measures which trades
were which.
§2 Every existing answer is a guess made first
By volatility Charge more when the tape is moving. Retail trades in volatility too.
By size Charge more for size. The best-informed trade of the day can be small.
By address Charge more for addresses that look like bots. A fresh EOA costs nothing to make.
Every one of them is a proxy. Every proxy is gameable. And all of them end up taxing
the retail buyer in order to insure against the arbitrageur.
You cannot tell an informed trade from an uninformed one when it arrives.
You can tell perfectly well an hour later.
If the price came back, the trade was noise. If the price stayed, the trade was
information. That is not a heuristic and not a model — it is the definition, and it
is legible in the pool's own state, with no oracle, no committee, and nothing that
can be lied to.
§3 So the pool stops guessing. It waits.
A bond, on top of the ordinary fee
Every swap posts BOND_BPS basis points, taken in afterSwap from the currency you did not name. It is held, not taken.
The pool writes down what happened
The tick before your swap, the tick after, and the block number. Nothing else. It has no opinion about you.
After WINDOW blocks, anybody can settle
The hook reads its own current tick and asks one question: how much of your impact came back?
The bond splits on the answer
All of it came back — the whole bond returns to you. None of it — the whole bond is donated to the providers who were in range. Some of it — split, in proportion.
Collect it promptly, or it decays
A verdict collected within GRACE blocks of becoming available is worth all of itself. After that it decays in a straight line to nothing at DEADLINE. refund = bond × ratio × weight.
A retail buyer the market shrugs off gets everything back, and has paid the ordinary
fee and nothing else — less than they would pay in any pool priced to survive
arbitrageurs. An arbitrageur who moves the price and is right pays the bond, to
precisely the people they took it from. Nobody is banned. Nobody is scored on how they
look. The pool never needs to know who you are, because it finds out what you did.
Drag the price and watch the verdict change.
Notice
What this costs, said plainly
Your capital is held for WINDOW blocks.
That is a real cost and there is no way around it. A verdict that arrived instantly would be a guess again.
You must come back and settle, and reasonably promptly.
Anyone can call settle for you. Nobody is paid to. In practice you settle your own, and you pay that gas. Leave it past the grace period and the refund starts decaying; leave it to DEADLINE and there is nothing left to collect.
The swap costs about 59 000 more gas.
Measured on a mainnet fork against an identical pool with no hook: 109 938 against 51 220 in steady state. That is the price of two storage slots and a transfer, and it is not free.
The window is a parameter, not a truth.
Too short and slow information looks like noise. Too long and ordinary drift looks like information. It is fixed at construction and cannot be changed by anyone, including whoever deployed it.
You choose when the price is read, and that is a real option.
The verdict has to be read live — nothing on chain can read a tick from a block that has already passed. So a trader can wait for a friendlier reading, and over a long enough deadline ordinary drift will eventually supply one. That option cannot be removed. It is priced: waiting past GRACE costs more than the better reading is worth. Inside the grace window it still exists.
The receipt is issued to tx.origin.
Through a normal wallet that is you. Through a smart-contract account — a 4337 bundle, a Safe module fired by a keeper — it is not you, and the bond is not yours to reclaim. Swap from an EOA.
A forfeit with nobody in range is parked, not returned.
If the donation has no in-range liquidity to land on, the value becomes stranded and anyone can call sweepStranded() later to retry. It never goes back to the trader.
§4 What it deliberately does not do
It does not gate beforeInitialize and it does not gate liquidity.
The hook flags are 0x1044 — afterInitialize,
afterSwap and afterSwapReturnsDelta — and nothing
else. Every one of them fires after the thing it observes has already happened, so none of
them can refuse anything. The Uniswap interface can open this pool, mint a real position, and let
the provider leave whenever they like. A hook that can refuse a withdrawal is a hook whose promises
are worth nothing.
There is no owner. There is no upgrade path. There is no address anywhere in the
contract that can be changed after it is deployed.
Status
Nothing is deployed. There are no contract addresses to publish, no pool to trade,
and no numbers on this page that came from a chain.
Mechanism
Six things happen, in order. Five of them are on-chain and one of them is you
remembering to come back, reasonably promptly.
§1 The bond
When the pool opens, the hook writes down the price
afterInitialize, once, and only that. It exists to close a hole:
pre is the tick the previous swap left behind, so with no opening
tick the very first swap would be judged against itself —
pre == post, zero impact, full refund whatever it did. This is the
only reason the hook needs to know a pool was created at all, and it still cannot refuse to
let one be.
The swap executes normally
No beforeSwap. The hook is not consulted before your trade and
cannot price it, delay it, or refuse it. You pay the ordinary pool fee, at the ordinary
rate, to the ordinary place.
afterSwap holds back BOND_BPS
The bond is taken from the unspecified currency — the one you did not
name. On an exact-input swap that is the side you receive, so you get slightly less than
quoted. On an exact-output swap it is the side you pay, so you pay slightly more. Either
way it is your money, and either way you can have it back.
Mechanically: the hook calls take to move the bond out of the pool
and into itself, which leaves it owing the pool exactly that much, and returns an
int128 that credits exactly that much back. The hook's position inside
the lock nets to zero, which is the only thing v4 checks.
The pool writes down the trace
pre — the tick your swap started from.
post — the tick it ended at.
block.number. And tx.origin, as the claimant.
pre is the tick the previous swap left behind, or the opening tick
if yours is the first. Minting, burning and donating never move the tick, so the pool is its
own record of where it was — no oracle, no snapshot, no separate storage to keep honest.
After WINDOW blocks, settle(id) is open to anyone
The refund always goes to the claimant, never to the caller. There is nothing to be gained
by settling somebody else's trade and nothing to be lost by letting them settle yours.
The verdict splits the bond
refund = bond × ratio × weight to the claimant.
toProviders = bond − refund, donate()d to the
in-range liquidity providers of that exact pool. Not a treasury. Not the deployer. The
people who were standing there when it happened.
The weight is the second half of the answer and it is covered in §3. In short: collect
inside GRACE and it is 1.
§2 The verdict
A swap moves the price from pre to post.
Some blocks later the price is at now. The only question the
contract answers is how much of that move came back.
ratio = (post − now) ÷ (post − pre), clamped to [0, 1]
Move the tick one way and post − pre is positive; if the price
comes back, post − now is positive too, and the ratio is positive.
Move it the other way and both terms are negative, and the ratio is positive again.
The sign cancels — which is what lets the same line judge a buyer and a seller without a
branch that somebody could get backwards. In this pool the token is
currency1 and ETH is currency0, so a
buy lowers the tick and a sell raises it. The formula does not care, which is the point.
The five cases, exhaustively
Condition
Ratio
Means
impact == 0
1.0
The swap did not move the tick at all — too small to cross a boundary. Nothing to have reverted, so the honest answer is a full refund. Treating a zero denominator as undefined and defaulting to zero would charge the smallest traders in the pool the maximum penalty, which is precisely backwards.
sign(back) ≠ sign(impact)
0.0
The price kept going the way you pushed it. Everything you did to the price is still there, and then some.
|back| ≥ |impact|
1.0
Fully reverted, or overshot past where it started. Overshoot counts as a full reversion and no more: a price that blew past its origin did not do so because of this trade.
0 < |back| < |impact|
|back| / |impact|
Partial. The market kept some of what you did and gave the rest back.
weightAt == 0
0.0
Once the decay ramp reaches zero at openedAt + DEADLINE, the answer is fixed and the price is not read at all. preview short-circuits to (0, 0, bond) and the whole bond belongs to the providers.
§3 The weight, and the one thing you can still game
The ratio says what the trade was. It does not say when anybody asked. And when somebody asks
turns out to matter, because the reading has to be taken live — nothing on
chain can read a tick from a block that has already passed. There is no historical price to
consult. The pool's tick is a current value or it is nothing.
So the trader chooses the moment the question is asked, and over a long enough deadline that
choice is worth real money. Wait, and sooner or later ordinary drift carries the price back past
where you left it — at which point a trade that genuinely was informed collects a ratio of 1 for
a move that never actually reverted on its own merits. That option cannot be removed. It can be
made not worth having.
refund = bond × ratio × weight
A verdict collected within GRACE blocks of becoming settleable is
worth all of itself — weight = 1. After that the weight falls in a
straight line to zero at DEADLINE. Waiting for a friendlier reading
costs more than the friendlier reading is worth, and the better number arrives attached to a
smaller multiplier.
The same ramp is what retires an abandoned bond, which is the other job it is doing. There is no
cliff at the deadline and nothing in the contract that turns on a single block — the value drains
away continuously instead of vanishing at one.
Be clear about what this does not fix. Inside the grace window the option is free: a trader
watching their own position who sees a favourable tick forty minutes in will take it, and there
is no answer to that beyond WINDOW being long enough for the price to
have said something. The ramp prices the patient version of the strategy, not the quick one.
§4 The simulator
The arithmetic below is a direct port of Reversion.ratio,
Reversion.split and weightAt,
integer-for-integer in the same fixed point, including the clamp, the zero-impact case and the
order in which preview composes them. Move the controls and the verdict
moves with them.
Presets
Impact +80 ticks (+0.80%) · set 0.01 for the zero-impact case
kept goingstayedhalf backall back
Contract bound: 0 < BOND_BPS ≤ 500
Direction
One formula, no branch. The sign cancels.
Blocks after your swap. The ramp below is what this slider is moving along.
Price trace from the tick before the swap, to the tick after, to the tick at settlement.
Refunded to youTo the providers
Reversion ratio
1.000000
100.00%
Verdict weight
×1.0000
full value
Bond posted
0.0060
ETH-equivalent
Refunded to you
0.0060
100.00% of bond
To the providers
0.0000
0.00% of bond
—
What the verdict is worth when you collect it
full value
×1×0
0swap300WINDOW600GRACE ENDS216 000DEADLINE
450 blocks since your swap
≈ 1.5 hours at 12s blocks
WINDOW = 300, GRACE = 300 and
DEADLINE = 216 000 — roughly an hour, an hour and thirty days — are the
values this pool is built for. All three are constructor arguments and all three are immutable
once set. Nothing is deployed, so nothing is set yet. The horizontal axis is compressed over the
first 600 blocks so that the window and the grace period are visible at all; the decay itself is
drawn on a linear block axis, which is why it is a straight line.
Tick impact is derived from trade size against an illustrative full-range pool of 500 ETH a
side (Δtick ≈ 40 × size). A real pool's impact is whatever its own
liquidity produces; the contract never models it, it only reads the tick before and after.
The bond is shown in ETH-equivalent terms so the arithmetic is legible — on an exact-input
buy it is actually taken from the tokens you receive.
§5 Where the forfeit goes
donate(), into the same pool key, in the same currency the bond was
taken in. That means it accrues to whoever holds in-range liquidity at that moment, in
proportion to how much of it they hold. It is not routed through a treasury, it does not
touch the deployer, and there is no fee on it.
If nobody is in range, donate reverts. The hook catches that, parks
the value as stranded0 / stranded1, and emits
Stranded. Anyone can call sweepStranded()
afterwards to retry. It is never handed back to the trader, who has just demonstrated they did
not deserve it.
The Docket
A pending verdict is an asset. The Docket makes that explicit: it turns a bond awaiting
judgment into an ERC-721 you can hold, transfer or sell, and it keeps the running public
record of how every address's trades turned out.
§1 The receipt
Every swap already produces a trade id inside the hook, whether or not you do anything about
it. Minting the matching receipt is optional, costs gas, and is deliberately kept out of the
swap itself — a trader who never touches the Docket is paid exactly the same. What it changes
is who gets paid: the hook asks the Docket holderOf(id) at
settlement, and if a receipt exists the refund goes to whoever holds it.
function claimantOf(uint256 id) public view returns (address) {
address h = DOCKET.holderOf(id);
return h == address(0) ? pending[id].trader : h;
}
claim(id) mints it, and only the original trader can call it, only
while the trade is unsettled and unclaimed. The token id is the trade id. After that it
is an ordinary ERC-721: transfer it, approve it, sell it.
Which means you can sell the verdict before it arrives. You bought, you would rather have the
bond back now than probably-the-bond-back later, and somebody else disagrees with you about what
the price is going to do. That disagreement has a price, and this is where it gets one.
The metadata is generated on chain — tokenURI returns a base64 data
URI with an SVG drawn from the trade's own numbers: three points, where the price was, where
this trade put it, and where it is now, which is the whole argument in one line. Status reads
PENDING, READY or
SETTLED, and the attributes carry the opening block, the impact in
ticks and the reverted percentage.
§2 The ledger
The Docket also keeps a standing per address: trades settled, bonds posted, and
bonds refunded. It is appended to by the bound hook at settlement and by
nothing else. There is no way to edit it, no way to delete a line, and no address that can.
It is written against the address that made the trade — never against whoever
ended up holding the receipt, because a receipt can be sold and the trade it describes cannot
be. So selling your claim moves the money and not the record. The hook is explicit about this:
the refund goes to claimantOf(id) while the line goes to
p.trader.
An address with no history scores 1e18, because an address that has
done nothing has taken nothing from anyone. Treat a perfect score with no trades behind it as
exactly the information it is.
score = refunded ÷ posted
→ 1.00
The market never cared what you did
Your trades moved the price and the price came back. You were noise. You were also
cheap to fill, which is why you got your bonds back.
→ 0.00
You moved prices that stayed moved
You were right, repeatedly and measurably. You paid for it, to the people you took it
from. It is also the most expensive number in this system to fake: you would have to be
publicly correct, over and over, at your own cost.
This site takes no position on which end is better. A high score is not a compliment and a
low score is not an accusation. They describe two different jobs, and the pool needs both:
it cannot price without informed flow, and it cannot pay without uninformed flow. The number
exists because the pool has to compute it anyway. Making it readable is the only editorial
decision here.
§3 The shape of the record
Illustrative
Nothing is deployed. There is no ledger to read. The rows below are made up to show the
structure of the table — the columns, the units and the arithmetic. They are not
live data, not historical data, and not a projection of anything.
Example rows · illustrative only · not live data
Address
Trades
Posted
Refunded
Score
0xEXAMPLE…a1
142
0.8410
0.8236
0.979
0xEXAMPLE…4c
1 903
12.6072
1.4130
0.112
0xEXAMPLE…7f
28
0.1902
0.1143
0.601
0xEXAMPLE…b0
6
0.0231
0.0231
1.000
0xEXAMPLE…d9
417
3.0885
0.0000
0.000
Posted and refunded are denominated in whichever currency each bond was taken in, which
depends on the direction of the trade and whether it was exact-input or exact-output. Any
interface that renders this ledger has to say which, or the column is meaningless.
§4 The pool's own ledger
Separately from the per-address record, the hook itself exposes four all-time totals and one
derived figure. These are plain public state on Verdict, not on the
Docket, and they need no indexer to read.
function ledger() external view returns (
uint256 trades, // every trade ever bonded
uint256 bonded, // total posted
uint256 refunded, // total returned to traders
uint256 toProviders, // total forfeited, INCLUDING anything still parked
uint256 held, // bonded - refunded - toProviders
uint256[2] parked // forfeited but not yet delivered: [currency0, currency1]
);
held is the honest one. It is every bond that has been posted and not
yet judged — capital sitting in the contract because its window has not elapsed, or because
nobody has bothered to settle it. If that number is large and growing, people are forgetting to
come back, and the decay ramp is quietly moving their money to the providers.
parked is the one that will catch an indexer out.
toProviders counts everything forfeited, including value that never
reached anybody because no liquidity was in range at the moment it was donated. That residue
sits in parked until somebody calls
sweepStranded(). If you want the amount providers have actually
received, subtract one from the other.
Technical reference
Solidity 0.8.26, Uniswap v4. Read the source before you read this page; this page is only
here to save you time.
§1 Contracts
File
Role
Status
Hindsight.sol
The ERC-20. 1 000 000 000 supply, 18 decimals, EIP-2612 permit, burn. No owner, no mint, no tax, no pause.
not deployed
Verdict.sol
The hook. Holds the bond in afterSwap, judges it in settle.
not deployed
Docket.sol
Optional ERC-721 receipt per trade with on-chain SVG metadata, plus the public per-address standing of posted and refunded.
not deployed
libs/Reversion.sol
The settlement arithmetic. Pure, two functions, no state.
library
libs/Ink.sol
Base64 and number formatting for the Docket's on-chain metadata.
library
libs/V4Params.sol
The v4 parameter structs, restated locally so the source compiles identically against either v4-core release.
library
HookDeployer.sol
CREATE2 factory. A v4 hook's permissions are its address, so the hook has to be mined onto one whose low bits already read 0x1044.
not deployed
No addresses are published on this site because none exist. Any address presented elsewhere as
a Hindsight contract, today, is not one.
§2 Hook flags — 0x1044
The PoolManager reads the low 14 bits of a hook's address to decide which callbacks to fire.
The permissions are not configuration; they are the address. Three bits are set. Here is
every bit, and why it is set or clear.
01000010000100
bit 13 ← → bit 0 · 0x1000 | 0x40 | 0x04 = 0x1044
The three that are set
bit 12 · afterInitialize · 0x1000
Writes down the tick the pool opened at, once, and nothing else. It exists because of a
hole it would otherwise leave: pre is the tick the previous swap
left behind, so without an opening tick the very first swap in the pool would be judged
against itself — pre == post, an impact of zero, and by
the zero-impact rule a full refund no matter what it did. The opening trade would be free.
It still cannot refuse anything. It runs once the pool already exists, it returns its
selector for every pool whether bound or not, and it only writes if the pool is the bound
one and nothing has been recorded yet.
bit 6 · afterSwap · 0x0040
Where the bond is held and the trace is written down. After the swap, so it cannot
price, delay or refuse the trade it is looking at.
bit 2 · afterSwapReturnsDelta · 0x0004
Required for afterSwap's returned
int128 to be applied at all. Without this bit the hook could observe
a swap but not hold anything back from it.
The three that matter most by being clear
beforeInitialize (bit 13) is clear, so nothing here can refuse to let
the pool exist — anyone, including the Uniswap interface, can open it.
beforeAddLiquidity (bit 11) and
beforeRemoveLiquidity (bit 9) are clear, so providers need no permission
to arrive and — the important one — none to leave. Every callback this hook does receive fires
after the fact it observes, which is the same property the whole mechanism is built on.
Bit
Flag
State
Why
13
beforeInitialize
clear
Anyone can open this pool, including the Uniswap interface. There is nothing to gate at initialization and a gate here would mean the pool could only be created by whoever was allowed to.
12
afterInitialize
set
Records the opening tick. Without it the first swap in the pool is judged against itself — pre == post, impact zero, refunded in full whatever it did.
11
beforeAddLiquidity
clear
Providers do not need permission to arrive.
10
afterAddLiquidity
clear
No accounting to keep. Donations are distributed by the PoolManager, not by the hook.
9
beforeRemoveLiquidity
clear
The important one. A hook that can refuse a withdrawal is a hook whose promises are worth nothing. This bit being clear is the reason to trust any of the rest.
8
afterRemoveLiquidity
clear
Leaving is not the hook's business.
7
beforeSwap
clear
At the moment a swap arrives this contract has no opinion about it, which is the entire point of the design. It cannot see your trade before it executes, so it cannot price you.
6
afterSwap
set
Where the bond is held and the trace is written down.
5
beforeDonate
clear
Forfeits are donated by the hook; it does not need to observe donations.
4
afterDonate
clear
Same.
3
beforeSwapReturnsDelta
clear
Follows from bit 7 being clear.
2
afterSwapReturnsDelta
set
Required for afterSwap's returned int128 to be applied at all. Without it the hook could observe but not hold.
1
afterAddLiquidityReturnsDelta
clear
The hook never alters a liquidity delta.
0
afterRemoveLiquidityReturnsDelta
clear
Nor on the way out.
The constructor refuses to exist anywhere else:
uint160 public constant FLAGS = 0x1044;
// in the constructor
if (uint160(address(this)) & 0x3FFF != FLAGS) revert BadFlags();
The four reverting stubs — beforeSwap,
beforeInitialize, beforeAddLiquidity,
beforeRemoveLiquidity — exist only to say out loud, in code, what the
address bits already say: they revert with NeverCalled(), because the
PoolManager will never call them.
§3 The afterSwap delta
afterSwap returns (bytes4, int128). The
int128 is applied to the unspecified currency — the
side the trader did not name. The hook determines which side that is with exactly the same test
v4-core uses, because if the two disagree the bond lands on the wrong side of the trade:
take moves the bond out of the pool and into the hook, which leaves
the hook owing the pool exactly that much. The returned delta credits exactly that much back.
The hook's position inside the lock nets to zero, which is the only thing v4 checks.
If the computed bond rounds to zero — a trade small enough that
mag × BOND_BPS / 10 000 == 0 — the hook returns a zero delta and opens
no trade. There is nothing to settle and no receipt to claim.
Swaps against any pool key that is not the bound one pass through untouched.
isBoundPool compares both currencies, the fee, the tick spacing and
the hook address; anything else gets a zero delta and no record.
§4 Settlement arithmetic
function ratio(int24 pre, int24 post, int24 now_) internal pure returns (uint256) {
int256 impact = int256(post) - int256(pre);
if (impact == 0) return ONE; // moved nothing, so nothing stuck
int256 back = int256(post) - int256(now_);
if ((impact > 0 && back <= 0) || (impact < 0 && back >= 0)) return 0;
uint256 a = uint256(back > 0 ? back : -back);
uint256 b = uint256(impact > 0 ? impact : -impact);
if (a >= b) return ONE;
return (a * ONE) / b;
}
function split(uint256 bond, uint256 r) internal pure returns (uint256 refund, uint256 forfeit) {
refund = (bond * r) / ONE;
forfeit = bond - refund;
}
ONE is 1e18. Division truncates, so the
refund rounds down and the remainder goes to the providers — at most one wei per settlement,
and always in the direction that cannot leave the contract short.
The weight
The ratio says what the trade was. The weight says what the verdict is worth by the time
somebody collects it, and the two multiply.
function weightAt(uint64 openedAt) public view returns (uint256) {
uint256 full = uint256(openedAt) + WINDOW + GRACE;
if (block.number <= full) return 1e18;
uint256 dead = uint256(openedAt) + DEADLINE;
if (block.number >= dead) return 0;
return ((dead - block.number) * 1e18) / (dead - full);
}
Flat at 1e18 through WINDOW + GRACE, then a
straight line to zero at DEADLINE. Note that it does not care whether
the trade is settleable yet — a trade inside its window still has full weight, it simply cannot
be settled.
This ramp is doing two jobs at once. It retires abandoned bonds without a cliff, so nothing in
the contract turns on a single block. And it prices the one thing a trader can still game:
you choose when the price is read. Nothing on chain can read a tick from a block
that has already passed, so the reading has to be live, and the option to wait for a friendlier
one cannot be removed. It can only be made not worth having.
preview composes the two, and it is worth reading the order carefully —
the weight is applied to the refund, and the forfeit is whatever is left of the bond, so both
truncations land on the providers' side:
ready = block.number >= p.openedAt + WINDOW;
uint256 weight = weightAt(p.openedAt);
if (weight == 0) {
// Past the deadline the answer is fixed and the price is irrelevant.
return (0, 0, p.bond, ready);
}
(, int24 now_,,) = POOL_MANAGER.getSlot0(_key().toId());
ratio = Reversion.ratio(p.pre, p.post, now_);
(refund,) = Reversion.split(p.bond, ratio);
refund = (refund * weight) / 1e18;
toProviders = p.bond - refund;
What the ramp costs, at the intended parameters
Settled
Blocks
Weight
Effect
Before the window
< 300
1.000
settle reverts TooEarly. preview still answers.
Window to end of grace
300 – 600
1.000
The verdict is worth all of itself. This is where you are meant to collect.
About 3 days
21 600
0.903
You keep roughly 90% of whatever the verdict said.
About a week
50 400
0.769
Roughly 77%.
About 30 days
216 000
0.000
Nothing left. The price is not read at all; the whole bond is the providers'.
Rows computed from weightAt at WINDOW = 300,
GRACE = 300, DEADLINE = 216 000. Block counts
converted at 12 seconds a block, which is a convention, not a guarantee.
Reads the pool's current tick, computes the ratio, splits the bond, donates the forfeit,
pays the claimant, writes the Docket record, emits Settled.
Callable by anyone; the refund always goes to claimantOf(id) and
never to the caller.
What the trade pays out if settled at this block. Callable before the window elapses —
ready tells you whether settle would
currently succeed. Once the weight has reached zero it returns
(0, 0, 0, bond, ready) without reading the price at all.
ratio is the reversion on its own and
weight is the decay on its own; the refund is the product of the
two and the bond. They are returned separately so a trader can see which of the two is
costing them — but do not compute bond × ratio and expect
refund.
Reverts NoSuchTrade
weightAt(uint64 openedAt) → uint256
The decay multiplier at this block, in 1e18 fixed point.
1e18 through openedAt + WINDOW + GRACE,
then linear to 0 at openedAt + DEADLINE.
Takes the open block rather than an id, so it can be evaluated for a hypothetical trade.
claimantOf(uint256 id) → address
The Docket holder of the receipt, if one has been minted; otherwise the original
trader recorded at swap time.
Every trade of who's in the id range that can be settled right
now. Ids start at one, and a from of zero is treated as one.
to is clamped to nextId. For wallets and
scripts only — it is linear in the range and should never be called on-chain.
sweepStranded()
Retries a donation that had nobody in range to receive it. Anyone may call it, and it
can only ever move value towards the providers. Emits Swept on
success; silently re-parks the value on failure.
All-time totals. held = bonded − refunded − toProviders is every
bond posted and not yet judged.
toProviders counts everything forfeited,
including value that has not actually reached anybody because nobody was in range
when it was donated. parked is that residue, in currency0 and
currency1 respectively, and sweepStranded retries it. Subtract
parked from toProviders if you want the
amount that has genuinely been delivered.
poolKey() → PoolKey · isBoundPool(PoolKey) → bool
The exact key the hook will donate into, and the test it applies to decide whether a
swap is one of its own.
pending(uint256 id) → (bond, pre, post, onCurrency1, settled, trader, openedAt)
The raw record. Public mapping, no accessor needed.
Internal plumbing with external visibility — the first so a failed donation can be
caught, the second because the PoolManager requires it. Both reject every caller but the one
that is allowed (NotSelf, NotPoolManager).
Opened and Settled are jointly enough to
reconstruct the whole history off-chain: who traded, what they moved, what came back, and what
it cost them. No indexer-only state.
Basis points of the unspecified side held pending a verdict. The cap is the constructor's, and its stated reason is that above about 5% the held capital costs an honest trader more than the mechanism can plausibly hand back.
WINDOW
uint32
x > 0
Blocks that must pass before a trade can be settled. Intended value 300, about an hour.
GRACE
uint32
—
Blocks after WINDOW during which a verdict is worth its full value. Intended value 300, about an hour. May be zero, in which case the decay begins the moment the trade becomes settleable.
DEADLINE
uint32
x > WINDOW + GRACE
Blocks after which the weight is zero and an unsettled bond belongs to the providers, price irrelevant. Intended value 216 000, about thirty days.
TICK_SPACING
int24
x > 0
Bound, unusually for a hook, because donate needs the whole pool key and a guessed spacing would donate into a pool that does not exist. Must equal what the Uniswap interface derives from POOL_FEE.
POOL_FEE
uint24
—
The ordinary pool fee, unchanged and untouched by the hook. Static, not dynamic.
CURRENCY0 / CURRENCY1
Currency
sorted
Derived in the constructor from the token address against address(0): native ETH is currency0, the token is currency1.
POOL_MANAGER · DOCKET
address
—
Set once. No setter exists for either.
FLAGS
uint160
== 0x1044
Checked against the contract's own address in the constructor. A deployment to the wrong address does not exist.
There is no owner, no onlyOwner, no proxy, no initializer, no
timelock and no pause. Every one of the above is immutable, which
means it lives in the deployed bytecode and not in storage. Changing one requires deploying a
different contract at a different address, which is a different pool that nobody's liquidity is
in.
§7 Integration notes
If you are providing liquidity
Mint a normal v4 position through any interface that supports v4, including the Uniswap
one. There is no allow-list, no lockup, no vesting, and no callback on add or remove.
Forfeits arrive as donate(), which credits in-range liquidity at
that instant. Out-of-range liquidity earns nothing from a forfeit, exactly as it earns nothing
from a fee.
You are not obliged to settle anybody's trade, and settling does not pay you — the refund
goes to the claimant regardless of who calls. Your one reason to settle somebody else's trade is
to fix the verdict at a block of your choosing rather than theirs, and the first call wins. The
decay ramp is what makes that a convenience rather than a necessity: a trader who waits for a
friendlier price pays for the wait out of the same bond.
Forfeits arrive whether or not anybody settles promptly. A bond left to run all the way to
DEADLINE is donated in full.
The hook cannot stop you leaving. That is the entire content of bits 9 and 8 being clear.
If you are trading
Swap from an EOA. The receipt is made out to tx.origin, which
through a 4337 bundle or a keeper-fired Safe module is not you.
You do not need extra slippage tolerance. The live Uniswap V4Quoter
(0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203) simulates the hook as part
of quoting, so the number the interface shows you already has the bond deducted. Verified on a
mainnet fork by quoting a swap and then executing it with amountOutMinimum
set to the quote exactly — zero tolerance — and it succeeds.
Budget about 59 000 extra gas for the swap: 109 938 against 51 220 for the identical pool with
no hook, measured in steady state on a mainnet fork. Settling later is a separate transaction and
a separate cost.
Note the id from the Opened event, or find your open trades later
with settleable. Then come back after
WINDOW and call settle — and do it inside
GRACE, because after that every block costs you part of the refund.
There is no hookData. Nothing about the swap call is non-standard, which is why an
off-the-shelf router can make it.
Risks and limitations
The parts that are genuinely bad for you
Your capital is held for WINDOW blocks.
Real cost. Unavoidable. A verdict that arrived instantly would be a guess again, which is the thing this design exists to stop doing.
You must come back and settle, inside the grace period.
Anyone can settle for you, but nobody is paid to. Assume you will pay your own settlement gas, and that on a small bond that gas may exceed the refund. Past WINDOW + GRACE the refund decays every block, and at DEADLINE there is none of it left. It is still your money and you will still have lost it.
The swap costs about 59 000 more gas than a hookless pool.
109 938 against 51 220 in steady state, measured on a mainnet fork. The first swap into a cold pool is worse — about 127 000 over. Settlement is a second transaction on top. On a small trade these costs can be a larger number than the bond being argued over.
The window is a parameter, not a truth.
Too short and slow information looks like noise. Too long and ordinary drift looks like information. There is no value of WINDOW that is correct for every trade, the value chosen will be wrong for some of them, and it is immutable.
The settlement instant is chosen by whoever settles, and that option is real.
The verdict is read at whatever block settle runs. Nothing on chain can read a tick from a block that has already passed, so the reading has to be live and this option cannot be designed away. Over a long enough deadline, ordinary drift will eventually carry the price back past where a trade left it, and a genuinely informed trade that waits long enough can collect a full ratio. The ramp prices that wait rather than preventing it. Inside GRACE the option still exists at no cost at all — a trader who sees a good reading an hour in will take it. What is bought is that the expensive version of the strategy is not worth running.
The receipt is issued to tx.origin.
Through a normal wallet that is you. Through a smart-contract account — an ERC-4337 bundle, a Safe module fired by a keeper — it is not you, and the bond is not yours to reclaim. There is no recovery path for this. Swap from an EOA.
A forfeit with nobody in range is stranded, not refunded.
donate reverts when no liquidity is in range. The hook catches that and parks the value; anyone can call sweepStranded() to retry. It is never returned to the trader.
Ticks are a coarse instrument.
The verdict is computed on tick differences, so a move too small to cross a tick boundary registers as zero impact and is refunded in full. That is deliberate, and it is also a rounding rule that somebody will eventually try to sit exactly underneath.
The Docket has one privileged address, used once.
The hook has no owner and no admin function of any kind. The Docket is not quite the same: its deployer is recorded as BINDER and may call bind exactly once, to point it at the hook. This exists because the hook's mined address depends on the Docket's address, so the Docket has to exist first and there is no ordering in which both can be immutable. After that one call bind reverts for ever and nothing else on either contract is privileged. Verify it has been bound to the right address before trusting a receipt.
None of this has been deployed or audited.
There is no mainnet contract, no testnet contract, no audit, and no third-party review to cite. Anything on this page describes source code, not a running system.
Questions
The hostile ones first, since they are the only interesting ones.
Isn't this just a delayed fee?
For an arbitrageur, yes — it is a fee they pay late. For a retail trader it is a fee they
do not pay at all, and that is the whole difference. The point was never the delay. The
point is that the amount is settled by evidence instead of a guess.
A conventional pool sets one number in advance and charges it to everybody, which means
the number is either too low to cover the informed flow or too high for the uninformed
flow, and in practice it is both at once. This pool charges the ordinary fee to everybody
and then decides, after the fact, who owes the surcharge. The gross take is comparable.
The distribution is not.
What stops someone settling at a cherry-picked block?
Nobody stops them. The option is real, it cannot be removed, and it is priced instead.
That is worth setting out exactly rather than waving at.
The verdict is read at whatever block settle actually runs,
because nothing on chain can read a tick from a block that has already passed. There is no
historical price to consult and no way to bind the reading to a fixed instant — the pool's
tick is a live value or it is nothing. So the trader chooses when the question is asked.
Over a long enough deadline that choice is worth something: ordinary drift will eventually
carry the price back past where they left it, at which point a trade that really was
informed collects a ratio of 1.
So the wait is charged for. A verdict collected within GRACE
blocks of becoming available is worth all of itself; after that its weight decays in a
straight line to zero at DEADLINE, and
refund = bond × ratio × weight. Waiting a week for drift to
manufacture a better ratio costs about 23% of the bond in weight. Waiting a month costs
all of it. The better reading arrives attached to a smaller multiplier, and past a very
short horizon that trade stops making money.
What this does not do is eliminate the option. Inside the grace window it
is free: a trader watching their own position who sees a favourable tick forty minutes
after becoming settleable will take it, and the mechanism has no answer to that beyond
WINDOW being long enough that the price has had time to say
something. Settlement is also open to everyone and the first call wins, so a provider who
wants the verdict fixed early can call settle themselves. Expect
both sides to run bots.
This is a designed-for property with a stated cost, not a hidden one. But it is a
materially different thing from a contract that samples at a single predetermined block,
and if that is what you need, this is not the mechanism.
Why not just raise the fee?
Because the fee is charged to the wrong people. Raising it taxes the flow that was
profitable to fill — the retail buyer whose price snapped back — and drives it to a
cheaper venue, which is the flow the pool was earning from.
And it still does not cover the informed flow. To make an arbitrage unprofitable the fee
has to be wider than the price discrepancy the arbitrageur is closing, and at that point
the pool simply stops being arbitraged, which does not save it: the quoted price goes
stale instead, and the provider now holds a position marked at a number nobody else
agrees with. There is no fee level that solves this, which is the reason to stop trying
to find one.
Can the pool steal my bond?
There is no owner, no admin function, no proxy and no upgrade path. Every parameter is
immutable and lives in bytecode. The bond sits in the hook and
can leave it in exactly two directions: to
claimantOf(id), or by donate() into
the pool's in-range liquidity. There is no third branch and no address in the contract
that can be set after deployment.
What can happen without anybody stealing anything: you do not settle before
DEADLINE and the bond goes to the providers. That is a rule you
can read in the source before you trade, not a discretionary act by anyone.
What if I never settle?
It drains away rather than expiring. Through WINDOW + GRACE the
verdict is worth all of itself. After that its weight falls in a straight line, so at
roughly three days you collect about 90% of whatever the verdict said, at a week about
77%, and at thirty days nothing at all — at which point
preview returns (0, 0, bond) without
even reading the price.
Anyone can settle on your behalf at any point in that range and the money still comes to
you: a provider who wants the verdict locked in, a bot someone runs as a service, a
friend. Nobody is paid to, so do not count on it.
The ramp exists because the alternative is a contract slowly accumulating unclaimed
capital for ever. A cliff would do that job too, but a cliff makes a single block worth
fighting over, and this way nothing in the contract turns on one.
Does this work on the Uniswap app?
Yes, and the flags were chosen so that it would. There is no
beforeInitialize gate, so the interface can create the pool.
There are no liquidity gates, so it can mint and burn a normal position. There is no
beforeSwap and no dynamic fee, so the quoting path is the
ordinary one. Nothing takes hookData, so an off-the-shelf
router can make the call.
The slippage question is the one that would have broken it, and it is settled. The live
Uniswap V4Quoter at
0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203 simulates the hook as
part of producing a quote, so the number the interface shows you already has the bond
deducted. Verified on a mainnet fork: quote a swap, then execute it with
amountOutMinimum set to that quote exactly — a slippage
tolerance of zero — and it succeeds. App swaps do not fail their own slippage check, and
you do not need to widen anything.
What the interface will not show you is the bond coming back. That part is a second
transaction, to this contract, that you have to make yourself.
What does the hook cost in gas?
About 59 000 gas on top of the swap. Measured on a mainnet fork against an identical pool
with no hook: 109 938 against 51 220 in steady state. The first swap into a cold pool is
worse, roughly 127 000 over, because the storage slots are being written for the first
time.
That is the price of two storage slots and a transfer, and there is no version of this
mechanism that does not pay it — the pool has to write down what your trade did in order to
be able to judge it later. Settling is a second transaction with its own cost. On a small
enough trade, the gas is a larger number than the bond being argued over, and for those
trades this pool is simply a worse deal than an ordinary one.
What if the price moves for reasons that have nothing to do with me?
Then the verdict is wrong about you, and this is the honest weakness of the design. A
retail buyer who happens to trade thirty seconds before a market-wide move will look
informed and pay the bond. Someone genuinely informed whose information is priced in by
somebody else first will look like noise and get a refund.
The claim is not that every individual verdict is correct. It is that the measurement is
unbiased and not gameable by presentation — you cannot make your trade look uninformed,
because the thing being measured happens after you are gone. Over many trades the
classification is right far more often than a volatility threshold or a size bucket,
which are wrong in ways a bot can arrange on purpose.
What stops me settling my own trade with a swap that pushes the price back?
Arithmetic, mostly. Moving the tick back to where it started costs you the pool fee, the
price impact of the reversing trade, and a fresh bond on that trade — which is then
itself judged, and judged badly, because you just moved the price and are about to leave
it there. To recover a bond of BOND_BPS you would spend rather
more than BOND_BPS doing it.
The ratio is also clamped at 1, so overshooting buys you nothing, and the forfeit you
were trying to avoid goes to the liquidity providers — the same people collecting the
fee on the trade you would have to make to avoid it.
Why tx.origin? Everyone says not to use it.
The usual warning is about authorisation — tx.origin
must never be used to decide whether a caller is allowed to do something, because a
contract you call can relay the check. That is not what it is doing here. It is used to
address a refund, and the failure mode of getting it wrong is that somebody gets paid who
should not have, not that somebody gets robbed.
The alternative is worse. Through the Uniswap interface,
msg.sender in afterSwap is the
Universal Router — the same address for every trader alive. A receipt made out to the
router is a receipt nobody can claim. The cost of the choice is stated plainly on this
site and in the source: swap from an EOA, or the bond is not yours.
Is there a token, and does it do anything?
HINDSIGHT, one billion, eighteen decimals, minted once in the
constructor. ERC-20 with EIP-2612 permit and a burn function. No mint, no owner, no tax,
no pause, no blacklist, no transfer hook, no proxy.
It is deliberately boring, and the reason is structural: the hook only has permission to
be interesting because the token has none. A pool can hold your bond for a hundred blocks
precisely because the token cannot stop you leaving once you have it back. The verdict is
deferred; the exit never is.
There is no sale, no allocation table, no vesting schedule and no date on this site,
because none of those things exist yet.
Where is it deployed?
Nowhere. There is no mainnet address, no testnet address and no audit. The contracts are
written; that is a different thing from being live, and this site will not blur the two.
Any address circulating as a Hindsight contract today is not one.