Documentation

▶ Watch the demo 2:46

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 publishes a losing bid. Collateral is escrowed up front, and a bid that is not revealed in time is settled around, not waited for. The winner and the price are proved with hash chains instead of disclosure. On chain, a losing bid is two hashes and a ciphertext that only the auction’s key opens.

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 and a ciphertext encrypted to the auction’s key — including the winner’s own
  • A bid whose reveal is not posted in time is marked forfeited and settlement proceeds without it. What the bidder gets back depends on where the bid sat

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, reveals posted on chain, and the auctioneer’s 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 No amount is readable while bidding is open. The auctioneer reads them only after the set is frozen

    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. After `seal()` freezes the set, each bid is posted on chain encrypted to a key the auctioneer made for this auction alone, and the auctioneer learns every exact bid. That ordering is the protection: by then the set cannot change, no bid can be added or dropped, and the clearing price is already fixed by bids nobody could edit. The encrypted bids stay on chain, so anyone who ever obtains that auction's key can read them; the auctioneer is prompted to delete it once the auction is final.

  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 are bids revealed, encrypted to the auctioneer. Leaving out a bid whose reveal was posted in time is provable: that bidder can void the settlement and takes the auctioneer's bond. Sealing is permissionless — the contract checks only that the bid deadline has passed — so an auctioneer cannot stall an auction by refusing to seal it, and any bidder can start the clock themselves.

  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 on chain only as two hashes and a ciphertext that the auction's key opens.

  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 A silent bidder is settled around, not waited for

    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 whose reveal was not posted in the reveal window: their bid is marked forfeited and the auction completes without them. What they get back depends on where the bid sat — at or below the clearing price, they redeem the escrow afterwards with a late loser-side proof built from their seed; above it, redeem_forfeit cannot return it.

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. It is designed to come back on the private rail as a note inside the pool, so even the refund would not reveal how much was unspent; that path is waiting on the pool, and for now every refund is collected publicly.

A bid that is not revealed is settled around, not waited for

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 each bid is revealed after the seal: its seed and level are posted on chain, encrypted to the auctioneer’s key for that auction, by Vickrey’s relay or by the bidder. A bid whose reveal is not posted within the reveal window is marked forfeited and settlement proceeds without it. The auction does not wait, does not stall, and does not need their cooperation.

What the bidder gets back depends on where their bid sat. At or below the clearing price, they can build the loser-side proof from their seed after the auction finalizes, and redeem_forfeit returns the escrow in full: going quiet cost them a delay. Above the clearing price, redeem_forfeit cannot return it.

Forfeited escrow stays in the contract, deliberately

Read the contract and you will find money that can never come out. A bid whose anchors match no rung — which place_bid cannot detect, because it is handed two hashes and never a level — is marked forfeited at settlement, and its escrow then sits there permanently. No claim path releases it: collect refuses a forfeited bid, redeem_forfeit needs a witness that cannot exist for a bogus anchor, and finalize never sweeps it.

That looks like a bug. It is what keeps a different one closed.

Marking a bid forfeited requires no proof at all — the auctioneer simply declares it, and the dispute window is what checks them. So if forfeited escrow were paid out to the seller, an auctioneer who is also the seller would profit from forfeiting everybody. The obvious fix funds the attack.

Stranding it removes the incentive completely: a forfeit pays nobody, so there is nothing to gain by declaring one. Someone honest who went offline with a bid at or below the clearing price is not stranded: they can produce the loser-side proof late, and redeem_forfeit returns their escrow in full. Which escrow does stay in the contract is set out under the lot question below.

Four end-to-end tests cover this, and a conservation test asserts the contract holds nothing at all once an ordinary auction has been claimed out.

Leaving a bid out is on the record

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.

Ordering limits it: seal() freezes the set and stamps the block before any bid is revealed, so the set cannot be chosen after seeing the contents. Each reveal is then posted on chain, encrypted to the auctioneer, inside a reveal window that closes before settlement can start. A reveal posted in time is a public record that the auctioneer had the bid.

If the settlement marks a bid forfeited although its reveal was posted in time, and the bid beat the clearing price or the settlement named no winner, that bidder can void the settlement while the dispute window is open. The auction is cancelled, every escrow comes back, and the auctioneer’s bond is added to that bidder’s escrow.

Voiding opens that one bid’s reveal, so its amount becomes public. Only a bid the settlement wronged can do it: the winner’s bid, a bid that was ranked, or one whose reveal was never posted cannot.

Walking away is not free: abandon forfeits the auctioneer’s bond to the bidders, split evenly and paid out with their escrow.

The bond is bounded at both ends. At least one tick, so there is always something at stake in a settlement bidders are asked to trust. At most the uniform collateral, so a bidder’s share of a forfeited bond can never exceed what they staked themselves — otherwise the auction failing would be worth more to them than it succeeding.

This was wrong until 30 August 2026. The bond was returned to the seller by abandon, and it is pulled from the seller at listing — so where one address was both seller and auctioneer, discarding an outcome cost only gas, and it was cheaper than the exclusion attack the bond existed to deter. This page claimed a protection the contract did not provide. It was found by auditing the exit paths rather than by a failing test, and the tests that now cover it were written from the audit.

