Skip to main content

Settling

Settlement is a four-phase sequence carried out by the orchestrator:

  1. Wait for deadline.
  2. Call trigger(auctionId) — flips state OpenTriggered, which opens the AuctionRevealCondition gate for CDR validators.
  3. Decrypt every sealed bid ciphertext (and the sealed reserve, if present) via CDR partial-decryption shares.
  4. 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 the decryptTxHashes).
  • Polls until t-of-n validators 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 ecrecover on every reveal; mismatched signatures are rejected.
  • Rejects any reveal where amount > deposit.
  • Verifies the reserveReveal signature recovers to the seller address (revert InvalidReserveReveal if 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 winningAmount in WIP to the seller.
  • Refunds the winner's overpayment and every losing deposit.
  • State transitions: Settled, ExpiredNoWinner (all bids below reserve), or ExpiredEmpty (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

ErrorWhen it occurs
DeadlineNotReachedtrigger called before block.timestamp >= deadline
WrongStatetrigger called on an auction that is not Open
WrongStateForSettlesettle called when state is not Triggered
InvalidReserveRevealReserve signature does not recover to the seller