-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathCrowdSale.sol
355 lines (307 loc) · 13 KB
/
CrowdSale.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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol";
import { IPermissioner } from "../Permissioner.sol";
import { IPToken } from "../IPToken.sol";
enum SaleState {
UNKNOWN,
RUNNING,
SETTLED,
FAILED
}
struct Sale {
IERC20Metadata auctionToken;
IERC20Metadata biddingToken;
address beneficiary;
//how many bidding tokens to collect
uint256 fundingGoal;
//how many auction tokens to sell
uint256 salesAmount;
//a timestamp
uint64 closingTime;
//can be address(0) if there are no rules to enforce on token actions
IPermissioner permissioner;
}
struct SaleInfo {
SaleState state;
uint256 total;
uint256 surplus;
bool claimed;
uint16 feeBp;
}
error BadDecimals();
error BadSalesAmount();
error BadSaleDuration();
error SaleAlreadyActive();
error SaleClosedForBids();
error BidTooLow();
error SaleNotFund(uint256);
error SaleNotConcluded();
error BadSaleState(SaleState expected, SaleState actual);
error AlreadyClaimed();
error FeesTooHigh();
/**
* @title CrowdSale
* @author molecule.to
* @notice a fixed price sales base contract
*/
contract CrowdSale is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20Metadata;
using FixedPointMathLib for uint256;
mapping(uint256 => Sale) internal _sales;
mapping(uint256 => SaleInfo) internal _saleInfo;
mapping(uint256 => mapping(address => uint256)) internal _contributions;
/**
* @notice currently configured fee cut expressed in basis points (1/10_000)
*/
uint16 public currentFeeBp = 0;
event Started(uint256 indexed saleId, address indexed issuer, Sale sale, uint16 feeBp);
event Settled(uint256 indexed saleId, uint256 totalBids, uint256 surplus);
/// @notice emitted when participants of the sale claim their tokens
event Claimed(uint256 indexed saleId, address indexed claimer, uint256 claimed, uint256 refunded);
event Bid(uint256 indexed saleId, address indexed bidder, uint256 amount);
event Failed(uint256 indexed saleId);
/// @notice emitted when sales owner / beneficiary claims `fundingGoal` `biddingTokens` after a successful sale
event ClaimedFundingGoal(uint256 indexed saleId);
/// @notice emitted when sales owner / beneficiary claims `salesAmount` `auctionTokens` after a non successful sale
event ClaimedAuctionTokens(uint256 indexed saleId);
event FeesUpdated(uint16 feeBp);
constructor() Ownable() { }
/**
* @notice This will only affect future auctions
* @param newFeeBp uint16 the new fee in basis points. Must be <= 50%
*/
function setCurrentFeesBp(uint16 newFeeBp) public onlyOwner {
if (newFeeBp > 5000) {
revert FeesTooHigh();
}
emit FeesUpdated(newFeeBp);
currentFeeBp = newFeeBp;
}
/**
* @notice bidding tokens can have arbitrary decimals, auctionTokens must be 18 decimals
* if no beneficiary is provided, the beneficiary will be set to msg.sender
* caller must approve `sale.fundingGoal` auctionTokens before calling this.
* @param sale the sale's base configuration.
* @return saleId
*/
function startSale(Sale calldata sale) public virtual returns (uint256 saleId) {
//[M-02]
if (sale.closingTime < block.timestamp || sale.closingTime > block.timestamp + 180 days) {
revert BadSaleDuration();
}
if (sale.auctionToken.decimals() != 18) {
revert BadDecimals();
}
//close to 0 cases lead to precision issues.Using 0.01 bidding tokens as minimium funding goal
if (sale.fundingGoal < 10 ** (sale.biddingToken.decimals() - 2) || sale.salesAmount < 0.5 ether) {
revert BadSalesAmount();
}
saleId = uint256(keccak256(abi.encode(sale)));
if (address(_sales[saleId].auctionToken) != address(0)) {
revert SaleAlreadyActive();
}
_sales[saleId] = sale;
_saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false, currentFeeBp);
sale.auctionToken.safeTransferFrom(msg.sender, address(this), sale.salesAmount);
_afterSaleStarted(saleId);
}
/**
* @return SaleInfo information about the sale
*/
function getSaleInfo(uint256 saleId) external view returns (SaleInfo memory) {
return _saleInfo[saleId];
}
/**
* @param saleId sale id
* @param contributor address
* @return uint256 the amount of bidding tokens `contributor` has bid into the sale
*/
function contribution(uint256 saleId, address contributor) external view returns (uint256) {
return _contributions[saleId][contributor];
}
/**
* @dev even though `auctionToken` is casted to `IPToken` this should still work with IPNFT agnostic tokens
* @param saleId the sale id
* @param biddingTokenAmount the amount of bidding tokens
* @param permission bytes are handed over to a configured permissioner contract. Set to 0x0 / "" / [] if not needed
*/
function placeBid(uint256 saleId, uint256 biddingTokenAmount, bytes calldata permission) public {
if (biddingTokenAmount == 0) {
revert BidTooLow();
}
Sale storage sale = _sales[saleId];
if (sale.fundingGoal == 0) {
revert SaleNotFund(saleId);
}
// @notice: while the general rule is that no bids aren't accepted past the sale's closing time
// it's still possible for derived contracts to fail a sale early by changing the sale's state
if (block.timestamp > sale.closingTime || _saleInfo[saleId].state != SaleState.RUNNING) {
revert SaleClosedForBids();
}
if (address(sale.permissioner) != address(0)) {
sale.permissioner.accept(IPToken(address(sale.auctionToken)), msg.sender, permission);
}
_bid(saleId, biddingTokenAmount);
}
/**
* @notice anyone can call this for the beneficiary.
* beneficiary must claim their respective proceeds by calling `claimResults` afterwards
* @param saleId the sale id
*/
function settle(uint256 saleId) public virtual nonReentrant {
Sale storage sale = _sales[saleId];
SaleInfo storage saleInfo = _saleInfo[saleId];
if (block.timestamp < sale.closingTime) {
revert SaleNotConcluded();
}
if (saleInfo.state != SaleState.RUNNING) {
revert BadSaleState(SaleState.RUNNING, saleInfo.state);
}
if (saleInfo.total < sale.fundingGoal) {
saleInfo.state = SaleState.FAILED;
emit Failed(saleId);
return;
}
saleInfo.state = SaleState.SETTLED;
saleInfo.surplus = saleInfo.total - sale.fundingGoal;
emit Settled(saleId, saleInfo.total, saleInfo.surplus);
_afterSaleSettled(saleId);
}
/**
* @notice [L-02] lets the auctioneer pull the results of a succeeded / failed crowdsale
* only callable once after the sale was settled
* this is callable by anonye
* @param saleId the sale id
*/
function claimResults(uint256 saleId) external {
SaleInfo storage saleInfo = _saleInfo[saleId];
if (saleInfo.claimed) {
revert AlreadyClaimed();
}
saleInfo.claimed = true;
Sale storage sale = _sales[saleId];
if (saleInfo.state == SaleState.SETTLED) {
uint256 claimableAmount = sale.fundingGoal;
if (saleInfo.feeBp > 0) {
uint256 saleFees = (saleInfo.feeBp * sale.fundingGoal) / 10_000;
claimableAmount -= saleFees;
sale.biddingToken.safeTransfer(owner(), saleFees);
}
//transfer funds to issuer / beneficiary
emit ClaimedFundingGoal(saleId);
sale.biddingToken.safeTransfer(sale.beneficiary, claimableAmount);
} else if (saleInfo.state == SaleState.FAILED) {
//return auction tokens
emit ClaimedAuctionTokens(saleId);
sale.auctionToken.safeTransfer(sale.beneficiary, sale.salesAmount);
} else {
revert BadSaleState(SaleState.SETTLED, saleInfo.state);
}
}
function _afterSaleSettled(uint256 saleId) internal virtual { }
/**
* @dev computes commitment ratio of bidder
*
* @param saleId sale id
* @param bidder bidder
* @return auctionTokens wei value of auction tokens to return
* @return refunds wei value of bidding tokens to return
*/
function getClaimableAmounts(uint256 saleId, address bidder) public view virtual returns (uint256 auctionTokens, uint256 refunds) {
SaleInfo storage saleInfo = _saleInfo[saleId];
uint256 biddingRatio = (saleInfo.total == 0) ? 0 : _contributions[saleId][bidder].divWadDown(saleInfo.total);
auctionTokens = biddingRatio.mulWadDown(_sales[saleId].salesAmount);
if (saleInfo.surplus != 0) {
refunds = biddingRatio.mulWadDown(saleInfo.surplus);
}
}
/**
* @dev even though `auctionToken` is casted to `IPToken` this should still work with IPNFT agnostic tokens
* @notice public method that refunds and lets user redeem their sales shares
* @param saleId the sale id
* @param permission. bytes are handed over to a configured permissioner contract
*/
function claim(uint256 saleId, bytes memory permission) external nonReentrant returns (uint256 auctionTokens, uint256 refunds) {
SaleState currentState = _saleInfo[saleId].state;
if (currentState == SaleState.FAILED) {
return claimFailed(saleId);
}
//[L-05]
if (currentState != SaleState.SETTLED) {
revert BadSaleState(SaleState.SETTLED, currentState);
}
Sale storage sales = _sales[saleId];
//we're not querying the permissioner if the sale has failed.
if (address(sales.permissioner) != address(0)) {
sales.permissioner.accept(IPToken(address(sales.auctionToken)), msg.sender, permission);
}
(auctionTokens, refunds) = getClaimableAmounts(saleId, msg.sender);
//a reentrancy won't have any effect after setting this to 0.
_contributions[saleId][msg.sender] = 0;
claim(saleId, auctionTokens, refunds);
}
/**
* @dev will send `tokenAmount` auction tokens and `refunds` bidding tokens to msg.sender
* This trusts the caller to have checked the amount
*
* @param saleId sale id
* @param tokenAmount amount of tokens to claim.
* @param refunds biddingTokens to refund
*/
function claim(uint256 saleId, uint256 tokenAmount, uint256 refunds) internal virtual {
//the sender has claimed already
if (tokenAmount == 0) {
return;
}
emit Claimed(saleId, msg.sender, tokenAmount, refunds);
if (refunds != 0) {
_sales[saleId].biddingToken.safeTransfer(msg.sender, refunds);
}
_claimAuctionTokens(saleId, tokenAmount);
}
/**
* @dev lets users claim back refunds when the sale has failed
*
* @param saleId sale id
* @return auctionTokens the amount of auction tokens claimed (0)
* @return refunds the amount of bidding tokens refunded
*/
function claimFailed(uint256 saleId) internal virtual returns (uint256 auctionTokens, uint256 refunds) {
uint256 _contribution = _contributions[saleId][msg.sender];
_contributions[saleId][msg.sender] = 0;
emit Claimed(saleId, msg.sender, 0, _contribution);
_sales[saleId].biddingToken.safeTransfer(msg.sender, _contribution);
return (0, _contribution);
}
/**
* @dev internal bid method
* increases bidder's contribution balance
* increases sale's bid total
*
* @param saleId sale id
* @param biddingTokenAmount the amount of tokens bid to the sale
*/
function _bid(uint256 saleId, uint256 biddingTokenAmount) internal virtual {
_saleInfo[saleId].total += biddingTokenAmount;
_contributions[saleId][msg.sender] += biddingTokenAmount;
_sales[saleId].biddingToken.safeTransferFrom(msg.sender, address(this), biddingTokenAmount);
emit Bid(saleId, msg.sender, biddingTokenAmount);
}
/**
* @dev overridden in LockingCrowdSale (will lock auction tokens in vested contract)
*/
function _claimAuctionTokens(uint256 saleId, uint256 tokenAmount) internal virtual {
_sales[saleId].auctionToken.safeTransfer(msg.sender, tokenAmount);
}
/**
* @dev allows us to emit different events per derived contract
*/
function _afterSaleStarted(uint256 saleId) internal virtual {
emit Started(saleId, msg.sender, _sales[saleId], _saleInfo[saleId].feeBp);
}
}