Settling
Settlement is a four-phase sequence carried out by the orchestrator:
- Wait for
deadline. - Call
trigger(auctionId)— flips stateOpen→Triggered, which opens theAuctionRevealConditiongate for CDR validators. - Decrypt every sealed bid ciphertext (and the sealed reserve, if present) via CDR partial-decryption shares.
- Call
settle(auctionId, reveals, reserveReveal)— the contract re-verifies all signatures on-chain, picks the winner, mints the license token, transfers WIP to the seller, and refunds losers. All in one transaction.
The settleAuction helper from @sealedip/sdk/orchestrator automates the
whole sequence.
Node only. decryptBid calls into @piplabs/cdr-sdk WASM. See
Installation: WASM constraint.
settleAuction
import {settleAuction} from '@sealedip/sdk/orchestrator'
const result = await settleAuction({
auctionId: 3n,
orchestratorPrivateKey: process.env.ORCHESTRATOR_KEY as `0x${string}`,
rpcUrl: 'https://aeneid.storyrpc.io',
waitForDeadline: true, // default; set false if deadline is already past
})
console.log({
triggerTxHash: result.triggerTxHash, // undefined if already Triggered
decryptTxHashes: result.decryptTxHashes, // one per bid + one for reserve
settleTxHash: result.settleTxHash,
bidCount: result.bidCount,
})
SettleAuctionInput
interface SettleAuctionInput {
auctionId: bigint
orchestratorPrivateKey: `0x${string}`
rpcUrl?: string
/**
* Default true. When true, waits until
* `auction.deadline + DEFAULT_POST_DEADLINE_BUFFER_SECONDS`
* before issuing the trigger. Set to false if the auction is
* already past deadline.
*/
waitForDeadline?: boolean
}
SettleAuctionResult
interface SettleAuctionResult {
triggerTxHash?: `0x${string}` // absent if auction was already Triggered
decryptTxHashes: `0x${string}`[] // CDR read txs; one per decrypted vault
settleTxHash: `0x${string}`
bidCount: number
}
Step-by-step walkthrough
1. Wait and trigger
The orchestrator polls block timestamps until
block.timestamp >= auction.deadline + buffer, then calls:
SealedAuction.trigger(auctionId)
Anyone can call trigger; there is no permission check. If the auction is
already Triggered, settleAuction skips this step.
The trigger flips state and satisfies the second gate of
AuctionRevealCondition (the first gate is block.timestamp >= deadline).
Both gates must be true before CDR validators release decryption shares.
2. Decrypt bids
For each bid slot where hasCiphertext === true:
// decryptBid is an internal helper; settleAuction calls it for you.
// Shown here to illustrate what happens per bid slot.
import {decryptBid} from './decrypt' // sdk/src/decrypt.ts — no public subpath export
const {payload, readTxHash} = await decryptBid({
uuid: bid.ciphertextUuid,
decryptorPrivateKey: orchestratorPrivateKey,
rpcUrl,
timeoutMs: 60_000,
})
// payload: { bidder, amount, nonce, signature }
decryptBid calls CDRClient.consumer.accessCDR, which:
- Submits an on-chain
CDR.read()transaction (this is one of thedecryptTxHashes). - Polls until
t-of-nvalidators publish partial decryption shares. - Combines shares via TDH2 and returns the original plaintext.
Bid slots with hasCiphertext === false are skipped; settle() refunds
those deposits automatically without a reveal entry.
3. Decrypt the sealed reserve
If auction.reserveHasCiphertext === true, the orchestrator decrypts the
reserve vault using the same decryptBid call, but with auction.reserveUuid:
const {payload, readTxHash} = await decryptBid({
uuid: auction.reserveUuid,
decryptorPrivateKey: orchestratorPrivateKey,
rpcUrl,
})
const reserveReveal = {
amount: payload.amount,
nonce: payload.nonce,
signature: payload.signature,
}
If the seller never submitted a reserve ciphertext
(reserveHasCiphertext === false), pass an all-zeros ReserveReveal. The
contract treats a missing or all-zeros reserve as a floor of 0.
const reserveReveal = {
amount: 0n,
nonce: ('0x' + '00'.repeat(32)) as `0x${string}`,
signature: ('0x' + '00'.repeat(65)) as `0x${string}`,
}
4. Call settle
The contract function signature is:
settle(uint256 auctionId, BidReveal[] reveals, ReserveReveal reserveReveal)
The SDK builds reveals from the decrypted payloads:
const reveals = [
{bidIndex: 0n, amount: payload.amount, nonce: payload.nonce, signature: payload.signature},
// one entry per bid with hasCiphertext === true
]
On-chain, settle:
- Re-runs
ecrecoveron every reveal; mismatched signatures are rejected. - Rejects any reveal where
amount > deposit. - Verifies the
reserveRevealsignature recovers to the seller address (revertInvalidReserveRevealif not); floor defaults to 0 if no reserve was sealed. - Picks the highest valid bid that clears the reserve.
- Mints a Story PIL license token to the winner via
LicensingModule.mintLicenseTokens. - Transfers
winningAmountin WIP to the seller. - Refunds the winner's overpayment and every losing deposit.
- State transitions:
Settled,ExpiredNoWinner(all bids below reserve), orExpiredEmpty(no bids).
Partial settlement is impossible: the transaction is atomic; it all succeeds or all reverts.
Permissionless design
There is no onlyOrchestrator modifier on trigger or settle. Any address
with the decrypted reveals can call settle. If the SealedIP orchestrator
goes offline after an auction is triggered, anyone who can read CDR can
step in and settle the auction. The contract re-verifies every signature
on-chain, so a malicious caller cannot forge a winner.
Reading auction and bid state
import {getAuction, getBid, getAllBids} from '@sealedip/sdk/auction'
// Single auction header
const auction = await getAuction(3n)
// auction.reserveUuid, auction.reserveHasCiphertext, auction.state, ...
// All bid slots
const bids = await getAllBids(3n)
for (const bid of bids) {
console.log(bid.ciphertextUuid, bid.hasCiphertext, bid.deposit)
}
See Types for the full field lists.
Proven live on testnet
This flow has been run end-to-end against the real CDR network on Aeneid:
settleAuction triggered the auction, threshold-decrypted both bids and the
sealed reserve, and settled (settle tx 0x15003bbc…), minting the license
token to the winner. See
Audit status: Verified live end-to-end
for the full transaction list, and sdk/scripts/live-settle-test.ts for the
runnable harness.
Error reference
| Error | When it occurs |
|---|---|
DeadlineNotReached | trigger called before block.timestamp >= deadline |
WrongState | trigger called on an auction that is not Open |
WrongStateForSettle | settle called when state is not Triggered |
InvalidReserveReveal | Reserve signature does not recover to the seller |