Glasshouse  /  1inch SwapVM  /  ETHOnline 2026

Every auction on chain is won by the fastest bidder, not the highest one.

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 →
by identity WhitelistSequential · 0x2d

A ladder of privileged takers, written into the order by hand. Everyone else reverts.

by clock DutchAuctionBalanceIn · 0x94

A descending price. Everyone in a block sees the same number, so the tie breaks on transaction order.

by bid GlasshouseAuction · 0x2e

A sealed second-price auction. The highest bidder wins and pays what the runner-up offered.

The order is already spoken for

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.

The other one is worse, and it is not obvious

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.

The same order, three ways

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.

Source · Foundry test · test/Comparison.t.sol
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
What produced this: 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.

What replaces them

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.

Three phases, disjoint in block space

Source · Config · config/auction.json “advocated”
commit Sealed bids arrive. The bond is posted here, so staying silent later is not free. n ≤ commitEnd · 30 blocks
reveal Bids open. The running top-two settles. Nothing may fill yet, for anyone. ≤ revealEnd · 30 blocks
exclusive The winner fills at the improved price. 15 blocks
open Exclusivity expires. Anyone fills, at the base price. Permissionless again. unbounded
What produced this: the block counts are the 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).

Why the split has to exist

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.

“Isn't that just a Dutch auction?”

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 parameters are derived, not chosen

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.

Source · Config · config/auction.json “advocated”
ParameterValueWhere the number came from
commitBlocks3060 s. Half the ladder's lockup, and long enough for a person to sign.
revealBlocks3060 s. Generous on purpose: a missed reveal forfeits the bond.
exclusiveBlocks1530 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.
reserveBps50The mid moves ~44 bps across the lockup at 200% annual volatility (simulation). Below that, running the auction is worse than posting a limit order.
maxBps500The cap the maker signs. Ten times the reserve, leaving room for real competition.
What produced this: the 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.

What it costs

Everything above has a price, and the page would be less useful if it left them out.

Honest limits

  • It is slower than what it replaces. A latency race resolves in one block. This takes 150 seconds, during which the maker's order is frozen and their price can go stale. That is the trade, stated plainly.
  • The improvement delivered is slightly under the bid. The winner pays the runner-up's 250 bps nominally, but the improvement is applied as a divisor on 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.
  • The exclusive window is still a free option. At 30 seconds it is worth about 4.7 bps at normal volatility and 15.6 on a volatile pair (simulation, docs/design/window-sizing.md) — roughly 2% of the maker's improvement, spent on optionality rather than allocation.
  • Forfeiture requires evidence. The book cannot verify that the maker wired the fill hook correctly, so it only penalises a winner when someone else demonstrably filled. A no-show that nobody else fills behind keeps its bond. The alternative was a mechanism the maker could use to take bonds at will.
  • Bidder rings are bounded, not solved. Posting the bond at commit makes withholding a reveal cost something. It does not make collusion impossible.