Skip to content

Commit 1b878a1

Browse files
committed
added dutch auction no refund contract
1 parent 01abe2a commit 1b878a1

File tree

3 files changed

+1328
-33
lines changed

3 files changed

+1328
-33
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
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 DutchAuctionSellingConfidentialERC20NoRefund 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+
// Amount of money to pay
189+
euint64 amountToTransfer = TFHE.mul(currentPricePerToken, newTokenAmount);
190+
191+
// Transfer money, and only if OK send the tokens
192+
euint64 transferredBalance = _handleTransfer(amountToTransfer);
193+
ebool transferOK = TFHE.eq(transferredBalance, amountToTransfer);
194+
195+
// Transfer tokens
196+
euint64 tokensToTransfer = TFHE.select(transferOK, newTokenAmount, TFHE.asEuint64(0));
197+
_handleTokenTransfer(tokensToTransfer);
198+
199+
// Update bid
200+
_updateBidInfo(tokensToTransfer, transferredBalance);
201+
202+
emit BidSubmitted(msg.sender, currentPricePerToken);
203+
}
204+
205+
/// @dev Helper function to handle token transfers
206+
function _handleTransfer(euint64 amountToTransfer) private returns (euint64) {
207+
euint64 balanceBefore = paymentToken.balanceOf(address(this));
208+
TFHE.allowTransient(amountToTransfer, address(paymentToken));
209+
paymentToken.transferFrom(msg.sender, address(this), amountToTransfer);
210+
euint64 balanceAfter = paymentToken.balanceOf(address(this));
211+
return TFHE.sub(balanceAfter, balanceBefore);
212+
}
213+
214+
/// @dev Helper function to handle refunds
215+
function _handleRefund(euint64 amountToTransfer) private {
216+
TFHE.allowTransient(amountToTransfer, address(paymentToken));
217+
paymentToken.transfer(msg.sender, amountToTransfer);
218+
}
219+
220+
/// @dev Helper function to handle token transfer
221+
function _handleTokenTransfer(euint64 amountToTransfer) private {
222+
TFHE.allowTransient(amountToTransfer, address(token));
223+
token.transfer(msg.sender, amountToTransfer);
224+
}
225+
226+
/// @dev Helper function to update bid information
227+
function _updateBidInfo(euint64 newTokenAmount, euint64 newPaidAmount) private {
228+
// Handle previous bid adjustments
229+
Bid storage userBid = bids[msg.sender];
230+
231+
if (TFHE.isInitialized(userBid.tokenAmount)) {
232+
bids[msg.sender].tokenAmount = TFHE.add(newTokenAmount, bids[msg.sender].tokenAmount);
233+
bids[msg.sender].paidAmount = TFHE.add(newPaidAmount, bids[msg.sender].paidAmount);
234+
} else {
235+
bids[msg.sender].tokenAmount = newTokenAmount;
236+
bids[msg.sender].paidAmount = newPaidAmount;
237+
}
238+
TFHE.allowThis(bids[msg.sender].tokenAmount);
239+
TFHE.allowThis(bids[msg.sender].paidAmount);
240+
TFHE.allow(bids[msg.sender].tokenAmount, msg.sender);
241+
TFHE.allow(bids[msg.sender].paidAmount, msg.sender);
242+
243+
// Update remaining tokens
244+
tokensLeft = TFHE.sub(tokensLeft, newTokenAmount);
245+
TFHE.allowThis(tokensLeft);
246+
TFHE.allow(tokensLeft, owner());
247+
}
248+
249+
/// @notice Claim tokens and refund for a bidder after auction ends
250+
/// @dev Transfers tokens to bidder and refunds excess payment based on final price
251+
function claimUserRefund() external onlyAfterAuctionEnds {
252+
Bid storage userBid = bids[msg.sender];
253+
254+
uint finalPrice = getPrice();
255+
euint64 finalPricePerToken = TFHE.asEuint64(finalPrice);
256+
euint64 finalCost = TFHE.mul(finalPricePerToken, userBid.tokenAmount);
257+
euint64 refundAmount = TFHE.sub(userBid.paidAmount, finalCost);
258+
259+
// Transfer refund
260+
_handleRefund(refundAmount);
261+
262+
// Clear the bid
263+
delete bids[msg.sender];
264+
}
265+
266+
/// @notice Claim proceeds for the seller after auction ends
267+
/// @dev Transfers all remaining tokens and payments to seller
268+
function claimSeller() external onlyOwner onlyAfterClaimsEnd {
269+
// Get the total amount of payment tokens in the contract
270+
euint64 contractPaymentBalance = paymentToken.balanceOf(address(this));
271+
euint64 contractAuctionBalance = token.balanceOf(address(this));
272+
273+
// Transfer all payment token and auction tokens to the seller
274+
TFHE.allowTransient(contractPaymentBalance, address(paymentToken));
275+
paymentToken.transfer(seller, contractPaymentBalance);
276+
277+
TFHE.allowTransient(contractAuctionBalance, address(token));
278+
token.transfer(seller, contractAuctionBalance);
279+
}
280+
281+
/// @notice Request decryption of remaining tokens
282+
/// @dev Only owner can request decryption
283+
function requestTokensLeftReveal() public onlyOwner {
284+
uint256[] memory cts = new uint256[](1);
285+
cts[0] = Gateway.toUint256(tokensLeft);
286+
Gateway.requestDecryption(cts, this.callbackUint64.selector, 0, block.timestamp + 100, false);
287+
}
288+
289+
/// @notice Callback function for 64-bit unsigned integer decryption
290+
/// @dev Only callable by the Gateway contract
291+
/// @param decryptedInput The decrypted 64-bit unsigned integer
292+
/// @return The decrypted value
293+
function callbackUint64(uint256, uint64 decryptedInput) public onlyGateway returns (uint64) {
294+
tokensLeftReveal = decryptedInput;
295+
return decryptedInput;
296+
}
297+
298+
/// @notice Cancel the auction and return tokens to seller
299+
/// @dev Only owner can cancel before auction ends
300+
function cancelAuction() external onlyOwner onlyBeforeEnd {
301+
TFHE.allowTransient(tokensLeft, address(token));
302+
303+
// Refund remaining tokens
304+
token.transfer(seller, tokensLeft);
305+
}
306+
307+
/// @notice Modifier to ensure function is called before auction ends
308+
/// @dev Reverts if called after the auction end time or if manually stopped
309+
modifier onlyBeforeEnd() {
310+
if (!auctionStart) revert AuctionNotStarted();
311+
if (block.timestamp >= expiresAt || manuallyStopped == true) revert TooLate(expiresAt);
312+
_;
313+
}
314+
315+
/// @notice Modifier to ensure function is called after auction ends
316+
/// @dev Reverts if called before the auction end time and called after claims time expire and not manually stopped
317+
modifier onlyAfterAuctionEnds() {
318+
if (!auctionStart) revert AuctionNotStarted();
319+
if (block.timestamp < expiresAt && manuallyStopped == false) revert TooEarly(expiresAt);
320+
if (block.timestamp >= claimsExpiresAt) revert TooLate(claimsExpiresAt);
321+
_;
322+
}
323+
324+
/// @notice Modifier to ensure function is called after refund claim period ends
325+
/// @dev Reverts if called before the auction refund claims period end time and not manually stopped
326+
modifier onlyAfterClaimsEnd() {
327+
if (!auctionStart) revert AuctionNotStarted();
328+
if (block.timestamp < claimsExpiresAt && manuallyStopped == false) revert TooEarly(claimsExpiresAt);
329+
_;
330+
}
331+
332+
/// @notice Get the user's current bid information
333+
/// @dev Returns the decrypted values of token amount and paid amount
334+
/// @return tokenAmount Amount of tokens bid for
335+
/// @return paidAmount Amount paid for the tokens
336+
function getUserBid() external view returns (euint64 tokenAmount, euint64 paidAmount) {
337+
Bid storage userBid = bids[msg.sender];
338+
return (userBid.tokenAmount, userBid.paidAmount);
339+
}
340+
}

0 commit comments

Comments
 (0)