Skip to content

Commit 3bc9f66

Browse files
committed
finalize documentation on encrypted auction example
1 parent 8e84179 commit 3bc9f66

3 files changed

Lines changed: 138 additions & 3 deletions

File tree

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ evm_version = "istanbul"
8282
|---|---|---|
8383
| [Encrypted Value Registry](encrypted-value-registry/README.md) | Stores a value encrypted and reveals it to authorized accounts. Authorized viewers are hidden (encrypted). | CTX, ECIES, TE |
8484
| [Role-Based Value Registry](role-based-value-registry/README.md) | Manages confidential values per role, distributing role secrets to users and encrypting shared role values with a role public key. | Roles, CTX, ECIES, TE, re-encryption |
85+
| [Sealed-Bid Auction](sealed-bid-auction/README.md) | Auctions an NFT with fully encrypted bids. Decrypts the entire bid set through a self-referential CTX chain and settles with the highest qualified bidder. | CTX, TE, recursive CTX chain, ERC-20, ERC-721 |
8586

8687
## Local testing with mocks
8788

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!-- cspell:words ciphertext ciphertexts ECIES leaderboard -->
2+
3+
# Sealed-Bid Auction
4+
5+
> **Disclaimer:** This example is **not** production-ready. It is provided for educational and demonstration purposes only.
6+
7+
## Overview
8+
9+
This example demonstrates a fully on-chain sealed-bid auction for an ERC-721 token. Bidders submit encrypted bids so that no one — including the contract — can see the bid amounts until after the auction closes. When the auction ends, the contract decrypts all bids through a **recursive CTX chain**, ranks them in a leaderboard, and settles with the highest bidder who can cover their bid.
10+
11+
It exercises three core BITE features together:
12+
13+
- **Threshold Encryption (TE):** Each bid is encrypted client-side with the network's threshold key before being sent on-chain. The plaintext amount is never visible in transaction data or contract storage.
14+
- **Conditional Transaction (CTX):** Two distinct CTX flows are used. The first decrypts and validates each individual bid at submission time. The second decrypts the full batch of stored bids during the processing phase.
15+
- **Self-referential CTX chains:** When processing bids, `onDecrypt` can re-invoke `_processBids`, which submits a new CTX from within the callback itself. This allows the contract to process arbitrarily large bid sets across multiple rounds without requiring a single transaction to have enough gas for the entire set.
16+
17+
## How it works
18+
19+
### 1. Deployment
20+
21+
The deployer calls the constructor with the NFT contract, token ID, minimum bid amount, ERC-20 payment currency, and the maximum number of top winners to track. The constructor immediately calls `nft.transferFrom` to take custody of the token, so the NFT must be approved before deployment.
22+
23+
The predicted deployment address can be computed via `getCreateAddress` from the deployer nonce, allowing a single approve→deploy flow.
24+
25+
### 2. Starting the auction
26+
27+
The owner calls `startAuction(endTime)` and sends enough gas Tokens to cover the gas cost of at least one batch-processing callback. The auction transitions to `OPEN` state and the bidding window is set.
28+
29+
### 3. Placing a sealed bid
30+
31+
A bidder calls `sendBid(encryptedBid)` with:
32+
33+
- `encryptedBid` — their bid ABI-encoded as `(address bidder, uint256 amount)` and TE-encrypted off-chain. The expected size is enforced on-chain.
34+
- `msg.value` — gas Tokens deposit covering the gas cost of both the per-bid CTX callback and a share of the batch-processing phase.
35+
36+
Internally:
37+
38+
1. The contract submits a CTX carrying `encryptedBid` as the encrypted argument and `(msg.sender, encryptedBid)` as plaintext arguments, so the callback can verify the bidder's identity without storing plaintext.
39+
2. The returned `ctxSender` address is whitelisted and funded for the callback.
40+
41+
### 4. Bid registration callback (`onDecrypt` — bid path)
42+
43+
When BITE decrypts the per-bid CTX, it calls `onDecrypt`. The contract recognises the bid path by `decryptedArgs.length == 1 && plaintextArgs.length == 2` and:
44+
45+
1. Decodes the bidder address from `plaintextArgs[0]` and the decrypted `Bid` struct from `decryptedArgs[0]`.
46+
2. Asserts `bid.bidder == bidder` — the encrypted bid must claim the same sender as the original transaction (prevents spoofing another account's identity).
47+
3. Checks `bid.amount >= minimumBid` and the per-bidder cap.
48+
4. Pushes the raw ciphertext into `encryptedBids[]` for the processing phase.
49+
5. On the first valid bid from a given address, locks `minimumBid` ERC-20 tokens via `transferFrom` as a commitment deposit.
50+
6. Emits `BidPlaced`.
51+
52+
### 5. Processing bids — recursive CTX chain
53+
54+
After the auction closes, anyone can call `startProcessingBids()`. This marks the state as `PROCESSING`, emits `BidProcessingStarted`, and calls the internal `_processBids`.
55+
56+
`_processBids` submits a CTX for the next batch (up to 5 bids at a time), passing the raw ciphertexts as encrypted arguments with no plaintext arguments. When the callback fires, the contract decodes each `Bid` struct and updates the `TopUniqueBids` leaderboard, then calls `_processBids` again to schedule the next batch.
57+
58+
This creates a **self-referential CTX chain**: `_processBids → CTX → onDecrypt → _processBids → …` The chain terminates naturally when all bids have been processed and the state advances to `FINALIZED`.
59+
60+
If a callback runs low on gas before it can issue the next CTX, `_allowResume` is set to `true`. A keeper can then call `startProcessingBids()` again to resume from where the chain left off, emitting `BidProcessingResumed`.
61+
62+
### 6. Settling the auction
63+
64+
Once `FINALIZED`, anyone can call `settleAuction()`. The contract iterates the leaderboard from highest to lowest and:
65+
66+
- **Disqualifies** a bidder if their ERC-20 balance plus locked deposit is insufficient or their allowance for the remaining amount is too low. Their locked `minimumBid` is forfeited to the owner as a penalty for not having the funds available.
67+
- **Accepts** the first bidder who can pay in full: pulls `bid.amount - minimumBid` additional ERC-20 from them, forwards the full `bid.amount` to the owner, and transfers the NFT to the winner.
68+
- If no bidder qualifies, the NFT is returned to the owner.
69+
70+
After settlement, any remaining gas Tokens are divided equally among the remaining bidders as their gas refund share (`_refundGas`). Usualy this amount will be zero or very close to zero.
71+
72+
### 7. Refunds
73+
74+
Once the state is `SETTLED` or `CANCELLED`, each non-winning bidder calls `refundBidder(address)` to recover their locked `minimumBid` ERC-20 and their remaining gas share.
75+
76+
When all bidders have been refunded (`_bidders` is empty), the next call to `refundBidder` forwards any remaining contract gas Tokens to the owner.
77+
78+
In case the auction gets permanently stuck, an emergency path allows refunds 7 days after `endTime` regardless of state. Additionally, the owner can reclaim the NFT via `unblockNft()` after the same 7-day window if it is still held by the contract.
79+
80+
## Contract interface
81+
82+
| Function | Visibility | Description |
83+
|---|---|---|
84+
| `constructor(nft, tokenId, minimumBid, currency, topWinnersCount)` || Takes custody of the NFT and initialises auction parameters |
85+
| `startAuction(endTime)` | `external payable onlyOwner` | Opens the auction; requires gas Tokens for initial processing gas |
86+
| `cancelAuction()` | `external onlyOwner` | Cancels from `NOT_STARTED` or `OPEN`; returns NFT to owner |
87+
| `sendBid(encryptedBid)` | `external payable` | Submits an encrypted bid and funds the per-bid CTX callback |
88+
| `startProcessingBids()` | `external` | Starts or resumes the recursive batch-decryption CTX chain |
89+
| `settleAuction()` | `external` | Iterates the leaderboard and transfers the NFT to the first qualified winner |
90+
| `refundBidder(bidder)` | `external` | Returns a bidder's locked ERC-20 and gas Tokens share; drains leftover gas Tokens to the owner when the set is empty |
91+
| `changeGasPrice(gasPrice)` | `external onlyOwner` | Raises the tracked gas price for CTX funding (safety measure for network-wide increases) |
92+
| `unblockNft()` | `external onlyOwner` | Emergency NFT recovery after 7 days past `endTime` |
93+
| `getTopBid(index)` | `external view` | Returns the bid at leaderboard rank `index` (0 = highest) |
94+
| `rankedTopBidderCount()` | `external view` | Number of entries currently in the leaderboard |
95+
| `isRankedTopWinner(bidder)` | `external view` | Whether an address is currently in the top-N leaderboard |
96+
| `onDecrypt(decryptedArgs, plaintextArgs)` | `external` | BITE callback — handles both the per-bid path and the batch-processing path |
97+
98+
## `TopUniqueBids` library
99+
100+
The `TopUniqueBids` library (`contracts/libraries/TopUniqueBids.sol`) maintains a descending-sorted leaderboard of at most `cap` bids with **unique bidders**. If the same bidder submits a higher bid, the old entry is replaced in-place. Bids at or below the current last place are dropped when the leaderboard is full - first bids have priority on ties.
101+
102+
It is a pure in-storage sorted insert — no off-chain sort is needed. It is reccomended to store a low amount of top bidders, for gas costs.Gas configs were not tested for numbers higher than 3.
103+
104+
## Script
105+
106+
The script in [scripts/deployAndTest.ts](scripts/deployAndTest.ts) performs a full end-to-end flow against a live BITE chain:
107+
108+
1. Deploys `MockERC721` and `MockERC20`, mints tokens, and funds four bidder wallets (Deployer, A, B, C).
109+
2. Predicts the auction address via deployer nonce, approves the NFT, and deploys `SealedBidAuction`.
110+
3. Each bidder approves `minimumBid` ERC-20 to the auction contract, then calls `startAuction`.
111+
4. Each bidder TE-encrypts their bid off-chain with `bite.encryptMessageForCTX` and calls `sendBid`.
112+
5. Polls until all bid callbacks have landed (`numBids >= 1` for each bidder).
113+
6. Waits for the auction `endTime`, then calls `startProcessingBids`.
114+
7. Polls until the contract reaches `FINALIZED` state.
115+
8. Verifies the leaderboard order and boundary conditions (`getTopBid`, `isRankedTopWinner`, out-of-range revert).
116+
9. **Settlement:** The rank-0 bidder (Deployer, 1500 tokens) has no extra ERC-20 allowance and is disqualified. The rank-1 bidder (Bidder B, 1000 tokens) approves the remaining `bid.amount - minimumBid` and wins the NFT. The owner receives both the forfeited `minimumBid` from the disqualified bidder and the full winning bid.
117+
10. **Refunds:** All non-winning, non-disqualified bidders (A and C) reclaim their locked `minimumBid` ERC-20.
118+
11. **Owner Gas claim:** Once the bidder set is empty, the owner reclaims any remaining contract gas Tokens via `refundBidder`.
119+
120+
### Environment variables
121+
122+
| Variable | Required | Description |
123+
|---|---|---|
124+
| `PRIVATE_KEY` | yes | Deployer private key |
125+
| `ENDPOINT` | yes | RPC endpoint used for both chain access and BITE off-chain encryption |
126+
127+
### Run
128+
129+
```bash
130+
PRIVATE_KEY=0x... ENDPOINT=https://... yarn hardhat run scripts/deployAndTest.ts --network custom
131+
```

examples/sealed-bid-auction/contracts/SealedBidAuction.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,18 @@ contract SealedBidAuction is IBiteSupplicant, Ownable2Step {
332332
encryptedArgs[i] = encryptedBids[bids.length + i];
333333
}
334334
bytes[] memory plaintextArgs = new bytes[](0);
335-
335+
uint256 gasNeeded = gasForProcessingBids * missingBids;
336+
if(missingBids < 5) {
337+
gasNeeded += 350_000; // For final bids, in case only few are sent.
338+
}
336339
address payable ctxSender = BITE.submitCTX(
337340
BITE.SUBMIT_CTX_ADDRESS,
338-
gasForProcessingBids * missingBids,
341+
gasNeeded,
339342
encryptedArgs,
340343
plaintextArgs
341344
);
342345
_authorizedCtxSenders[ctxSender] = true;
343-
ctxSender.sendValue((350_000 + gasForProcessingBids * missingBids) * gasPrice);
346+
ctxSender.sendValue(gasNeeded * gasPrice);
344347
}
345348

346349
function _handleBidCallback(bytes[] calldata decryptedArgs, bytes[] calldata plaintextArgs) private {

0 commit comments

Comments
 (0)