-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathDutchAuction.sol
320 lines (262 loc) · 12.1 KB
/
DutchAuction.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title DutchAuction
* @notice A Dutch auction selling multiple identical items.
* @dev
* By default the price starts high and decreases over time with a linear curve. But this can be changed by overriding
* `currentPrice()` to implement any custom price curve, including a reverse dutch auction(where price starts low and
* increases over time).
* - The price decreases from `startPrice` to `floorPrice` over `duration`.
* - Buyers can purchase at the current price until inventory = 0 or time runs out.
* - Once time runs out or inventory hits zero, the auction is considered finalized.
* - If inventory remains after time ends, the seller can reclaim them via `withdrawUnsoldAssets()`.
*
* To use this contract, you must:
* 1. Provide an implementation of `_transferAssetToBuyer(address buyer, uint256 quantity)` that transfers the
* auctioned assets (e.g. NFTs) to the buyer.
* 2. Provide an implementation of `_withdrawUnsoldAssets(address seller, uint256 quantity)` that transfers the
* unsold assets back to the seller(if not all assets are sold).
* 3. Optionally override `_beforeBuy` or `_afterBuy` to implement custom bidding logic such as
* whitelisting or additional checks.
*/
abstract contract DutchAuction is ReentrancyGuard {
/// @dev The address of the seller
address internal immutable seller;
/// @dev Timestamp (in seconds) at which the auction starts
uint256 internal immutable startTime;
/// @dev The duration (in seconds) of the auction
uint256 internal immutable duration;
/// @dev The initial price at `startTime`
uint256 internal immutable startPrice;
/// @dev The lowest possible price at the end of `duration`
uint256 internal immutable floorPrice;
/// @dev The number of identical items available for sale
uint256 private inventory;
/// @notice Emitted when a new auction is started
/// @param seller The address of the seller
/// @param startPrice The initial price per item
/// @param floorPrice The minimum possible price per item
/// @param startTime The timestamp when the auction begins
/// @param duration The length of the auction in seconds
/// @param inventory The total number of items available for sale
event AuctionCreated(
address indexed seller,
uint256 startPrice,
uint256 floorPrice,
uint256 startTime,
uint256 duration,
uint256 inventory
);
/// @notice Emitted when items are purchased from the auction
/// @param buyer The address of the buyer
/// @param quantity The number of items purchased
/// @param totalPaid The total amount of ETH paid for the items
event Purchased(address indexed buyer, uint256 quantity, uint256 totalPaid);
/// @notice Emitted when the seller withdraws auction proceeds
/// @param recipient The address receiving the funds
/// @param amount The amount of ETH withdrawn
event FundsWithdrawn(address indexed recipient, uint256 amount);
/// @notice Emitted when unsold items are withdrawn by the seller
/// @param seller The address of the seller
/// @param quantity The number of unsold items withdrawn
event UnsoldAssetsWithdrawn(address indexed seller, uint256 quantity);
/// @dev Thrown when trying to interact with an auction before its start time
error AuctionNotStarted();
/// @dev Thrown when trying to interact with an auction that has ended (due to time or sold out)
error AuctionEnded();
/// @dev Thrown when insufficient ETH is sent to cover the purchase
/// @param sent The amount of ETH sent
/// @param required The amount of ETH required
error InsufficientAmount(uint256 sent, uint256 required);
/// @dev Thrown when attempting to purchase an invalid quantity of items
/// @param quantity The requested quantity
/// @param available The available inventory
error InvalidQuantity(uint256 quantity, uint256 available);
/// @dev Thrown when trying to withdraw unsold assets before auction has ended
error AuctionNotEnded();
/// @dev Thrown when trying to withdraw unsold assets when all items were sold
error NoUnsoldAssetsToWithdraw();
/// @dev Thrown when auction duration is set to zero
error InvalidDuration();
/// @dev Thrown when start time is set to a past timestamp
/// @param startTime The specified start time
/// @param blockTimestamp The current block timestamp
error StartTimeInPast(uint256 startTime, uint256 blockTimestamp);
/// @dev Thrown when trying to create an auction with zero items
error InvalidInventory();
// -------------------------
// Modifiers
// -------------------------
/// @dev Auction is considered active during [startTime, endTime] and inventory > 0
modifier auctionActive() {
if (block.timestamp < startTime) revert AuctionNotStarted();
if (isFinished()) revert AuctionEnded();
_;
}
// -------------------------
// Constructor
// -------------------------
/**
* @param _seller The address of the seller
* @param _startPrice Price at start time (per item)
* @param _floorPrice The minimum price at the end (per item)
* @param _startTime When the auction starts
* @param _duration How long it lasts
* @param _inventory How many identical items are for sale
*/
constructor(
address _seller,
uint256 _startPrice,
uint256 _floorPrice,
uint256 _startTime,
uint256 _duration,
uint256 _inventory
) {
if (_duration == 0) revert InvalidDuration();
if (_startTime < block.timestamp) revert StartTimeInPast(_startTime, block.timestamp);
if (_inventory == 0) revert InvalidInventory();
seller = _seller;
startPrice = _startPrice;
floorPrice = _floorPrice;
startTime = _startTime;
duration = _duration;
inventory = _inventory;
emit AuctionCreated(seller, startPrice, floorPrice, startTime, duration, inventory);
}
// -------------------------
// Public / External
// -------------------------
/**
* @notice Buy `quantity` items at the current price.
* @dev The cost is `quantity * currentPrice()`.
* If more ETH than required is sent, excess is refunded.
* @param quantity The number of items to buy
*/
function buy(uint256 quantity) external payable auctionActive nonReentrant {
if (quantity == 0 || quantity > inventory) revert InvalidQuantity(quantity, inventory);
uint256 pricePerItem = currentPrice();
uint256 totalCost = pricePerItem * quantity;
if (msg.value < totalCost) revert InsufficientAmount(msg.value, totalCost);
_beforeBuy(msg.sender, quantity, pricePerItem, msg.value);
// Reduce inventory
inventory -= quantity;
uint256 excess = msg.value - totalCost;
if (excess > 0) {
(bool refundSuccess,) = payable(msg.sender).call{value: excess}("");
require(refundSuccess, "Refund failed");
}
// Transfer assets to buyer
_transferAssetToBuyer(msg.sender, quantity);
_afterBuy(msg.sender, quantity, pricePerItem, totalCost);
emit Purchased(msg.sender, quantity, totalCost);
}
/**
* @notice Send all funds in the contract to the seller.
* @dev By default, this will send all funds to the seller.
* It is safe to send all funds, since items are purchased immediately, so no bids are left outstanding.
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning
* them)
* When overriding, make sure to add necessary access control.
*/
function withdrawSellerProceeds() external virtual {
uint256 amount = address(this).balance;
(bool success,) = payable(seller).call{value: amount}("");
require(success, "Transfer failed");
emit FundsWithdrawn(seller, amount);
}
/**
* @notice Send unsold assets(if any) back to the seller after the auction ends.
* @dev This can only be done if the auction ended due to time running out and inventory still > 0.
* Override to implement custom logic if necessary (e.g. sending the assets to a different address)
* When overriding, make sure to add necessary access control.
*/
function withdrawUnsoldAssets() external virtual {
if (!isFinished()) revert AuctionNotEnded();
uint256 remaining = inventory;
if (remaining == 0) revert NoUnsoldAssetsToWithdraw(); // nothing to withdraw if sold out
// Set inventory to 0 because we're moving them out of the contract
inventory = 0;
// Implementer handles the actual asset transfer to the seller
_withdrawUnsoldAssets(seller, remaining);
emit UnsoldAssetsWithdrawn(seller, remaining);
}
// -------------------------
// View Functions
// -------------------------
/**
* @notice Gets the current price per item at the current timestamp.
* @dev By default, the price is a linear decrease from `startPrice` to `floorPrice` over `duration`.
* Override to implement custom curve, like exponential decay or even reverse dutch auction(where price starts
* low and increases over time)
* @return The current price per item.
*/
function currentPrice() public view virtual returns (uint256) {
if (block.timestamp <= startTime) {
return startPrice;
}
uint256 endTime = getEndTime();
if (block.timestamp >= endTime) {
return floorPrice;
}
return floorPrice + ((startPrice - floorPrice) * (endTime - block.timestamp)) / duration;
}
function getSeller() public view returns (address) {
return seller;
}
function getStartTime() public view returns (uint256) {
return startTime;
}
function getStartPrice() public view returns (uint256) {
return startPrice;
}
function getFloorPrice() public view returns (uint256) {
return floorPrice;
}
function getInventory() public view returns (uint256) {
return inventory;
}
function getEndTime() public view returns (uint256) {
return startTime + duration;
}
function isFinished() public view returns (bool) {
return block.timestamp > getEndTime() || inventory == 0;
}
// -------------------------
// Hooks for Extension
// -------------------------
/**
* @dev Hook called before processing a buy.
* @param buyer_ The buyer address
* @param quantity The number of items the buyer wants
* @param pricePerItem The current price per item
* @param amountPaid The total amount sent by the buyer
*/
function _beforeBuy(address buyer_, uint256 quantity, uint256 pricePerItem, uint256 amountPaid) internal virtual {
// No-op by default
}
/**
* @dev Hook called after a successful buy.
* @param buyer_ The buyer address
* @param quantity The number of items bought
* @param pricePerItem The price per item for this purchase
* @param totalCost The total cost paid by the buyer
*/
function _afterBuy(address buyer_, uint256 quantity, uint256 pricePerItem, uint256 totalCost) internal virtual {
// No-op by default
}
/**
* @dev MUST be implemented to transfer `quantity` items of the asset to `buyer_`.
* It is recommended that assets are escrowed in the contract and transferred to the buyer here.
* @param buyer_ The buyer's address
* @param quantity The quantity of items to transfer
*/
function _transferAssetToBuyer(address buyer_, uint256 quantity) internal virtual;
/**
* @dev MUST be implemented to transfer unsold items back to the seller after the auction ends.
* @param seller_ The seller's address
* @param quantity The quantity of unsold items
*/
function _withdrawUnsoldAssets(address seller_, uint256 quantity) internal virtual;
}