Glasshouse / 1inch SwapVM / ETHOnline 2026
SwapVM already ships two ways to decide who may fill an order. One asks who you are. The other asks what time it is. Neither asks what the fill is worth to you.
Watch it run →A ladder of privileged takers, written into the order by hand. Everyone else reverts.
A descending price. Everyone in a block sees the same number, so the tie breaks on transaction order.
A sealed second-price auction. The highest bidder wins and pays what the runner-up offered.
WhitelistSequential lets a maker name the takers who may fill, each with an exclusive window. It reads like a preference. It isn't. An unlisted taker doesn't get a worse price, it hits a require and reverts, and keeps reverting until the entire cumulative ladder has elapsed.
Who fills was decided somewhere off chain, for reasons the chain cannot see. And the maker is paid nothing for granting the privilege: the insider fills at the ordinary price.
DutchAuctionBalanceIn looks like a real auction. Here is the whole of it:
// swap-vm/src/instructions/DutchAuction.sol
uint256 elapsed = block.timestamp - start;
ctx.swap.balanceIn =
ctx.swap.balanceIn * uint256(decay).pow(elapsed, ONE) / ONE;
The price is a pure function of block.timestamp. Every transaction in a block shares one timestamp, so every bidder in that block faces an identical number. Valuation cannot break the tie. There is nothing left to decide the winner except position in the block, and position in the block is sold to whoever pays the builder most.
So the surplus does leave the order. It just doesn't reach the maker: it is competed away into priority fees. The descending clock doesn't price the order. It runs a latency race and pays the proceeds to someone who wasn't party to the trade.
One order. Three participants with the same valuations each time. Identical balances and swap curve in all three programs, so the only variable is how taker priority is decided. The participant who values the fill least is also the fastest, and is the incumbent on the ladder, because that is the situation the mechanism is supposed to handle.
| Gate | Winner values | Maker gives up | Against base |
|---|---|---|---|
| By identity 0x2d | 100 bps | 10 000 | Level with no auction at all |
| By clock 0x94 | 100 bps | 10 618 | 6.2% worse for the maker |
| By bid 0x2e | 400 bps | 9 756 | 2.4% better, and the right bidder won |
test_Comparison_AllThreeGatesOnTheSameOrder in
test/Comparison.t.sol, run with npx hardhat test solidity. Two mock tokens
(TokenMock “Token I”/“Token J”), static balances 1000 / 2000, swap 1.
Three test addresses with assigned valuations — Ada 400 bps, Bram 250, Cyd 100 —
where Cyd is also assigned the first position in the block and the first rung on the ladder. The clock row is
read once, 60 s after start, with decay 0.999 per second over a 600 s window (the slowest factor in
upstream’s own tests, i.e. the one kindest to the clock). Auction parameters in the test are
5 / 5 / 10 blocks, reserve 10, max 500, not the deployed
30 / 30 / 15. Nothing here was observed on a network.
Basis points of the base price; lower is better for the maker. Both existing gates hand the fill to the participant who values it least. The clock additionally concedes to the taker every second it runs.
One instruction, at 0x2e — the free slot immediately after the ladder it replaces. Bidders commit to sealed bids, reveal them, and the highest bidder wins the right to fill while paying what the runner-up offered, not their own bid.
The winning bid is expressed as a price improvement in basis points applied to balanceIn. Ordinary SwapVM settlement then delivers it to the maker. No escrow, no payout path, no custody, nothing new to trust.
Bids are sealed for a reason that survives questioning: not anti-sniping, but shill resistance. In an open second-price auction the maker can watch the top bid and insert one just underneath it.
advocated set in
config/auction.json; the phase boundaries are how GlasshouseBook itself decides,
comparing block.number against stored ends (:159, :185-186,
:219). The contract stores no phase variable, so this diagram is the rule, not a reading of
state. Seconds assume Base’s two-second blocks.
Total lockup 150 seconds on Base (75 blocks × 2 s, config), against the 300 seconds a single-entry WhitelistSequential ladder locks outsiders out for (a duration the maker picks; 300 is the value our comparison test uses).
SwapVM runs the same program bytes in quote() and in swap(), in different static contexts. An instruction that writes state makes the two diverge, and every downstream guarantee goes with them.
So the auction writes state in its own transactions, and the instruction only ever reads the result, through a view call to the book. The instruction itself is internal view, so it cannot emit an event: the compiler rejects a LOG there, which is a stronger guarantee than a runtime one. Fills are therefore recorded through a maker hook, which lives outside the program and cannot affect either path.
The book that holds the auction has no owner, no admin and no upgrade path. Parameters are immutable once an auction opens. The answer to "who can change what the result says?" is nobody, which is what makes the consistency argument hold rather than merely sound good.
A Dutch auction is strategically equivalent to a first-price sealed bid — in a frictionless model. On chain the equivalence breaks at two points, and both are visible in the source above.
block.timestamp is quantised to the block, so the clock cannot separate two bidders who arrive in the same one. And ordering within that block is sold to the highest priority fee. So the descending clock systematically awards the fill to the lowest-latency participant, whatever they think it is worth.
The fix is not a better clock. It is to stop using one.
The exclusive window is the interesting one, because it is not execution time. It is a free American call granted to the winner: the right, not the obligation, to fill at the improved price. A longer window raises bids — and lets the winner exercise only when the market has moved against the maker.
Monte Carlo over both exercise rules settles it. On a volatile pair, stretching the window from 2 seconds to 300 raises the clearing price from 79 to 124 basis points, which looks 57% better, while the maker's net falls from 75.7 to 67.2 (simulation, docs/design/window-sizing.md). The option premium does not cover the adverse selection it creates.
| Parameter | Value | Where the number came from |
|---|---|---|
| commitBlocks | 30 | 60 s. Half the ladder's lockup, and long enough for a person to sign. |
| revealBlocks | 30 | 60 s. Generous on purpose: a missed reveal forfeits the bond. |
| exclusiveBlocks | 15 | 30 s. The simulation rules out the long end and is indifferent across the short end, so this is what a human winner needs to sign. |
| reserveBps | 50 | The mid moves ~44 bps across the lockup at 200% annual volatility (simulation). Below that, running the auction is worse than posting a limit order. |
| maxBps | 500 | The cap the maker signs. Ten times the reserve, leaving room for real competition. |
advocated set in config/auction.json,
which is what the live auction on Base is opened with. The exclusive window comes from the Monte Carlo in
docs/design/window-sizing.md and the reserve from the staleness calculation in the same file;
the rest are the maker’s choices, argued for in the third column. These are parameters, not
measurements — a different maker would pick differently.
Everything above has a price, and the page would be less useful if it left them out.
balanceIn rather than subtracted, so the price actually moves 244 bps. On the constant-product curve the deployed router uses, it is 243 (both measured, test/fork/AquaBaseFork.t.sol). The direction is exact; the magnitude is not.docs/design/window-sizing.md) — roughly 2% of the maker's improvement, spent on optionality rather than allocation.