Skip to main content

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:

  1. block.timestamp >= deadline
  2. SealedAuction.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 licenseTermsId is attached to ipId before 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.