VickreySepolia (rehearsal)

Documentation

How it works

A Vickrey auction has been the theoretically right way to sell one thing since 1961, and has never worked on a public chain. This is what it takes to make one work, written so you can check the claims rather than take them.

Contents

What this is

In a sealed-bid auction everyone submits one bid without seeing the others. In a Vickrey auction — second-price — the highest bidder wins but pays the second-highest bid.

That second part sounds like a giveaway and is the opposite. If you pay your own bid, you shade it down to leave room for profit, and you are guessing about other people rather than about the thing being sold. If you pay the runner-up’s bid, bidding your honest valuation is your best move no matter what anyone else does. The auction stops being a game about opponents and starts being a question about value.

Getting there needs the bids sealed. On a public chain they never really were. Either you hand your amount to an auctioneer and trust them not to look, not to leak, and not to insert a bid of their own once they have seen yours — or you use commit–reveal, where every bid is published at the end.

Publishing them destroys the thing the auction was for. And commit–reveal has a second problem that is less discussed: it locks no money, so a bidder who dislikes the outcome simply never reveals. In a second-price auction one silent bidder changes what the winner pays.

Vickrey never opens a bid. Collateral is escrowed up front, so silence costs money, and the winner and the price are proved with hash chains instead of disclosure. The losing bids are not withheld — they are never on the chain at all.

The difference from commit–reveal, concretely

Almost every sealed-bid auction on a public chain is commit–reveal: you post a hash of your bid, and after bidding closes you post the bid itself so the contract can check it against the hash. It is a sound and well-understood construction, and it is not what this is.

The distinction is what the chain holds when the auction is over.

Commit–reveal, at the end

  • Every bid is public, winners and losers alike
  • Your valuation is readable by anyone, permanently, and it is still true at the next auction
  • A bidder who dislikes the outcome can withhold their reveal, and in a second-price auction that moves what the winner pays

Here, at the end

  • One number is published: the clearing price, because it is the price
  • Every other bid is still two hashes — including the winner’s own
  • Silence forfeits escrowed collateral and settlement proceeds regardless

There is a corollary worth being explicit about. In commit–reveal the question “can the auctioneer exclude a rival’s bid?” largely dissolves, because by settlement every bid is public and anyone can compute the result. That is a real answer — but it is bought by publishing the bids, which is the thing being avoided here. Keeping them sealed means the exclusion problem has to be solved rather than dissolved, which is what sealing before reveal and the auctioneer’s slashable bond are for.

Six properties, and why each is hard

Each of these is a place a straightforward implementation breaks. They are listed because they are checkable, not because they are features.

  1. 1 Bids are real escrowed funds, not promises

    What normally goes wrong: Commit-reveal locks nothing. A bidder can commit to a price they cannot pay, and you only find out at the end.

    Here: Placing a bid transfers collateral into the contract in the same transaction that records it. A bid that is not funded does not exist.

  2. 2 Sealed from everyone, including the auctioneer

    What normally goes wrong: Designs with a trusted auctioneer leak every bid to whoever runs the server, from the moment it arrives.

    Here: During bidding the chain holds two hashes per bid and nothing else. Nobody — not the auctioneer, not us — can read an amount, because no amount was sent.

  3. 3 The bid set is frozen before any amount can be read

    What normally goes wrong: If the party producing the result picks the set after seeing the contents, they can drop a rival's high bid and claim it never arrived.

    Here: `seal()` stamps the block number and freezes the set on-chain. Only afterwards do bidders send their seeds. Excluding a bid that arrived is provable, and slashes the auctioneer's bond.

  4. 4 Losing bids are never published

    What normally goes wrong: Every commit-reveal auction ends by publishing all of them. Your valuation is a business fact, and it is still true at the next auction.

    Here: Settlement proves the outcome from bounds. The clearing price is revealed because it is the price; every other bid stays a pair of hashes forever.

  5. 5 The outcome is proved, not asserted

    What normally goes wrong: Most implementations ask you to trust that the settlement transaction did the arithmetic honestly.

    Here: The contract verifies N+1 hash-preimage witnesses: the winner at or above the clearing level, the runner-up exactly at it, everyone else at or below. A false outcome cannot produce them.

  6. 6 Refusing to reveal cannot grief the auction

    What normally goes wrong: In commit-reveal, a bidder who dislikes the result simply never reveals — and in a second-price auction one silent bidder moves the price the winner pays.

    Here: Settlement needs no cooperation from a bidder who stays silent: their collateral is forfeit and the auction completes without them.

