Settlement flow
This is the most important transaction in the protocol. Everything that
needs to happen at auction close — the reserve reveal, the license mint, the
seller payout, the winner's overpay refund, and every losing bidder's refund
— happens in one atomic call to settle().
The full sequence
Step-by-step
1. Trigger
Anyone calls trigger(auctionId). The contract checks
block.timestamp >= auction.deadline and flips state to Triggered. If
bidCount == 0, state goes directly to ExpiredEmpty and the flow ends.
trigger() is the second gate that opens AuctionRevealCondition. Validators
will not publish decryption shares until both gates pass: the deadline has
elapsed AND isTriggered(auctionId) returns true.
2. Validators publish shares
This happens off-chain inside CDR. The AuctionTriggered event signals
validators that every vault registered for this auction is now eligible for
decryption. That includes both the bid vaults and the reserve vault.
Each validator computes their share for each uuid and publishes it to the CDR
contract. Once t-of-n shares are available per uuid, the plaintext can be
reconstructed. Validators only ever see their own share.
3. Orchestrator reads plaintext
The orchestrator calls into CDR to read the plaintext payload for every uuid in the auction: one per bid vault, one for the reserve vault.
The plaintext for each vault is a 149-byte payload:
| address (20 bytes) | amount (32) | nonce (32) | signature (65) |
signer uint256 bytes32 r,s,v
For a bid, signer is the bidder. For the reserve, signer is the seller.
Both use the same digest format:
keccak256(abi.encode(auctionId, signer, amount, nonce)), prefixed with the
Ethereum signed message prefix so browser personal_sign and SDK
private-key signing produce identical results.
4. Settle call
The orchestrator calls settle(auctionId, reveals[], reserveReveal) with
the array of decrypted bid payloads plus the decrypted reserve payload.
The BidReveal struct per bid: {uint256 bidIndex, uint256 amount, bytes32 nonce, bytes signature}.
The ReserveReveal struct: {uint256 amount, bytes32 nonce, bytes signature}.
5. Reserve reveal and verification
Before evaluating any bid, the contract processes the reserve reveal:
- If
reserveHasCiphertext == false(seller never sealed a reserve), the floor is 0. Every bid is a valid candidate. - If
reserveHasCiphertext == true, the contract callsBidPayload.recoverSigneron thereserveReveal. The recovered signer must equalauction.seller. If it does not, the contract reverts withInvalidReserveRevealand the entire settlement fails. - On a valid reveal,
reserveReveal.amountbecomes the floor.
The reserve is never stored in plaintext on-chain; it exists only during this verification step.
6. Bid reveal verification
For each reveal in the bid array, the contract checks:
- Signature recovery.
BidPayload.recoverSigner(reveal)must equalb.bidder(the address that allocated the slot). Tampered or forged signatures fail here. - Ciphertext presence. The slot must have
hasCiphertext == true. A slot that was allocated but never had ciphertext written is skipped and refunded. - Deposit cap.
reveal.amount <= b.deposit. You cannot reveal a bid larger than what you escrowed. - Reserve floor.
reveal.amount >= revealed reserve. Bids below the floor are skipped.
Bids that fail any check are dropped silently and refunded. A corrupt or malicious bid cannot block settlement.
7. Pick winner
Among valid revealed bids, the contract picks the highest amount. Ties are
broken by lowest bidIndex (earlier slot allocation wins).
If no bid is valid, the contract goes to the no-winner path (step 9).
8. Winner path
The contract executes, in order:
LicensingModule.mintLicenseTokens(winner, ipId, termsId, 1, ...)— mints the PIL license token to the winner. Reverts on failure, reverting the whole settlement.WIP.transfer(seller, winningAmount)— pays the seller the winning bid amount.WIP.transfer(winner, deposit - winningAmount)— refunds the winner's overpayment.- For each loser:
WIP.transfer(loser, deposit)— full refund.
Emits AuctionSettled(auctionId, winner, winningAmount, licenseTokenId) and
flips state to Settled.
9. No-winner path
If no valid reveal cleared the reserve, the contract:
- For each bidder:
WIP.transfer(bidder, deposit)— full refund. - Flips state to
ExpiredNoWinner. - Emits
AuctionExpiredNoWinner(auctionId).
No license is minted. The seller receives nothing. Every deposit is returned.
What's atomic about it
Steps 5–9 happen inside the same settle() call. If any single transfer or
mint call fails, the entire transaction reverts. State stays at Triggered
and the orchestrator can retry once the underlying issue is resolved.
This means:
- Partial settlements are impossible. Either every payout happens or none do.
- The license must mint, or nothing else happens. The seller cannot receive payment for a license that did not issue.
- Loser refunds cannot be skipped. A buggy or malicious orchestrator cannot keep any bidder's deposit.
These properties are what allow the marketplace UI to say "license plus payment in one transaction" without hedging.
What a malicious orchestrator can and cannot do
The orchestrator relays the settle() call, but the contract re-verifies
every signature and deposit cap on-chain. A malicious orchestrator can:
- Censor (refuse to call
settle()at all) — but anyone else can call it once they have the decrypted reveals. - Submit an incorrect reveal — but the signature check will reject it; the bid gets refunded.
A malicious orchestrator cannot:
- Forge a winner (no valid signature)
- Pay a non-winner seller (contract enforces the correct payout path)
- Keep a loser's deposit (every deposit is pushed out atomically)
Gas considerations
Settlement gas scales linearly with the number of bids: O(n) WIP.transfer
calls, one per bidder. The orchestrator pays this gas in the current
deployment.
For now: typical auctions have few bidders, gas is paid by the orchestrator, and the atomic push-payment model is the simpler, safer default.
See also
- Atomic settlement — the guarantee in plain terms
- Sealed reserve — why the seller also seals a number
SealedAuction.settle— the function referenceBidPayload.recoverSigner— the signature verification- Defenses encoded — what specifically is verified at settle