Skip to content

Commit 1458d81

Browse files
feat: added Blind Auctions contract to the dApps with tests (#16)
* blind auction tests work with one error * feat: fix missing edge case in select and use euint256 for tickets (#17) * remove one comment --------- Co-authored-by: jat <[email protected]>
1 parent dda4a16 commit 1458d81

File tree

6 files changed

+490
-2
lines changed

6 files changed

+490
-2
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// SPDX-License-Identifier: BSD-3-Clause-Clear
2+
3+
pragma solidity ^0.8.24;
4+
5+
import "fhevm/lib/TFHE.sol";
6+
import { ConfidentialERC20 } from "fhevm-contracts/contracts/token/ERC20/ConfidentialERC20.sol";
7+
import "@openzeppelin/contracts/access/Ownable2Step.sol";
8+
import "fhevm/config/ZamaFHEVMConfig.sol";
9+
import "fhevm/config/ZamaGatewayConfig.sol";
10+
import "fhevm/gateway/GatewayCaller.sol";
11+
12+
/// @notice Main contract for the blind auction
13+
contract BlindAuction is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConfig, GatewayCaller, Ownable2Step {
14+
/// @notice Auction end time
15+
uint256 public endTime;
16+
17+
/// @notice Address of the beneficiary
18+
address public beneficiary;
19+
20+
/// @notice Current highest bid
21+
euint64 private highestBid;
22+
23+
/// @notice Ticket corresponding to the highest bid
24+
/// @dev Used during reencryption to know if a user has won the bid
25+
euint256 private winningTicket;
26+
27+
/// @notice Decryption of winningTicket
28+
/// @dev Can be requested by anyone after auction ends
29+
uint256 private decryptedWinningTicket;
30+
31+
/// @notice Ticket randomly sampled for each user
32+
mapping(address account => euint256 ticket) private userTickets;
33+
34+
/// @notice Mapping from bidder to their bid value
35+
mapping(address account => euint64 bidAmount) private bids;
36+
37+
/// @notice Number of bids
38+
uint256 public bidCounter;
39+
40+
/// @notice The token contract used for encrypted bids
41+
ConfidentialERC20 public tokenContract;
42+
43+
/// @notice Flag indicating whether the auction object has been claimed
44+
/// @dev WARNING : If there is a draw, only the first highest bidder will get the prize
45+
/// An improved implementation could handle this case differently
46+
ebool private objectClaimed;
47+
48+
/// @notice Flag to check if the token has been transferred to the beneficiary
49+
bool public tokenTransferred;
50+
51+
/// @notice Flag to determine if the auction can be stopped manually
52+
bool public stoppable;
53+
54+
/// @notice Flag to check if the auction has been manually stopped
55+
bool public manuallyStopped = false;
56+
57+
/// @notice Error thrown when a function is called too early
58+
/// @dev Includes the time when the function can be called
59+
error TooEarly(uint256 time);
60+
61+
/// @notice Error thrown when a function is called too late
62+
/// @dev Includes the time after which the function cannot be called
63+
error TooLate(uint256 time);
64+
65+
/// @notice Constructor to initialize the auction
66+
/// @param _beneficiary Address of the beneficiary who will receive the highest bid
67+
/// @param _tokenContract Address of the ConfidentialERC20 token contract used for bidding
68+
/// @param biddingTime Duration of the auction in seconds
69+
/// @param isStoppable Flag to determine if the auction can be stopped manually
70+
constructor(
71+
address _beneficiary,
72+
ConfidentialERC20 _tokenContract,
73+
uint256 biddingTime,
74+
bool isStoppable
75+
) Ownable(msg.sender) {
76+
// TFHE.setFHEVM(FHEVMConfig.defaultConfig());
77+
// Gateway.setGateway(GatewayConfig.defaultGatewayContract());
78+
beneficiary = _beneficiary;
79+
tokenContract = _tokenContract;
80+
endTime = block.timestamp + biddingTime;
81+
objectClaimed = TFHE.asEbool(false);
82+
TFHE.allowThis(objectClaimed);
83+
tokenTransferred = false;
84+
bidCounter = 0;
85+
stoppable = isStoppable;
86+
}
87+
88+
/// @notice Submit a bid with an encrypted value
89+
/// @dev Transfers tokens from the bidder to the contract
90+
/// @param encryptedValue The encrypted bid amount
91+
/// @param inputProof Proof for the encrypted input
92+
function bid(einput encryptedValue, bytes calldata inputProof) external onlyBeforeEnd {
93+
euint64 value = TFHE.asEuint64(encryptedValue, inputProof);
94+
euint64 existingBid = bids[msg.sender];
95+
euint64 sentBalance;
96+
if (TFHE.isInitialized(existingBid)) {
97+
euint64 balanceBefore = tokenContract.balanceOf(address(this));
98+
ebool isHigher = TFHE.lt(existingBid, value);
99+
euint64 toTransfer = TFHE.sub(value, existingBid);
100+
101+
// Transfer only if bid is higher, also to avoid overflow from previous line
102+
euint64 amount = TFHE.select(isHigher, toTransfer, TFHE.asEuint64(0));
103+
TFHE.allowTransient(amount, address(tokenContract));
104+
tokenContract.transferFrom(msg.sender, address(this), amount);
105+
106+
euint64 balanceAfter = tokenContract.balanceOf(address(this));
107+
sentBalance = TFHE.sub(balanceAfter, balanceBefore);
108+
euint64 newBid = TFHE.add(existingBid, sentBalance);
109+
bids[msg.sender] = newBid;
110+
} else {
111+
bidCounter++;
112+
euint64 balanceBefore = tokenContract.balanceOf(address(this));
113+
TFHE.allowTransient(value, address(tokenContract));
114+
tokenContract.transferFrom(msg.sender, address(this), value);
115+
euint64 balanceAfter = tokenContract.balanceOf(address(this));
116+
sentBalance = TFHE.sub(balanceAfter, balanceBefore);
117+
bids[msg.sender] = sentBalance;
118+
}
119+
euint64 currentBid = bids[msg.sender];
120+
TFHE.allowThis(currentBid);
121+
TFHE.allow(currentBid, msg.sender);
122+
123+
euint256 randTicket = TFHE.randEuint256();
124+
euint256 userTicket;
125+
if (TFHE.isInitialized(highestBid)) {
126+
if (TFHE.isInitialized(userTickets[msg.sender])) {
127+
userTicket = TFHE.select(TFHE.ne(sentBalance, 0), randTicket, userTickets[msg.sender]); // don't update ticket if sentBalance is null (or else winner sending an additional zero bid would lose the prize)
128+
} else {
129+
userTicket = TFHE.select(TFHE.ne(sentBalance, 0), randTicket, TFHE.asEuint256(0));
130+
}
131+
} else {
132+
userTicket = randTicket;
133+
}
134+
userTickets[msg.sender] = userTicket;
135+
136+
if (!TFHE.isInitialized(highestBid)) {
137+
highestBid = currentBid;
138+
winningTicket = userTicket;
139+
} else {
140+
ebool isNewWinner = TFHE.lt(highestBid, currentBid);
141+
highestBid = TFHE.select(isNewWinner, currentBid, highestBid);
142+
winningTicket = TFHE.select(isNewWinner, userTicket, winningTicket);
143+
}
144+
TFHE.allowThis(highestBid);
145+
TFHE.allowThis(winningTicket);
146+
TFHE.allowThis(userTicket);
147+
TFHE.allow(userTicket, msg.sender);
148+
}
149+
150+
/// @notice Get the encrypted bid of a specific account
151+
/// @dev Can be used in a reencryption request
152+
/// @param account The address of the bidder
153+
/// @return The encrypted bid amount
154+
function getBid(address account) external view returns (euint64) {
155+
return bids[account];
156+
}
157+
158+
/// @notice Manually stop the auction
159+
/// @dev Can only be called by the owner and if the auction is stoppable
160+
function stop() external onlyOwner {
161+
require(stoppable);
162+
manuallyStopped = true;
163+
}
164+
165+
/// @notice Get the encrypted ticket of a specific account
166+
/// @dev Can be used in a reencryption request
167+
/// @param account The address of the bidder
168+
/// @return The encrypted ticket
169+
function ticketUser(address account) external view returns (euint256) {
170+
return userTickets[account];
171+
}
172+
173+
/// @notice Initiate the decryption of the winning ticket
174+
/// @dev Can only be called after the auction ends
175+
function decryptWinningTicket() public onlyAfterEnd {
176+
uint256[] memory cts = new uint256[](1);
177+
cts[0] = Gateway.toUint256(winningTicket);
178+
Gateway.requestDecryption(cts, this.setDecryptedWinningTicket.selector, 0, block.timestamp + 100, false);
179+
}
180+
181+
/// @notice Callback function to set the decrypted winning ticket
182+
/// @dev Can only be called by the Gateway
183+
/// @param resultDecryption The decrypted winning ticket
184+
function setDecryptedWinningTicket(uint256, uint256 resultDecryption) public onlyGateway {
185+
decryptedWinningTicket = resultDecryption;
186+
}
187+
188+
/// @notice Get the decrypted winning ticket
189+
/// @dev Can only be called after the winning ticket has been decrypted - if `userTickets[account]` is an encryption of decryptedWinningTicket, then `account` won and can call `claim` succesfully
190+
/// @return The decrypted winning ticket
191+
function getDecryptedWinningTicket() external view returns (uint256) {
192+
require(decryptedWinningTicket != 0, "Winning ticket has not been decrypted yet");
193+
return decryptedWinningTicket;
194+
}
195+
196+
/// @notice Claim the auction object
197+
/// @dev Succeeds only if the caller was the first to get the highest bid
198+
function claim() public onlyAfterEnd {
199+
ebool canClaim = TFHE.and(TFHE.eq(winningTicket, userTickets[msg.sender]), TFHE.not(objectClaimed));
200+
objectClaimed = TFHE.or(canClaim, objectClaimed);
201+
TFHE.allowThis(objectClaimed);
202+
euint64 newBid = TFHE.select(canClaim, TFHE.asEuint64(0), bids[msg.sender]);
203+
bids[msg.sender] = newBid;
204+
TFHE.allowThis(bids[msg.sender]);
205+
TFHE.allow(bids[msg.sender], msg.sender);
206+
}
207+
208+
/// @notice Transfer the highest bid to the beneficiary
209+
/// @dev Can only be called once after the auction ends
210+
function auctionEnd() public onlyAfterEnd {
211+
require(!tokenTransferred);
212+
tokenTransferred = true;
213+
TFHE.allowTransient(highestBid, address(tokenContract));
214+
tokenContract.transfer(beneficiary, highestBid);
215+
}
216+
217+
/// @notice Withdraw a bid from the auction
218+
/// @dev Can only be called after the auction ends and by non-winning bidders
219+
function withdraw() public onlyAfterEnd {
220+
euint64 bidValue = bids[msg.sender];
221+
ebool canWithdraw = TFHE.ne(winningTicket, userTickets[msg.sender]);
222+
euint64 amount = TFHE.select(canWithdraw, bidValue, TFHE.asEuint64(0));
223+
TFHE.allowTransient(amount, address(tokenContract));
224+
tokenContract.transfer(msg.sender, amount);
225+
euint64 newBid = TFHE.select(canWithdraw, TFHE.asEuint64(0), bids[msg.sender]);
226+
bids[msg.sender] = newBid;
227+
TFHE.allowThis(newBid);
228+
TFHE.allow(newBid, msg.sender);
229+
}
230+
231+
/// @notice Modifier to ensure function is called before auction ends
232+
/// @dev Reverts if called after the auction end time or if manually stopped
233+
modifier onlyBeforeEnd() {
234+
if (block.timestamp >= endTime || manuallyStopped == true) revert TooLate(endTime);
235+
_;
236+
}
237+
238+
/// @notice Modifier to ensure function is called after auction ends
239+
/// @dev Reverts if called before the auction end time and not manually stopped
240+
modifier onlyAfterEnd() {
241+
if (block.timestamp < endTime && manuallyStopped == false) revert TooEarly(endTime);
242+
_;
243+
}
244+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-License-Identifier: BSD-3-Clause-Clear
2+
3+
pragma solidity ^0.8.24;
4+
5+
import "fhevm/lib/TFHE.sol";
6+
import "fhevm/config/ZamaFHEVMConfig.sol";
7+
import "fhevm/config/ZamaGatewayConfig.sol";
8+
import "fhevm/gateway/GatewayCaller.sol";
9+
import "fhevm-contracts/contracts/token/ERC20/extensions/ConfidentialERC20Mintable.sol";
10+
11+
/// @notice This contract implements an encrypted ERC20-like token with confidential balances using Zama's FHE library.
12+
/// @dev It supports typical ERC20 functionality such as transferring tokens, minting, and setting allowances,
13+
/// @dev but uses encrypted data types.
14+
contract BlindAuctionConfidentialERC20 is
15+
SepoliaZamaFHEVMConfig,
16+
SepoliaZamaGatewayConfig,
17+
GatewayCaller,
18+
ConfidentialERC20Mintable
19+
{
20+
// @note `SECRET` is not so secret, since it is trivially encrypted and just to have a decryption test
21+
euint64 internal immutable SECRET;
22+
23+
// @note `revealedSecret` will hold the decrypted result once the Gateway will relay the decryption of `SECRET`
24+
uint64 public revealedSecret;
25+
26+
/// @notice Constructor to initialize the token's name and symbol, and set up the owner
27+
/// @param name_ The name of the token
28+
/// @param symbol_ The symbol of the token
29+
constructor(string memory name_, string memory symbol_) ConfidentialERC20Mintable(name_, symbol_, msg.sender) {
30+
SECRET = TFHE.asEuint64(42);
31+
TFHE.allowThis(SECRET);
32+
}
33+
34+
/// @notice Request decryption of `SECRET`
35+
function requestSecret() public {
36+
uint256[] memory cts = new uint256[](1);
37+
cts[0] = Gateway.toUint256(SECRET);
38+
Gateway.requestDecryption(cts, this.callbackSecret.selector, 0, block.timestamp + 100, false);
39+
}
40+
41+
/// @notice Callback function for `SECRET` decryption
42+
/// @param `decryptedValue` The decrypted 64-bit unsigned integer
43+
function callbackSecret(uint256, uint64 decryptedValue) public onlyGateway {
44+
revealedSecret = decryptedValue;
45+
}
46+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { AddressLike, BigNumberish, Signer } from "ethers";
2+
import { ethers } from "hardhat";
3+
4+
import type { BlindAuction } from "../../types";
5+
6+
export async function deployBlindAuctionFixture(
7+
account: Signer,
8+
tokenContract: AddressLike,
9+
biddingTime: BigNumberish,
10+
isStoppable: boolean,
11+
): Promise<BlindAuction> {
12+
const contractFactory = await ethers.getContractFactory("BlindAuction");
13+
const contract = await contractFactory
14+
.connect(account)
15+
.deploy(account.getAddress(), tokenContract, biddingTime, isStoppable);
16+
await contract.waitForDeployment();
17+
return contract;
18+
}

0 commit comments

Comments
 (0)