Properties 3 and 4 are marked because they are the two that are genuinely hard to get elsewhere. 3 is an attack most designs never consider — the auctioneer choosing the set after seeing the contents. 4 is the one a bidder feels: the auction ends and their number was never anywhere but their own browser.

The thermometer commitment

This is the one piece of cryptography you have to follow, and it is a hash function used twice.

Bids are not free-form amounts. They are levels on a public ladder: level 0 is the reserve, and each step up adds a fixed tick. Bidding at all means bidding at least the reserve, so the reserve needs no separate rule.

A hash chain is a value hashed repeatedly. Given a link, anyone can walk forward by hashing again; walking backward would mean inverting the hash, which is the thing hash functions are for. So handing someone a link from a known depth proves you knew a value that far along — and proves nothing else.

Each bidder publishes two anchors, one from each end of the ladder:

step(x) = poseidon([CHAIN_TAG, auction_id, claim_commitment, x])

up_anchor   = step^(ℓ)        a depth-t preimage proves   ℓ ≥ t
down_anchor = step^(P−1−ℓ)    a depth-(P−1−t) preimage proves  ℓ ≤ t

Each witness reveals one bound, never the level. “At least 4” is compatible with 4, 5, 6 or 7. But the two together pin a level exactly, and that is what settlement needs: the winner proves at or above the clearing level, the runner-up proves exactly at it, and everyone else proves at or below.

Which gives the whole result. The second-highest bid is established as the price, by the person who made it, without that bid ever being stated — and every other bidder has said only “mine was not higher than that”. Producing a witness for a bound you did not commit to is a Poseidon preimage break.

Why it is N+1 witnesses, and why that is enough

For N bids the auctioneer submits N+1 witnesses: one per bid, plus a second for the runner-up. The runner-up needs two because they are the only party whose level must be pinned exactly — one witness proves they are at least at the clearing level, the other that they are at most at it, and together those say equals.

That set is sufficient to establish a Vickrey outcome, and the contract checks it rather than trusting it. If the claimed price were too low, the runner-up’s “exactly” proof would not verify. If it were too high, the winner’s “at or above” proof would not. And if a losing bid were really above the price, its “at or below” proof could not be produced at all.

Each witness is a hash chain walk, so the whole settlement is linear in the number of bids — no sorting network, no pairwise comparisons, and nothing that grows quadratically as the auction fills up.

Settlement is O(N): N+1 witnesses for N bids, each a few hashes. Measured cost for three bids is in the README.

Escrow, silence, and exclusion

Three mechanisms that are not cryptography. Each closes an attack the hash chains do not touch.

Everyone escrows the same amount, and it is the top of the ladder

A bidder posts collateral equal to the cap — the highest level on the ladder — regardless of what they actually bid. Bidding level 2 out of 8 and bidding level 7 lock identical amounts.

This is not caution, it is the whole point. Escrow moves as an ordinary ERC-20 transfer, and a transfer is public. If the amount tracked the bid, the transfer would publish the bid — and everything else here would be theatre.

A uniform amount says nothing beyond “someone bid”, which the chain already shows. The cost is capital efficiency: a low bidder locks more than they intend to spend. The difference comes back at settlement — and on the private rail it comes back as a note inside the pool, so even the refund does not reveal how much was unspent.

Staying silent cannot grief the auction

In commit–reveal, a bidder who dislikes the result simply never reveals. In a second-price auction that is not a small problem: the runner-up going quiet changes what the winner pays. So non-reveal is an attack, and it is free.

Here it costs the collateral. A bidder who never sends their seed is marked forfeited — the auctioneer proves it with the loser-side witness — and settlement proceeds without them. The auction does not wait, does not stall, and does not need their cooperation. Their escrow is not returned.

A bidder who forfeited but was genuinely above the clearing price can still redeem later by presenting the proof they withheld — so the penalty falls on obstruction, not on a lost connection.

The auctioneer cannot drop a rival’s bid

The auctioneer learns every level after sealing. The obvious attack is to pretend a high bid never arrived, settle lower, and win the lot cheaply — or hand it to a friend.

Two things stop it. Ordering: seal() freezes the set and stamps the block before any seed is sent, so the set cannot be chosen after seeing the contents. Consequence: during the dispute window, anyone holding a witness that their bid was above the claimed clearing price can present it. The contract verifies it, cancels the auction, and slashes the auctioneer’s bond to the disputer.

So excluding a bid is not merely detectable — it is detectable by exactly the person with the motive to detect it, and it pays them to do so.

How it uses STRK20

Sealing the amount is the auction’s job. STRK20 does the other half: unlinking the bidder from the bid.

