The protocol, end to end.
Every step that happens between a seller listing an IP and the winning bidder receiving a license token. No off-chain operator touches the bid amount. Not us, not the validators, not the seller.
Seller lists the IP
The seller calls SealedAuction.createAuction(ipId, licenseTermsId, deadline) on SealedAuction. The auction is registered with AuctionRevealCondition so CDR validators know exactly when the sealed fields can be revealed: deadline elapsed AND auction triggered.
The reserve price is sealed too: createAuction allocates a seller-side CDR vault, and the seller writes the encrypted reserve via submitEncryptedReserve(auctionId, ciphertext). Bidders never see the floor; it is revealed and verified against the seller's signature at settle. Neither side sees the other's number.
No deposit is escrowed yet. Listing is free aside from gas.
Bidder allocates a CDR vault slot
The bidder's wallet calls WIP.approve(SealedAuction, deposit) then SealedAuction.allocateBidSlot(auctionId, deposit). The contract pulls the deposit into escrow and asks CDR to allocate a fresh ciphertext uuid for this bid. The uuid + bidder + deposit get recorded on-chain.
At this point the world knows that you bid and how much you deposited, but not what you actually bid. The deposit is the upper bound the contract will trust at settle.
Bidder threshold-encrypts the bid off-chain
The bidder constructs a 149-byte payload (address || amount || nonce || signature) and signs the digest keccak256(abi.encode(auctionId, bidder, amount, nonce)) with the same wallet that allocated the slot. The signature is wrapped with the Ethereum-signed-message prefix so it matches browser-wallet personal_sign output.
The payload is then TDH2-encrypted under the CDR validator set's threshold public key, with the encryption label bound to the uuid from step 02. Only a quorum of validators can decrypt. Never a single party.
Bidder submits the ciphertext
The wallet calls SealedAuction.submitEncryptedBid(auctionId, uuid, ciphertext). The contract verifies that the caller is the same address that allocated the slot, that no ciphertext was already written, and that the deadline hasn't passed. The ciphertext lands in the CDR vault keyed by uuid.
The on-chain state is now committed and binding. The bidder cannot change their amount, and no one can read it.
Anyone triggers the auction at deadline
Once block.timestamp ≥ deadline, anyone can call SealedAuction.trigger(auctionId). The contract flips state to Triggered, satisfying both gates of the AuctionRevealCondition (deadline elapsed AND triggered), and emits an event. Validators begin submitting partial decryptions for every uuid in the auction.
Validators reveal in lockstep
Each validator submits a partial decryption share to CDR. Once t of n shares arrive per bid, the orchestrator reconstructs the plaintext payload: bidder, amount, nonce, signature. Every bid in the auction is revealed in the same window; no one bid is decryptable on its own.
Validators see only their own share. The complete plaintext never exists outside the reconstruction step.
Settlement in one transaction
The orchestrator calls SealedAuction.settle(auctionId, reveals[]) with the decrypted bid amounts and signatures. The contract:
- Re-verifies every signature with the prefixed-digest ecrecover.
- Rejects any reveal where the amount exceeds its deposit (you can't bid more than you escrowed).
- Picks the highest valid bid above reserve.
- Mints a Story PIL license token to the winner via PILicenseTemplate.
- Transfers the winning amount in WIP to the seller.
- Refunds the winner's overpayment (deposit − winning amount).
- Refunds every losing bidder's full deposit.
All of that happens in a single transaction. If any step would revert, the whole thing reverts and nobody loses anything.
If the wallet allocates a slot but never writes ciphertext, the slot is marked hasCiphertext=false. At settle, that bid is skipped and the deposit is refunded automatically.
If the off-chain decrypted signature recovers an address ≠ b.bidder (corrupted payload, tampering), the bid is skipped and the deposit refunded.
If someone reveals a higher amount than they actually deposited (impossible without breaking the threshold), the contract refuses the reveal and refunds.
If no valid bid clears the reserve price, every deposit is refunded and no license is minted. State flips to ExpiredNoWinner.
Any address can call trigger() after the deadline. There's no privileged operator. If the orchestrator goes down, you can do it yourself.
If t-of-n decryption shares don't arrive, the auction stays in Triggered state. Bids remain encrypted; deposits remain escrowed; the contract can be migrated only by governance.