Where your assets are, state by state

Both the lot and every escrow sit in the auction contract itself. It is not a vault contract or a multisig — it is the same contract that runs the auction, and nothing but the paths below moves anything out of it.

The lot

The seller’s full lot — an amount of a token, or one NFT — is pulled at create_auction, before the auction is visible to anyone. There is no partial or lazy funding: an unfunded listing does not exist. An off-chain lot is not held by anyone; its delivery escrow is set out below.

StatusWhere the lot isWho can move it, and how
OpenThe contractNobody. No entrypoint moves the lot while bidding is open — not the seller, not the auctioneer, not us.
SealedThe contractAnyone, via abandon, once the grace has expired (the reveal window, then the dispute window, after the seal). The auction is cancelled.
SettledThe contractA bidder the settlement left out, via dispute while the window is open — the auction is cancelled. Or anyone via finalize once the window closes.
FinalizedThe contractThe winner only, via collect, by presenting the claim secret. Once, and to any recipient they name; an NFT always goes to a public address.
CancelledThe contract, owed to the sellerAnyone, via reclaim_lot, which only ever sends it to the seller. All three routes to Cancelled — dispute, abandon, and finalize with no winner — make it reclaimable.

Escrowed payment

One escrow per bid, pulled at place_bid, always the top of the ladder so the amount transferred says nothing about the bid behind it.

StatusWhere it isWho can move it, and how
Open / SealedThe contractNobody. Not even the bidder — there is no withdraw.
SettledThe contractNobody. settle moves no money at all; it only records the outcome and opens the dispute window.
FinalizedThe contractThe clearing price has already left the winner’s escrow inside finalize: owed to the seller, who takes it with withdraw_seller, or held in the delivery escrow for an off-chain lot. What remains is claimable only by whoever holds each bid’s claim secret: collect for a loser’s full escrow or the winner’s surplus, redeem_forfeit for a bid marked forfeited at or below the clearing price.
CancelledThe contractEvery bid collects in full through collect, forfeits included — no settlement ever established who forfeited. If the auction was abandoned with bids in it, each claim also carries an equal share of the forfeited bond. If a dispute cancelled it, the disputer’s bid also carries the whole bond.

The bond

Pulled from the seller at listing, despite the name — seller and auctioneer may be different addresses. It goes to a successful disputer’s escrow, back to the seller on finalize (taken with withdraw_seller), back to the seller on abandon if nobody bid, and otherwise into a pot split equally among the bidders as they collect.

An off-chain lot: the delivery escrow

The contract holds nothing of the item. It holds the seller’s bond from listing and, after finalize, the clearing price. When the winner collects, they name a buyer address; only that address can confirm or reject delivery, and only before the delivery deadline.

  • Confirm pays the price and the seller’s bond to the seller.
  • Reject locks both in the contract for good. The buyer’s payment does not come back — it is destroyed — and the seller loses the bond. Neither side gains from either answer, which is what keeps both honest.
  • Neither: once the deadline passes, anyone can release the price and the bond to the seller.

Is there a state where the lot is stuck?

Yes, one. Finalized, with a winner who has lost their claim secret. The lot is claimable only by presenting it, no path returns it to the seller after finalization, and nothing sweeps it on a timer. It stays in the contract permanently. The same is true of that bid’s surplus.

Two escrow cases end the same way: a bid whose anchors match no rung, which can never produce the witness redeem_forfeit requires; and a bid that was forfeited while sitting above the clearing level, which redeem_forfeit cannot return once the auction is finalized.

One near-miss worth naming rather than hiding: an auction left Open forever holds the lot with no path out, because abandon requires Sealed. It is not actually stuck — seal is permissionless and callable by anyone the moment the bid deadline passes, which puts it on the road to abandon. But it takes somebody choosing to act, and nothing forces them to.

Which tokens work, and which quietly do not

The contract accepts any ERC-20 as payment or lot, and any ERC-721 as a lot. There is no allowlist and no decimals constraint; a lot token must answer as the kind it is listed as, and every ERC-20 pull is checked for what arrived. Decimals are read from the token itself everywhere they are displayed, never assumed.

Fee-on-transfer tokens are refused; rebasing tokens are not

Every amount here is a number the contract recorded. In this version every pull also measures what actually arrived — the contract’s balance before and after — and reverts if it is short. A fee-on-transfer token is therefore refused at listing and at bidding, before anything is booked. Two tests cover it with a deliberately hostile token.

A rebasing token is not caught: it changes balances after the transfer, and the contract never re-reads them. When it rebases down, a later claimant’s transfer can fail; when it rebases up, the surplus belongs to no bid and no path sweeps it.

It can affect other auctions, not just its own

One contract holds every auction’s funds in one balance per token, so a rebasing token’s shortfall is not paid by whoever listed it. The claims that come first succeed out of the pooled balance, and the last claimant’s transfer fails — on an auction that may never have dealt with the person who caused it. Choosing a well-behaved token protects you from your own mistake, not from someone else’s.