Bidding on the public rail is the ordinary path — connect, pick a level, sign. Your bid is sealed; your address is visible. The private rail funds the same bid from a shielded balance inside the STRK20 pool, so neither is visible.

Our AuctionAnonymizer makes that atomic. The pool withdraws collateral to the helper, the helper forwards it into the auction and returns an empty span — the protocol’s way of saying “credit nothing”, because the funds are parked, not returned. A revert anywhere aborts the whole pool transaction and no funds move. No bidder address ever crosses that boundary; the auction sees only the helper.

Every way value comes back — a loser’s refund, the winner’s surplus, a forfeited escrow redeemed late, the lot — returns as an open note credited inside the pool. There is no public leg on the way out, so winning does not put an address on chain next to a price.

Only one rail touches the pool

This distinction matters more than it first looks, because the two rails are not two grades of the same thing.

A public-rail bid is a direct call to the auction contract. The collateral moves from the bidder’s own address, and the STRK20 pool is not involved at any point. The bid is still sealed — the amount was never in the calldata — but nothing private happened. It is an ordinary transaction that happens to carry two hashes.

A private-rail bid is a pool transaction. The pool withdraws to the anonymizer, the anonymizer calls the auction, and the whole thing succeeds or reverts together. That is the only path where funds leave a shielded balance and the only one that produces a Routed event.

So a public-rail bid can never stand in for a private one when what is being demonstrated is the pool integration — however many of them there are. If you are checking whether this project really runs against STRK20, the transactions to look at are the ones carrying Routed from the anonymizer and BidPlaced from the auction, in the same transaction.

What Routed does and does not leak

The anonymizer emits one event per operation. It exists because a transaction that touches the pool otherwise looks identical whether it came through our contracts or somebody else’s.

It carries

  • auction_id — already public
  • the operation kind — bid, refund, forfeit, lot

It deliberately does not carry

  • note_id — a pool-side handle. Publishing it would let an observer tie a private note to an auction action, which is the exact link the helper exists to break
  • the bid index on a placement — the auction emits that itself; two contracts publishing the same correlator is one too many
  • any amount — collateral is uniform so it would leak nothing today, but an event is not something a later change can take back

The test asserts the exact event, so adding a member stops the suite compiling rather than quietly widening what is published.

Lifecycle and time gates

Two of these are deadlines a participant can miss, and missing one costs money. They are shown throughout the app as a countdown and an absolute UTC time, because a countdown alone cannot be quoted in a dispute.

  1. Open — bids arrive as two hashes plus escrow. Anyone can bid. Ends at the bid deadline.
  2. Sealed — the set is frozen and stamped from the block. Bidders now send seeds to the auctioneer. A seed not sent means collateral forfeited.
  3. Settled — the outcome is proved on-chain from N+1 witnesses. The dispute window opens here — the only time a wrong outcome can be challenged.
  4. Finalized — the window closed clean and funds move. The winner claims the lot; losers claim refunds in full.
  5. Cancelled — a dispute succeeded, nothing was awarded, or the auctioneer never settled. Everything unwinds and every bidder is refunded.

That last route matters. A sealed auction otherwise has exactly one way out — settlement, which only the auctioneer can perform — so an auctioneer who walks away would lock every bidder’s collateral permanently. abandon() is a permissionless timeout: after the grace period anyone can cancel a sealed auction and everyone is made whole.

What didn’t ship

An entry that states its own gaps is worth more than one that hides them.

Sponsored private bidding
Designed and costed; the pool supports paying another party’s fee. No relayer is deployed, so the interface shows it and does not offer it.
Multi-unit auctions
The ladder generalises to uniform-price and pay-as-bid. Only single-lot first-price and Vickrey are implemented.
An audit
None of this has been audited. The anonymizer in particular is app-team code that handles funds mid-transaction.
The privacy SDK
Not on public npm, so the Wallet API is the only installable route. See the README for the check.

Source, tests, runbook

Contracts

Checks anyone can run

  • snforge test — 70 contract tests, negative ones first
  • npm test — 50 client tests
  • npm run verify:pool — our encoding against the live mainnet pool, with controls that prove the check can fail

Written down

What this guarantees

What is assured: the winner and the clearing price are established by hash-preimage proofs verified on-chain over a bid set the contract froze before any bid could be opened, so the auctioneer cannot alter the outcome, exclude a bid, or misreport the price without failing a proof or being slashed in the dispute window. What is not: after sealing, the auctioneer learns every bid amount — it can never publish them, prove a false outcome, or spend anyone's funds, but it knows them; and the number of bids, their timing, and the uniform escrow amount are public on-chain.