-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathForgottenRunesWarriorsMinter.sol
631 lines (539 loc) · 20 KB
/
ForgottenRunesWarriorsMinter.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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
import '@openzeppelin/contracts/security/Pausable.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol';
import './interfaces/IWETH.sol';
import './interfaces/IForgottenRunesWarriorsGuild.sol';
/**
* @dev This implements the minter of the Forgotten Runes Warriors Guild. They are {ERC721} tokens.
*/
contract ForgottenRunesWarriorsMinter is Ownable, Pausable, ReentrancyGuard {
/// @notice The start timestamp for the Dutch Auction (DA) sale and price
uint256 public daStartTime =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice The start timestamp for mintlisters
/// @dev This is the end of DA phase. No more DA bids when this is hit
uint256 public mintlistStartTime =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice The start timestamp for the public sale
uint256 public publicStartTime =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice The start timestamp for the claims
uint256 public claimsStartTime =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice The start timestamp for self refunds
uint256 public selfRefundsStartTime =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice The main Merkle root
bytes32 public mintlist1MerkleRoot;
/// @notice The secondary Merkle root
/// @dev Having a backup merkle root lets us atomically update the merkletree without downtime on the frontend
bytes32 public mintlist2MerkleRoot;
/// @notice The claimslist Merkle root
bytes32 public claimlistMerkleRoot;
/// @notice The address of the Warriors contract
IForgottenRunesWarriorsGuild public warriors;
/// @notice The address of the vault
address public vault;
/// @notice The address of the WETH contract
address public weth;
/// @notice The start price of the DA
uint256 public startPrice = 2.5 ether;
/// @notice The lowest price of the DA
uint256 public lowestPrice = 0.6 ether;
/// @notice The length of time for the price curve in the DA
uint256 public daPriceCurveLength = 380 minutes;
/// @notice The interval of time in which the price steps down
uint256 public daDropInterval = 10 minutes;
/// @notice The final price of the DA. Will be updated when DA is over and then used for subsequent phases
uint256 public finalPrice = 2.5 ether;
/// @notice An array of the addresses of the DA minters
/// @dev An entry is created for every da minting tx, so the same minter address is quite likely to appear more than once
address[] public daMinters;
/// @notice Tracks the total amount paid by a given address in the DA
mapping(address => uint256) public daAmountPaid;
/// @notice Tracks the total amount refunded to a given address for the DA
mapping(address => uint256) public daAmountRefunded;
/// @notice Tracks the total count of NFTs minted by a given address in the DA
mapping(address => uint256) public daNumMinted;
/// @notice Tracks if a given address minted in the mintlist
mapping(address => bool) public mintlistMinted;
/// @notice Tracks the total count of NFTs claimed by a given address
mapping(address => bool) public claimlistMinted;
/// @notice The total number of tokens reserved for the DA phase
uint256 public maxDaSupply = 8000;
/// @notice Tracks the total count of NFTs sold (vs. freebies)
uint256 public numSold;
/// @notice Tracks the total count of NFTs for sale
uint256 public maxForSale = 14190;
/// @notice Tracks the total count of NFTs claimed for free
uint256 public numClaimed;
/// @notice Tracks the total count of NFTs that can be claimed
/// @dev While we will have a merkle root set for this group, putting a hard cap helps limit the damage of any problems with an overly-generous merkle tree
uint256 public maxForClaim = 1100;
/**
* @dev Create the contract and set the initial baseURI
* @param _warriors address the initial warriors contract address
*/
constructor(IForgottenRunesWarriorsGuild _warriors, address _weth) {
setWarriorsAddress(_warriors);
setWethAddress(_weth);
setVaultAddress(msg.sender);
}
/*
* Timeline:
*
* bidSummon : |------------|
* mintlistSummon : |------------|-------------------------------------|
* publicSummon : |------------|------------------------|
* claimSummon : |------------|-----------|
* teamSummon : |---------------------------------------------------------------|
*/
/**
* @notice Mint a Warrior in the Dutch Auction phase
* @param numWarriors uint256 of the number of warriors you're trying to mint
*/
function bidSummon(uint256 numWarriors)
external
payable
nonReentrant
whenNotPaused
{
require(numSold < maxDaSupply, 'Auction sold out');
require(numSold + numWarriors <= maxDaSupply, 'Not enough remaining');
require(daStarted(), 'Auction not started');
require(!mintlistStarted(), 'Auction phase over');
require(
numWarriors > 0 && numWarriors <= 20,
'You can summon no more than 20 Warriors at a time'
);
uint256 currentPrice = currentDaPrice();
require(
msg.value >= (currentPrice * numWarriors),
'Ether value sent is not sufficient'
);
daMinters.push(msg.sender);
daAmountPaid[msg.sender] += msg.value;
daNumMinted[msg.sender] += numWarriors;
numSold += numWarriors;
if (numSold == maxDaSupply) {
// optimistic: save gas by not setting on every mint, but will
// require manual `setFinalPrice` before refunds if da falls short
finalPrice = currentPrice;
}
for (uint256 i = 0; i < numWarriors; i++) {
_mint(msg.sender);
}
}
/**
* @notice Mint a Warrior in the mintlist phase (paid)
* @param _merkleProof bytes32[] your proof of being able to mint
*/
function mintlistSummon(bytes32[] calldata _merkleProof)
external
payable
nonReentrant
whenNotPaused
{
require(numSold < maxForSale, 'Sold out');
require(mintlistStarted(), 'Mintlist phase not started');
require(msg.value == finalPrice, 'Ether value incorrect');
// verify didn't already mint
require(mintlistMinted[msg.sender] == false, 'Already minted');
mintlistMinted[msg.sender] = true;
// verify mintlist merkle
bytes32 node = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(_merkleProof, mintlist1MerkleRoot, node) ||
MerkleProof.verify(_merkleProof, mintlist2MerkleRoot, node),
'Invalid proof'
);
numSold += 1;
_mint(msg.sender);
}
/**
* @notice Mint a Warrior in the Public phase (paid)
* @param numWarriors uint256 of the number of warriors you're trying to mint
*/
function publicSummon(uint256 numWarriors)
external
payable
nonReentrant
whenNotPaused
{
require(numSold < maxForSale, 'Sold out');
require(numSold + numWarriors <= maxForSale, 'Not enough remaining');
require(publicStarted(), 'Public sale not started');
require(
numWarriors > 0 && numWarriors <= 20,
'You can summon no more than 20 Warriors at a time'
);
require(
msg.value == (finalPrice * numWarriors),
'Ether value sent is incorrect'
);
numSold += numWarriors;
for (uint256 i = 0; i < numWarriors; i++) {
_mint(msg.sender);
}
}
/**
* @dev claim a warrior for free if you're in the claimlist
* @param _merkleProof bytes32[] the proof that you're eligible to mint here
*/
function claimSummon(bytes32[] calldata _merkleProof)
external
nonReentrant
whenNotPaused
{
require(numClaimed < maxForClaim, 'No more claims');
require(claimsStarted(), 'Claim phase not started');
// verify didn't already claim
require(claimlistMinted[msg.sender] == false, 'Already claimed');
claimlistMinted[msg.sender] = true;
// verify claimlist
bytes32 node = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(_merkleProof, claimlistMerkleRoot, node),
'Invalid proof'
);
numClaimed += 1;
_mint(msg.sender);
}
/**
* @notice Mint a Warrior (owner only)
* @param recipient address the address of the recipient
* @param count uint256 of the number of warriors you're trying to mint
*/
function teamSummon(address recipient, uint256 count) external onlyOwner {
require(address(recipient) != address(0), 'address req');
for (uint256 i = 0; i < count; i++) {
_mint(recipient);
}
}
function _mint(address recipient) private {
warriors.mint(recipient);
}
/*
* View utilities
*/
/**
* @notice returns the current dutch auction price
*/
function currentDaPrice() public view returns (uint256) {
if (!daStarted()) {
return startPrice;
}
if (block.timestamp >= daStartTime + daPriceCurveLength) {
// end of the curve
return lowestPrice;
}
uint256 dropPerStep = (startPrice - lowestPrice) /
(daPriceCurveLength / daDropInterval);
uint256 elapsed = block.timestamp - daStartTime;
uint256 steps = elapsed / daDropInterval;
uint256 stepDeduction = steps * dropPerStep;
// don't go negative in the next step
if (stepDeduction > startPrice) {
return lowestPrice;
}
uint256 currentPrice = startPrice - stepDeduction;
return currentPrice > lowestPrice ? currentPrice : lowestPrice;
}
/**
* @notice returns whether the dutch auction has started
*/
function daStarted() public view returns (bool) {
return block.timestamp > daStartTime;
}
/**
* @notice returns whether the mintlist has started
*/
function mintlistStarted() public view returns (bool) {
return block.timestamp > mintlistStartTime;
}
/**
* @notice returns whether the public mint has started
*/
function publicStarted() public view returns (bool) {
return block.timestamp > publicStartTime;
}
/**
* @notice returns whether the claims phase has started
*/
function claimsStarted() public view returns (bool) {
return block.timestamp > claimsStartTime;
}
/**
* @notice returns whether self refunds phase has started
*/
function selfRefundsStarted() public view returns (bool) {
return block.timestamp > selfRefundsStartTime;
}
/**
* @notice returns the number of minter addresses in the DA phase (includes duplicates)
*/
function numDaMinters() public view returns (uint256) {
return daMinters.length;
}
/*
* Refund logic
*/
/**
* @notice issues refunds for the accounts in minters between startIdx and endIdx inclusive
* @param startIdx uint256 the starting index of daMinters
* @param endIdx uint256 the ending index of daMinters, inclusive
*/
function issueRefunds(uint256 startIdx, uint256 endIdx)
public
onlyOwner
nonReentrant
{
for (uint256 i = startIdx; i < endIdx + 1; i++) {
_refundAddress(daMinters[i]);
}
}
/**
* @notice issues a refund for the address
* @param minter address the address to refund
*/
function refundAddress(address minter) public onlyOwner nonReentrant {
_refundAddress(minter);
}
/**
* @notice refunds msg.sender what they're owed
*/
function selfRefund() public nonReentrant {
require(selfRefundsStarted(), 'Self refund period not started');
_refundAddress(msg.sender);
}
function _refundAddress(address minter) private {
uint256 owed = refundOwed(minter);
if (owed > 0) {
daAmountRefunded[minter] += owed;
_safeTransferETHWithFallback(minter, owed);
}
}
/**
* @notice returns the amount owed the address
* @param minter address the address of the account that wants a refund
*/
function refundOwed(address minter) public view returns (uint256) {
uint256 totalCostOfMints = finalPrice * daNumMinted[minter];
uint256 refundsPaidAlready = daAmountRefunded[minter];
return daAmountPaid[minter] - totalCostOfMints - refundsPaidAlready;
}
/**
* @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH.
* @param to account who to send the ETH or WETH to
* @param amount uint256 how much ETH or WETH to send
*/
function _safeTransferETHWithFallback(address to, uint256 amount) internal {
if (!_safeTransferETH(to, amount)) {
IWETH(weth).deposit{value: amount}();
IERC20(weth).transfer(to, amount);
}
}
/**
* @notice Transfer ETH and return the success status.
* @dev This function only forwards 30,000 gas to the callee.
* @param to account who to send the ETH to
* @param value uint256 how much ETH to send
*/
function _safeTransferETH(address to, uint256 value)
internal
returns (bool)
{
(bool success, ) = to.call{value: value, gas: 30_000}(new bytes(0));
return success;
}
/*
* Only the owner can do these things
*/
/**
* @notice pause the contract
*/
function pause() public onlyOwner {
_pause();
}
/**
* @notice unpause the contract
*/
function unpause() public onlyOwner {
_unpause();
}
/**
* @notice set the dutch auction start timestamp
*/
function setDaStartTime(uint256 _newTime) public onlyOwner {
daStartTime = _newTime;
}
/**
* @notice set the mintlist start timestamp
*/
function setMintlistStartTime(uint256 _newTime) public onlyOwner {
mintlistStartTime = _newTime;
}
/**
* @notice set the public sale start timestamp
*/
function setPublicStartTime(uint256 _newTime) public onlyOwner {
publicStartTime = _newTime;
}
/**
* @notice set the claims phase start timestamp
*/
function setClaimsStartTime(uint256 _newTime) public onlyOwner {
claimsStartTime = _newTime;
}
/**
* @notice set the self refund phase start timestamp
*/
function setSelfRefundsStartTime(uint256 _newTime) public onlyOwner {
selfRefundsStartTime = _newTime;
}
/**
* @notice A convenient way to set all phase times at once
* @param newDaStartTime uint256 the dutch auction start time
* @param newMintlistStartTime uint256 the mintlst phase start time
* @param newPublicStartTime uint256 the public phase start time
* @param newClaimsStartTime uint256 the claims phase start time
*/
function setPhaseTimes(
uint256 newDaStartTime,
uint256 newMintlistStartTime,
uint256 newPublicStartTime,
uint256 newClaimsStartTime
) public onlyOwner {
// we put these checks here instead of in the setters themselves
// because they're just guardrails of the typical case
require(
newPublicStartTime >= newMintlistStartTime,
'Set public after mintlist'
);
require(
newClaimsStartTime >= newPublicStartTime,
'Set claims after public'
);
setDaStartTime(newDaStartTime);
setMintlistStartTime(newMintlistStartTime);
setPublicStartTime(newPublicStartTime);
setClaimsStartTime(newClaimsStartTime);
}
/**
* @notice set the merkle root for the mintlist phase
*/
function setMintlist1MerkleRoot(bytes32 newMerkleRoot) public onlyOwner {
mintlist1MerkleRoot = newMerkleRoot;
}
/**
* @notice set the alternate merkle root for the mintlist phase
* @dev we have two because it lets us idempotently update the website without downtime
*/
function setMintlist2MerkleRoot(bytes32 newMerkleRoot) public onlyOwner {
mintlist2MerkleRoot = newMerkleRoot;
}
/**
* @notice set the merkle root for the claimslist phase
*/
function setClaimlistMerkleRoot(bytes32 newMerkleRoot) public onlyOwner {
claimlistMerkleRoot = newMerkleRoot;
}
/**
* @notice set the vault address where the funds are withdrawn
*/
function setVaultAddress(address _newVaultAddress) public onlyOwner {
vault = _newVaultAddress;
}
/**
* @notice set the warriors token address
*/
function setWarriorsAddress(
IForgottenRunesWarriorsGuild _newWarriorsAddress
) public onlyOwner {
warriors = _newWarriorsAddress;
}
/**
* @notice set the weth token address
*/
function setWethAddress(address _newWethAddress) public onlyOwner {
weth = _newWethAddress;
}
/**
* @notice set the dutch auction start price
*/
function setStartPrice(uint256 _newPrice) public onlyOwner {
startPrice = _newPrice;
}
/**
* @notice set the dutch auction lowest price
*/
function setLowestPrice(uint256 _newPrice) public onlyOwner {
lowestPrice = _newPrice;
}
/**
* @notice set the length of time the dutch auction price should change
*/
function setDaPriceCurveLength(uint256 _newTime) public onlyOwner {
daPriceCurveLength = _newTime;
}
/**
* @notice set how long it takes for the dutch auction to step down in price
*/
function setDaDropInterval(uint256 _newTime) public onlyOwner {
daDropInterval = _newTime;
}
/**
* @notice set "final" price of the dutch auction
* @dev this is set automatically if the dutch-auction sells out, but needs to be set manually if the DA fails to sell out
*/
function setFinalPrice(uint256 _newPrice) public onlyOwner {
finalPrice = _newPrice;
}
/**
* @notice the max supply available in the dutch auction
*/
function setMaxDaSupply(uint256 _newSupply) public onlyOwner {
maxDaSupply = _newSupply;
}
/**
* @notice the total max supply available for sale in any phase
*/
function setMaxForSale(uint256 _newSupply) public onlyOwner {
maxForSale = _newSupply;
}
/**
* @notice the max supply available in the claimlist
*/
function setMaxForClaim(uint256 _newSupply) public onlyOwner {
maxForClaim = _newSupply;
}
/**
* @notice Withdraw funds to the vault
* @param _amount uint256 the amount to withdraw
*/
function withdraw(uint256 _amount) public onlyOwner {
require(address(vault) != address(0), 'no vault');
require(payable(vault).send(_amount));
}
/**
* @notice Withdraw all funds to the vault
*/
function withdrawAll() public payable onlyOwner {
require(address(vault) != address(0), 'no vault');
require(payable(vault).send(address(this).balance));
}
/**
* @dev ERC20s should not be sent to this contract, but if someone
* does, it's nice to be able to recover them
* @param token IERC20 the token address
* @param amount uint256 the amount to send
*/
function forwardERC20s(IERC20 token, uint256 amount) public onlyOwner {
require(address(msg.sender) != address(0));
token.transfer(msg.sender, amount);
}
}