Skip to main content

Atomic settlement

In SealedIP, everything that needs to happen for an auction to close happens in a single transaction. Either it all succeeds, or none of it does. There's no partial state where the seller got paid but the license wasn't minted, or the winner got their license but the loser's refund didn't arrive.

This is what "atomic settlement" means in the contract sense:

What "atomic" guarantees

When settle() returns successfully:

  • The PIL license token is in the winner's wallet.
  • The winning bid amount in WIP is in the seller's wallet.
  • The winner's deposit minus the winning bid amount is in the winner's wallet as a refund.
  • Every losing bidder's full deposit is in their wallet as a refund.
  • The auction state is Settled.

When settle() reverts:

  • None of those things happen.
  • Every bidder still has their deposit escrowed in the contract.
  • The state stays Triggered so settle can be retried with corrected inputs.

There is no in-between. The Solidity ABI guarantees this.

Why it matters

Without atomicity, you'd need claim flows. The winner would need to call claimLicense(). The seller would need to call withdraw(). Losers would need to call refund(). Each of those is a wallet popup, gas cost, and failure point. Worse, a malicious seller could refuse to mint a license that goes into a wallet they don't like.

Atomic settlement makes the contract — not any human — the executor of every state transition. The license must mint, the payment must clear, and the refunds must process, or the whole thing reverts.

What the contract checks before paying out

At settle time, the contract first verifies the sealed reserve, then verifies every bid reveal.

Reserve verification (before any bid is evaluated):

The orchestrator passes a ReserveReveal {amount, nonce, signature}. If the seller sealed a reserve, BidPayload.recoverSigner runs on this struct and must recover the seller's address. A mismatch reverts the entire settlement with InvalidReserveReveal. If the seller never submitted a reserve ciphertext, the floor defaults to 0 and this step is skipped.

Per-bid verification:

  1. Signature recovery. The decrypted payload contains a signature over keccak256(auctionId, bidder, amount, nonce). The contract recovers the signer and rejects the reveal if it does not match b.bidder.
  2. Deposit cap. If reveal.amount > b.deposit, the reveal is skipped. You cannot bid more than you escrowed.
  3. Reserve floor. Bids below the revealed reserve are skipped.
  4. Ciphertext presence. Slots that were allocated but never had ciphertext written (hasCiphertext == false) are skipped and refunded.

Bids that fail any of these checks are dropped silently and refunded. A corrupt or malicious bid cannot block settlement.

What happens when there's no winner

Three reasons the auction can close with no winner:

  • ExpiredEmpty — nobody bid at all. The contract has no deposits to move; state flips to ExpiredEmpty with one tx.
  • ExpiredNoWinner — bidders bid, but no revealed bid cleared the reserve. Every deposit is refunded; no license mints; state flips to ExpiredNoWinner.
  • Revert during settle — something in the verification logic threw. The state stays Triggered and the orchestrator can retry once the bad input is identified.

In all three "no winner" outcomes, sellers receive nothing and bidders get their deposits back. The license token never mints to anyone.

Implementation note

The atomic settlement is implemented in SealedAuction.settle(). The function body is mostly a single transaction's worth of:

// 1. Verify reserveReveal signature; recover floor (or use 0 if none sealed).
// 2. Verify each bid reveal; drop any that fail signature / deposit cap / floor.
// 3. Pick highest valid bid.
// 4. LicensingModule.mintLicenseTokens(winner, ...) — reverts on failure.
// 5. WIP.transfer(seller, winningAmount) — reverts on failure.
// 6. WIP.transfer(winner, deposit - winningAmount) — reverts on failure.
// 7. for each loser: WIP.transfer(loser, deposit) — reverts on failure.
// 8. Emit AuctionSettled.

Because every external call uses transfer/mint semantics that revert on failure, the whole transaction reverts atomically if any single piece fails.

This is the contract's most important property. Everything else flows from it.