There is no allowlist, deliberately: it would make us the arbiter of which assets may be auctioned. Use standard, non-rebasing ERC-20s. STRK and ordinary tokens are fine. If you are listing something unusual, check how it behaves first.

The contract supports a lot token and a payment token that differ. The create form here submits the same address for both, which is a simplification of the interface and not of the protocol.

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 — is built to return as an open note credited inside the pool, with no public leg on the way out. That path is not available yet: the pool screens open-note deposits from contracts it hasn’t cleared, and it hasn’t cleared the anonymizer. Until it does, every collect is public, and collecting links the collecting address to the auction.

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.

The reveal, and what it costs

After seal(), each bid’s { seed, level } is encrypted to a public key the auctioneer published with the listing, made fresh for that one auction, and posted on chain with post_reveal. The auctioneer decrypts it with the matching secret key and so learns the exact bid. Nobody without that key can read it.

The reveal window is enforced on chain. post_reveal is accepted only between the seal and the reveal deadline, and settle only after it. A bid whose reveal is not posted by the deadline is settled as forfeited. A posted reveal is checked against the bid’s own anchors wherever it counts, so an invented one changes nothing.

How it travels

Vickrey’s relay posts each reveal from its own account, so your wallet is not attached to it. The relay can fail to post, but it cannot alter a reveal or move anything. If it has not posted, the bid page offers to post it from your wallet, which puts your address next to your bid index on chain. Not your amount.

What this costs. The encrypted bids stay on chain permanently, so anyone who ever obtains an auction’s secret key can read every bid in it. The key is generated in the auctioneer’s browser for that auction alone and saved as a file, and the console prompts the auctioneer to delete it once the auction is final. A reveal never contains your claim secret, so it cannot move your money — only show what you bid.

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. Each bid’s reveal is posted on chain, encrypted to the auctioneer, until the reveal deadline. A reveal not posted in time marks the bid forfeited: at or below the clearing price that costs a delay; above it, redeem_forfeit cannot return the escrow.
  3. Settled — the outcome is proved on-chain from N+1 witnesses. The dispute window opens here — the only time a bid left out of the settlement can void it.
  4. Finalized — the window closed clean. The winner collects the lot and their surplus; every other bid collects its escrow in full. For an off-chain lot, the price waits in the delivery escrow.
  5. Cancelled — a dispute succeeded, nothing was awarded, or the auctioneer never settled. Everything unwinds and every bid collects its escrow in full.

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.

Disclosure: we shipped an endpoint that exposed every revealed bid

Found and fixed 30 August 2026.

The off-chain reveal relay of the first version had a read endpoint — GET /api/reveals?auctionId=N — with no authentication of any kind. It returned every reveal posted for that auction, each carrying the bidder’s exact level, in plaintext, to anyone who asked. Auction ids are sequential integers, so finding them required no guessing worth the name.

For the window between a bidder revealing and the auctioneer settling, on any auction whose bidders used the relay, every bid amount was public — on a site whose central claim is that losing bids are never published. The chain never held those amounts. We did.

What was affected, and what was not

Only reveals posted through the relay, and only after sealing. It never held a claim secret, so no funds were ever reachable through it, and it could not alter an outcome: every reveal is re-checked against the anchors the chain stores. The exposure was of information, not of money — but the information is the product.

How it was found

By auditing the reveal channel rather than the contract, after a lifecycle review asked where { index, seed, level } actually goes. The route’s own comment claimed “the worst it can do is withhold a reveal” — reasoning carefully about the write path and never asking what the read path exposed. Every contract audit we had run would have passed, because the defect was not in the contract.

The bar this failed

Worth stating plainly, because otherwise a reader cannot tell whether the standard we fell short of was one anybody holds. The other entries in this RFP publish every bid at reveal, by design. Commit–reveal ends with each bid posted on chain so the contract can check it against its hash; that is not a defect in those designs, it is how they work.

So the property we broke for a few days is one nobody else in the category is attempting at all. That cuts both ways and we would rather say both. It is not an excuse — we shipped a leak on the one path we claim to protect, and a claim you only keep by accident is not a claim. It is the context: the bar was ours, we set it higher than the field, we failed it in a place we had not looked, and we found it by going looking.

What changed

The relay was switched off the day it was found, and reveals went back to copy-and-paste. In the second version of the contracts it is gone: each reveal is posted on chain, encrypted to the auctioneer’s key for that auction, so no server ever holds a bid in plaintext.

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 misplace any bid it settles, or misreport the price those bids set, without failing a proof. What is not: after sealing, the auctioneer learns every bid amount — it can never publish them or spend anyone's funds, but it knows them, and every bid sits on chain encrypted to the auction's key, readable by anyone who ever holds that key; it can settle without a bid by recording it as forfeited, but a bid it left out that beat the price, whose reveal was posted in time, voids that settlement and takes the auctioneer's bond; and the number of bids, their timing, and the uniform escrow amount are public on-chain.