Defenses encoded in the contract
This page enumerates the specific checks the SealedAuction contract performs at each entry point. Use it to verify the threat-model claims yourself.
Deposit-bound bids
if (r.amount > b.deposit) continue; // refund b.deposit to b.bidder
A bidder cannot win with an amount greater than the deposit they escrowed.
settle() checks reveal.amount <= slot.deposit before treating the bid as
valid. Stealing via overbid is impossible without breaking the encryption.
Bid signature re-verification (eth-signed-prefix ecrecover)
address recovered = BidPayload.recoverSigner(
auctionId, b.bidder, r.amount, r.nonce, r.signature
);
if (recovered != b.bidder) continue;
For every bid in the reveals[] array, settle() recomputes the
eth-signed-message prefixed digest and calls ecrecover. The recovered
address must match the on-chain bid.bidder. A forged or tampered reveal is
silently skipped. The digest is:
bytes32 digest = keccak256(abi.encode(auctionId, bidder, amount, nonce));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)
);
return ecrecover(ethSignedHash, v, r, s);
Including the \x19Ethereum Signed Message:\n32 prefix means signatures
produced by browser wallets via personal_sign (or viem's signMessage)
verify identically to those from an SDK private key.
Sealed-reserve verification
The seller's reserve price is sealed in its own CDR vault. At settle, a
ReserveReveal {amount, nonce, signature} must be provided. The contract
applies the same recoverSigner logic, but the expected signer is the seller:
address recoveredSeller = BidPayload.recoverSigner(
auctionId, a.seller, reserveReveal.amount, reserveReveal.nonce, reserveReveal.signature
);
if (recoveredSeller != a.seller) revert InvalidReserveReveal();
A sealed reserve cannot be bypassed by omitting the reveal or passing an empty
signature. An empty signature recovers address(0), which does not equal the
seller, so the call reverts InvalidReserveReveal. If the seller never called
submitEncryptedReserve (i.e., reserveHasCiphertext == false), the contract
skips the verification entirely and uses a reserve floor of 0, allowing any
valid bid to win.
Highest-valid-bid wins at or above reserve
if (r.amount >= reserve) {
winnerIdx = r.bidIndex;
winnerAmount = r.amount;
...
}
After the reserve is resolved, only bids with amount >= reserve are
considered for the winner. If no bid clears the reserve, the auction
transitions to ExpiredNoWinner and all deposits are refunded. The winning
bid is the highest amount; earliest blockNumber wins ties.
Atomic settlement
License mint, payment to seller, winner-overpayment refund, and all loser
refunds occur in a single transaction. If any step fails, the entire
settle() reverts. Partial settlement is impossible.
1. Mint license token — reverts if PIL contract throws
2. Pay seller — reverts if WIP transfer fails
3. Refund winner's overpay — reverts if WIP transfer fails
4. Refund each loser — reverts if any WIP transfer fails
Multi-step reveal gate (deadline AND triggered)
Validators only release decryption shares once AuctionRevealCondition.checkReadCondition returns true. That requires BOTH:
block.timestamp >= deadlineSealedAuction.isTriggered(auctionId) == true
Passing the clock alone is not enough. Someone must also call trigger().
This eliminates a class of timing attacks where a deadline-aware observer
tries to race decryption.
Immutable deadline
auction.deadline is set at createAuction and never mutated. There is no
extend or shorten function. allocateBidSlot and submitEncryptedBid both
revert after the deadline. settle() requires Triggered state, which
itself cannot be reached before the deadline.
No admin fund-movement
There is no admin withdrawal, emergency drain, pause-and-sweep, or
owner-transfer function in SealedAuction. Deposited WIP can only leave the
contract via settle() (to the seller and winner) or refunds (to bidders).
No operator account can move funds unilaterally.
EIP-2 low-s signature enforcement
BidPayload.recoverSigner rejects high-s signatures to prevent malleability:
require(
uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
"bad-s"
);
This means a signature is unique in its canonical form and cannot be recast into a different valid encoding.
UUID binding
submitEncryptedBid checks that the caller is the same address that allocated
the slot:
if (slot.bidder != msg.sender) revert NotBidder();
Only the wallet that escrowed the deposit can write the ciphertext for that
uuid, and only once (CiphertextAlreadyWritten reverts a second write).
UUID uniqueness across auctions
CDR allocates each uuid fresh from its own counter. bidIndexByUuid maps each
uuid to exactly one slot in exactly one auction. A uuid from a closed auction
cannot be reused in a new one.
Ciphertext-required check
At settle, bids with hasCiphertext == false are skipped and refunded. A
bidder who allocated a slot but never submitted ciphertext gets their deposit
back without penalty.
Auction-id in signed digest
The digest includes auctionId. A signature produced for auction 5 will not
recover correctly when verified against auction 6, preventing cross-auction
replay.
Single-shot state transitions
trigger() requires state Open; settle() requires state Triggered. Each
writes a terminal target state in the same call. There is no path to
re-trigger or re-settle.
What is NOT defended at the contract level
- Validity of the IP being listed. The contract does not verify the seller owns or has rights to the IP.
- Accuracy of the license terms id. The contract does not confirm
licenseTermsIdis attached toipIdbefore listing. If it is not, settle reverts at the mint step. - Gas limit on large settle. Settlement gas scales linearly with bid count. No protection against running out of gas if bid count is extremely large.
- CDR reveal integrity vs. ciphertext. The contract verifies the seller signature on the revealed amount, but it does not check that the revealed amount matches what was encrypted in the CDR vault. Confidentiality and reveal-integrity rest on the CDR threshold network (see CDR network).
Test coverage
All defenses above are covered by Foundry tests. See Audit status for the full test inventory.