Skip to content

Commit 01abe2a

Browse files
committed
added confidential dutch auction
1 parent c265a99 commit 01abe2a

File tree

6 files changed

+1470
-0
lines changed

6 files changed

+1470
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
import "fhevm/lib/TFHE.sol";
7+
import { ConfidentialERC20 } from "fhevm-contracts/contracts/token/ERC20/ConfidentialERC20.sol";
8+
import "@openzeppelin/contracts/access/Ownable2Step.sol";
9+
import "fhevm/config/ZamaFHEVMConfig.sol";
10+
import "fhevm/config/ZamaGatewayConfig.sol";
11+
import "fhevm/gateway/GatewayCaller.sol";
12+
13+
/// @title Dutch Auction for Selling Confidential ERC20 Tokens
14+
/// @notice Implements a Dutch auction mechanism for selling confidential ERC20 tokens
15+
/// @dev Uses FHEVM for handling encrypted values and transactions
16+
contract DutchAuctionSellingConfidentialERC20 is
17+
SepoliaZamaFHEVMConfig,
18+
SepoliaZamaGatewayConfig,
19+
GatewayCaller,
20+
Ownable2Step
21+
{
22+
/// @notice The ERC20 token being auctioned
23+
ConfidentialERC20 public immutable token;
24+
/// @notice The token used for payments
25+
ConfidentialERC20 public immutable paymentToken;
26+
/// @notice Encrypted amount of tokens remaining in the auction
27+
euint64 private tokensLeft;
28+
29+
/// @notice Address of the seller
30+
address payable public immutable seller;
31+
/// @notice Initial price per token
32+
uint64 public immutable startingPrice;
33+
/// @notice Rate at which the price decreases
34+
uint64 public immutable discountRate;
35+
/// @notice Timestamp when the auction starts
36+
uint256 public immutable startAt;
37+
/// @notice Timestamp when the auction ends
38+
uint256 public immutable expiresAt;
39+
/// @notice Timestamp when the auction refund claims end
40+
uint256 public immutable claimsExpiresAt;
41+
/// @notice Minimum price per token
42+
uint64 public immutable reservePrice;
43+
/// @notice Total amount of tokens being auctioned
44+
uint64 public immutable amount;
45+
/// @notice Flag indicating if the auction has started
46+
bool public auctionStart = false;
47+
48+
/// @notice Flag to determine if the auction can be stopped manually
49+
bool public stoppable;
50+
51+
/// @notice Flag to check if the auction has been manually stopped
52+
bool public manuallyStopped = false;
53+
54+
/// @notice Decrypted value of remaining tokens
55+
uint64 public tokensLeftReveal;
56+
57+
/// @notice Structure to store bid information
58+
/// @param tokenAmount Amount of tokens bid for
59+
/// @param paidAmount Amount paid for the tokens
60+
struct Bid {
61+
euint64 tokenAmount;
62+
euint64 paidAmount;
63+
}
64+
/// @notice Mapping of addresses to their bids
65+
mapping(address => Bid) public bids;
66+
67+
/// @notice Emitted when a bid is submitted
68+
/// @param buyer Address of the bidder
69+
/// @param pricePerToken Price per token at the time of bid
70+
event BidSubmitted(address indexed buyer, uint pricePerToken);
71+
72+
/// @notice Error thrown when a function is called too early
73+
/// @dev Includes the time when the function can be called
74+
error TooEarly(uint256 time);
75+
76+
/// @notice Error thrown when a function is called too late
77+
/// @dev Includes the time after which the function cannot be called
78+
error TooLate(uint256 time);
79+
80+
/// @notice Error thrown when trying to start an already started auction
81+
error AuctionAlreadyStarted();
82+
/// @notice Error thrown when trying to interact with an unstarted auction
83+
error AuctionNotStarted();
84+
85+
/// @notice Creates a new Dutch auction contract
86+
/// @param _startingPrice Initial price per token
87+
/// @param _discountRate Rate at which price decreases
88+
/// @param _token Address of token being auctioned
89+
/// @param _paymentToken Address of token used for payment
90+
/// @param _amount Total amount of tokens to auction
91+
/// @param _reservePrice Minimum price per token
92+
/// @param _biddingTime Duration of the auction in seconds
93+
/// @param _isStoppable Whether the auction can be stopped manually
94+
constructor(
95+
uint64 _startingPrice,
96+
uint64 _discountRate,
97+
ConfidentialERC20 _token,
98+
ConfidentialERC20 _paymentToken,
99+
uint64 _amount,
100+
uint64 _reservePrice,
101+
uint256 _biddingTime,
102+
bool _isStoppable
103+
) Ownable(msg.sender) {
104+
seller = payable(msg.sender);
105+
startingPrice = _startingPrice;
106+
discountRate = _discountRate;
107+
startAt = block.timestamp;
108+
expiresAt = block.timestamp + _biddingTime;
109+
claimsExpiresAt = block.timestamp + 3 * _biddingTime; // could choose a potentially different design
110+
reservePrice = _reservePrice;
111+
stoppable = _isStoppable;
112+
113+
require(_startingPrice >= _discountRate * _biddingTime + _reservePrice, "Starting price too low");
114+
require(_reservePrice > 0, "Reserve price must be greater than zero");
115+
require(_startingPrice > _reservePrice, "Starting price must be greater than reserve price");
116+
117+
amount = _amount; // initial amount should be known
118+
tokensLeft = TFHE.asEuint64(_amount);
119+
tokensLeftReveal = _amount;
120+
token = _token;
121+
paymentToken = _paymentToken;
122+
TFHE.allowThis(tokensLeft);
123+
TFHE.allow(tokensLeft, owner());
124+
}
125+
126+
/// @notice Initializes the auction by transferring tokens from seller
127+
/// @dev Can only be called once by the owner
128+
function initialize() external onlyOwner {
129+
if (auctionStart) revert AuctionAlreadyStarted();
130+
131+
euint64 encAmount = TFHE.asEuint64(amount);
132+
133+
TFHE.allowTransient(encAmount, address(token));
134+
135+
// Transfer tokens from seller to the auction contract
136+
token.transferFrom(msg.sender, address(this), encAmount);
137+
138+
euint64 balanceAfter = token.balanceOf(address(this));
139+
140+
ebool encAuctionStart = TFHE.select(TFHE.ge(balanceAfter, amount), TFHE.asEbool(true), TFHE.asEbool(false));
141+
142+
uint256[] memory cts = new uint256[](1);
143+
cts[0] = Gateway.toUint256(encAuctionStart);
144+
Gateway.requestDecryption(cts, this.callbackBool.selector, 0, block.timestamp + 100, false);
145+
}
146+
147+
/// @notice Callback function for boolean decryption
148+
/// @dev Only callable by the Gateway contract
149+
/// @param encAuctionStart The decrypted boolean
150+
/// @return The decrypted value
151+
function callbackBool(uint256, bool encAuctionStart) public onlyGateway returns (bool) {
152+
auctionStart = encAuctionStart;
153+
return encAuctionStart;
154+
}
155+
156+
/// @notice Gets the current price per token
157+
/// @dev Price decreases linearly over time until it reaches reserve price
158+
/// @return Current price per token in payment token units
159+
function getPrice() public view returns (uint64) {
160+
if (block.timestamp >= expiresAt) {
161+
return reservePrice;
162+
}
163+
164+
uint256 timeElapsed = block.timestamp - startAt;
165+
uint256 discount = discountRate * timeElapsed;
166+
uint64 currentPrice = startingPrice > uint64(discount) ? startingPrice - uint64(discount) : 0;
167+
return currentPrice > reservePrice ? currentPrice : reservePrice;
168+
}
169+
170+
/// @notice Manually stop the auction
171+
/// @dev Can only be called by the owner and if the auction is stoppable
172+
function stop() external onlyOwner {
173+
require(stoppable);
174+
manuallyStopped = true;
175+
}
176+
177+
/// @notice Submit a bid for tokens
178+
/// @dev Handles bid logic including refunds from previous bids
179+
/// @param encryptedValue Encrypted amount of tokens to bid for
180+
/// @param inputProof Zero-knowledge proof for the encrypted input
181+
function bid(einput encryptedValue, bytes calldata inputProof) external onlyBeforeEnd {
182+
euint64 newTokenAmount = TFHE.asEuint64(encryptedValue, inputProof);
183+
uint64 currentPricePerToken = getPrice();
184+
185+
// Calculate how many new tokens can be bought
186+
newTokenAmount = TFHE.min(newTokenAmount, tokensLeft);
187+
188+
// Handle previous bid adjustments
189+
Bid storage userBid = bids[msg.sender];
190+
191+
if (TFHE.isInitialized(userBid.tokenAmount)) {
192+
// Previous bid exists - calculate total tokens bought and amount paid
193+
euint64 totalTokenAmount = TFHE.add(newTokenAmount, userBid.tokenAmount);
194+
euint64 oldPaidAmount = userBid.paidAmount;
195+
196+
// Calculate cost of total token at current price
197+
euint64 totalCostAtNewPrice = TFHE.mul(currentPricePerToken, totalTokenAmount);
198+
199+
// Calculate difference between paid already and new price
200+
ebool totalBiggerOld = TFHE.ge(totalCostAtNewPrice, oldPaidAmount);
201+
euint64 paidDiff = TFHE.sub(totalCostAtNewPrice, oldPaidAmount);
202+
euint64 amountToTransfer = TFHE.select(totalBiggerOld, paidDiff, TFHE.asEuint64(0));
203+
euint64 amountToRefund = TFHE.sub(amountToTransfer, paidDiff);
204+
205+
// Transfer money, and only if OK send the tokens and process refund
206+
euint64 transferredBalance = _handleTransfer(amountToTransfer);
207+
ebool transferOK = TFHE.eq(transferredBalance, amountToTransfer);
208+
209+
// Transfer tokens and refund
210+
euint64 finalTokenAmountToTransfer = TFHE.select(transferOK, newTokenAmount, TFHE.asEuint64(0));
211+
_handleTokenTransfer(finalTokenAmountToTransfer);
212+
213+
euint64 finalAmountToRefund = TFHE.select(transferOK, amountToRefund, TFHE.asEuint64(0));
214+
_handleRefund(finalAmountToRefund);
215+
216+
// Update bid
217+
euint64 finalTotalTokens = TFHE.select(transferOK, totalTokenAmount, userBid.tokenAmount);
218+
euint64 finalTotalCost = TFHE.select(transferOK, totalCostAtNewPrice, userBid.paidAmount);
219+
_updateBidInfo(finalTotalTokens, finalTotalCost, finalTokenAmountToTransfer);
220+
} else {
221+
// Amount of money to pay
222+
euint64 amountToTransfer = TFHE.mul(currentPricePerToken, newTokenAmount);
223+
224+
// Transfer money, and only if OK send the tokens
225+
euint64 transferredBalance = _handleTransfer(amountToTransfer);
226+
ebool transferOK = TFHE.eq(transferredBalance, amountToTransfer);
227+
228+
// Transfer tokens
229+
euint64 tokensToTransfer = TFHE.select(transferOK, newTokenAmount, TFHE.asEuint64(0));
230+
_handleTokenTransfer(tokensToTransfer);
231+
232+
// Update bid
233+
_updateBidInfo(tokensToTransfer, transferredBalance, tokensToTransfer);
234+
}
235+
236+
emit BidSubmitted(msg.sender, currentPricePerToken);
237+
}
238+
239+
/// @dev Helper function to handle token transfers
240+
function _handleTransfer(euint64 amountToTransfer) private returns (euint64) {
241+
euint64 balanceBefore = paymentToken.balanceOf(address(this));
242+
TFHE.allowTransient(amountToTransfer, address(paymentToken));
243+
paymentToken.transferFrom(msg.sender, address(this), amountToTransfer);
244+
euint64 balanceAfter = paymentToken.balanceOf(address(this));
245+
return TFHE.sub(balanceAfter, balanceBefore);
246+
}
247+
248+
/// @dev Helper function to handle refunds
249+
function _handleRefund(euint64 amountToTransfer) private {
250+
TFHE.allowTransient(amountToTransfer, address(paymentToken));
251+
paymentToken.transfer(msg.sender, amountToTransfer);
252+
}
253+
254+
/// @dev Helper function to handle token transfer
255+
function _handleTokenTransfer(euint64 amountToTransfer) private {
256+
TFHE.allowTransient(amountToTransfer, address(token));
257+
token.transfer(msg.sender, amountToTransfer);
258+
}
259+
260+
/// @dev Helper function to update bid information
261+
function _updateBidInfo(euint64 totalTokenAmount, euint64 totalPaidAmount, euint64 newTokenAmount) private {
262+
bids[msg.sender].tokenAmount = totalTokenAmount;
263+
bids[msg.sender].paidAmount = totalPaidAmount;
264+
TFHE.allowThis(bids[msg.sender].tokenAmount);
265+
TFHE.allowThis(bids[msg.sender].paidAmount);
266+
TFHE.allow(bids[msg.sender].tokenAmount, msg.sender);
267+
TFHE.allow(bids[msg.sender].paidAmount, msg.sender);
268+
269+
// Update remaining tokens
270+
tokensLeft = TFHE.sub(tokensLeft, newTokenAmount);
271+
TFHE.allowThis(tokensLeft);
272+
TFHE.allow(tokensLeft, owner());
273+
}
274+
275+
/// @notice Claim tokens and refund for a bidder after auction ends
276+
/// @dev Transfers tokens to bidder and refunds excess payment based on final price
277+
function claimUserRefund() external onlyAfterAuctionEnds {
278+
Bid storage userBid = bids[msg.sender];
279+
280+
uint finalPrice = getPrice();
281+
euint64 finalPricePerToken = TFHE.asEuint64(finalPrice);
282+
euint64 finalCost = TFHE.mul(finalPricePerToken, userBid.tokenAmount);
283+
euint64 refundAmount = TFHE.sub(userBid.paidAmount, finalCost);
284+
285+
// Transfer refund
286+
_handleRefund(refundAmount);
287+
288+
// Clear the bid
289+
delete bids[msg.sender];
290+
}
291+
292+
/// @notice Claim proceeds for the seller after auction ends
293+
/// @dev Transfers all remaining tokens and payments to seller
294+
function claimSeller() external onlyOwner onlyAfterClaimsEnd {
295+
// Get the total amount of payment tokens in the contract
296+
euint64 contractPaymentBalance = paymentToken.balanceOf(address(this));
297+
euint64 contractAuctionBalance = token.balanceOf(address(this));
298+
299+
// Transfer all payment token and auction tokens to the seller
300+
TFHE.allowTransient(contractPaymentBalance, address(paymentToken));
301+
paymentToken.transfer(seller, contractPaymentBalance);
302+
303+
TFHE.allowTransient(contractAuctionBalance, address(token));
304+
token.transfer(seller, contractAuctionBalance);
305+
}
306+
307+
/// @notice Request decryption of remaining tokens
308+
/// @dev Only owner can request decryption
309+
function requestTokensLeftReveal() public onlyOwner {
310+
uint256[] memory cts = new uint256[](1);
311+
cts[0] = Gateway.toUint256(tokensLeft);
312+
Gateway.requestDecryption(cts, this.callbackUint64.selector, 0, block.timestamp + 100, false);
313+
}
314+
315+
/// @notice Callback function for 64-bit unsigned integer decryption
316+
/// @dev Only callable by the Gateway contract
317+
/// @param decryptedInput The decrypted 64-bit unsigned integer
318+
/// @return The decrypted value
319+
function callbackUint64(uint256, uint64 decryptedInput) public onlyGateway returns (uint64) {
320+
tokensLeftReveal = decryptedInput;
321+
return decryptedInput;
322+
}
323+
324+
/// @notice Cancel the auction and return tokens to seller
325+
/// @dev Only owner can cancel before auction ends
326+
function cancelAuction() external onlyOwner onlyBeforeEnd {
327+
TFHE.allowTransient(tokensLeft, address(token));
328+
329+
// Refund remaining tokens
330+
token.transfer(seller, tokensLeft);
331+
}
332+
333+
/// @notice Modifier to ensure function is called before auction ends
334+
/// @dev Reverts if called after the auction end time or if manually stopped
335+
modifier onlyBeforeEnd() {
336+
if (!auctionStart) revert AuctionNotStarted();
337+
if (block.timestamp >= expiresAt || manuallyStopped == true) revert TooLate(expiresAt);
338+
_;
339+
}
340+
341+
/// @notice Modifier to ensure function is called after auction ends
342+
/// @dev Reverts if called before the auction end time and called after claims time expire and not manually stopped
343+
modifier onlyAfterAuctionEnds() {
344+
if (!auctionStart) revert AuctionNotStarted();
345+
if (block.timestamp < expiresAt && manuallyStopped == false) revert TooEarly(expiresAt);
346+
if (block.timestamp >= claimsExpiresAt) revert TooLate(claimsExpiresAt);
347+
_;
348+
}
349+
350+
/// @notice Modifier to ensure function is called after refund claim period ends
351+
/// @dev Reverts if called before the auction refund claims period end time and not manually stopped
352+
modifier onlyAfterClaimsEnd() {
353+
if (!auctionStart) revert AuctionNotStarted();
354+
if (block.timestamp < claimsExpiresAt && manuallyStopped == false) revert TooEarly(claimsExpiresAt);
355+
_;
356+
}
357+
358+
/// @notice Get the user's current bid information
359+
/// @dev Returns the decrypted values of token amount and paid amount
360+
/// @return tokenAmount Amount of tokens bid for
361+
/// @return paidAmount Amount paid for the tokens
362+
function getUserBid() external view returns (euint64 tokenAmount, euint64 paidAmount) {
363+
Bid storage userBid = bids[msg.sender];
364+
return (userBid.tokenAmount, userBid.paidAmount);
365+
}
366+
}

0 commit comments

Comments
 (0)