-
Notifications
You must be signed in to change notification settings - Fork 156
/
ERC721Drop.sol
1227 lines (1103 loc) · 62 KB
/
ERC721Drop.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
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/**
________ _____ ____ ______ ____
/\_____ \ /\ __`\/\ _`\ /\ _ \ /\ _`\
\/____//'/'\ \ \/\ \ \ \L\ \ \ \L\ \ \ \ \/\ \ _ __ ___ _____ ____
//'/' \ \ \ \ \ \ , /\ \ __ \ \ \ \ \ \/\`'__\/ __`\/\ '__`\ /',__\
//'/'___ \ \ \_\ \ \ \\ \\ \ \/\ \ \ \ \_\ \ \ \//\ \L\ \ \ \L\ \/\__, `\
/\_______\\ \_____\ \_\ \_\ \_\ \_\ \ \____/\ \_\\ \____/\ \ ,__/\/\____/
\/_______/ \/_____/\/_/\/ /\/_/\/_/ \/___/ \/_/ \/___/ \ \ \/ \/___/
\ \_\
\/_/
*/
import {ERC721AUpgradeable} from "erc721a-upgradeable/ERC721AUpgradeable.sol";
import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC721Upgradeable.sol";
import {IERC721AUpgradeable} from "erc721a-upgradeable/IERC721AUpgradeable.sol";
import {IERC2981Upgradeable, IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import {MerkleProofUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
import {IProtocolRewards} from "@zoralabs/protocol-rewards/src/interfaces/IProtocolRewards.sol";
import {ERC721Rewards} from "@zoralabs/protocol-rewards/src/abstract/ERC721/ERC721Rewards.sol";
import {ERC721RewardsStorageV1} from "@zoralabs/protocol-rewards/src/abstract/ERC721/ERC721RewardsStorageV1.sol";
import {IMetadataRenderer} from "./interfaces/IMetadataRenderer.sol";
import {IERC721Drop} from "./interfaces/IERC721Drop.sol";
import {IOwnable} from "./interfaces/IOwnable.sol";
import {IERC4906} from "./interfaces/IERC4906.sol";
import {IFactoryUpgradeGate} from "./interfaces/IFactoryUpgradeGate.sol";
import {ITransferHookExtension} from "./interfaces/ITransferHookExtension.sol";
import {OwnableSkeleton} from "./utils/OwnableSkeleton.sol";
import {FundsReceiver} from "./utils/FundsReceiver.sol";
import {Version} from "./utils/Version.sol";
import {PublicMulticall} from "./utils/PublicMulticall.sol";
import {ERC721DropStorageV1} from "./storage/ERC721DropStorageV1.sol";
import {ERC721DropStorageV2} from "./storage/ERC721DropStorageV2.sol";
import {ERC721TransferHookStorageV1, TransferHookStorage} from "./storage/ERC721TransferHookStorageV1.sol";
/**
* @notice ZORA NFT Base contract for Drops and Editions
*
* @dev For drops: assumes 1. linear mint order, 2. max number of mints needs to be less than max_uint64
* (if you have more than 18 quintillion linear mints you should probably not be using this contract)
* @author iain@zora.co
*
*/
contract ERC721Drop is
ERC721AUpgradeable,
UUPSUpgradeable,
IERC2981Upgradeable,
IERC4906,
ReentrancyGuardUpgradeable,
AccessControlUpgradeable,
IERC721Drop,
PublicMulticall,
OwnableSkeleton,
FundsReceiver,
Version(14),
ERC721DropStorageV1,
ERC721DropStorageV2,
ERC721Rewards,
ERC721RewardsStorageV1,
ERC721TransferHookStorageV1
{
/// @dev This is the max mint batch size for the optimized ERC721A mint contract
uint256 internal immutable MAX_MINT_BATCH_SIZE = 8;
/// @dev Gas limit to send funds
uint256 internal immutable FUNDS_SEND_GAS_LIMIT = 210_000;
/// @notice Access control roles
bytes32 public immutable MINTER_ROLE = keccak256("MINTER");
bytes32 public immutable SALES_MANAGER_ROLE = keccak256("SALES_MANAGER");
/// @dev ZORA V3 transfer helper address for auto-approval
address public immutable zoraERC721TransferHelper;
/// @dev Factory upgrade gate
IFactoryUpgradeGate public immutable factoryUpgradeGate;
/// @notice Zora Mint Fee
uint256 private immutable ZORA_MINT_FEE;
/// @notice Mint Fee Recipient
address payable private immutable ZORA_MINT_FEE_RECIPIENT;
/// @notice Max royalty BPS
uint16 constant MAX_ROYALTY_BPS = 50_00;
uint8 constant SUPPLY_ROYALTY_FOR_EVERY_MINT = 1;
// /// @notice Empty string for blank comments
// string constant EMPTY_STRING = "";
/// @notice Only allow for users with admin access
modifier onlyAdmin() {
if (!hasRole(DEFAULT_ADMIN_ROLE, _msgSender())) {
revert Access_OnlyAdmin();
}
_;
}
/// @notice Only a given role has access or admin
/// @param role role to check for alongside the admin role
modifier onlyRoleOrAdmin(bytes32 role) {
if (!hasRole(DEFAULT_ADMIN_ROLE, _msgSender()) && !hasRole(role, _msgSender())) {
revert Access_MissingRoleOrAdmin(role);
}
_;
}
/// @notice Allows user to mint tokens at a quantity
modifier canMintTokens(uint256 quantity) {
if (quantity + _totalMinted() > config.editionSize) {
revert Mint_SoldOut();
}
_;
}
function _presaleActive() internal view returns (bool) {
return salesConfig.presaleStart <= block.timestamp && salesConfig.presaleEnd > block.timestamp;
}
function _publicSaleActive() internal view returns (bool) {
return salesConfig.publicSaleStart <= block.timestamp && salesConfig.publicSaleEnd > block.timestamp;
}
/// @notice Presale active
modifier onlyPresaleActive() {
if (!_presaleActive()) {
revert Presale_Inactive();
}
_;
}
/// @notice Public sale active
modifier onlyPublicSaleActive() {
if (!_publicSaleActive()) {
revert Sale_Inactive();
}
_;
}
/// @notice Getter for last minted token ID (gets next token id and subtracts 1)
function _lastMintedTokenId() internal view returns (uint256) {
return _currentIndex - 1;
}
/// @notice Start token ID for minting (1-100 vs 0-99)
function _startTokenId() internal pure override returns (uint256) {
return 1;
}
/// @notice Global constructor – these variables will not change with further proxy deploys
/// @dev Marked as an initializer to prevent storage being used of base implementation. Can only be init'd by a proxy.
/// @param _zoraERC721TransferHelper Transfer helper
/// @param _factoryUpgradeGate Factory upgrade gate address
/// @param _mintFeeAmount Mint fee amount in wei
/// @param _mintFeeRecipient Mint fee recipient address
constructor(
address _zoraERC721TransferHelper,
IFactoryUpgradeGate _factoryUpgradeGate,
uint256 _mintFeeAmount,
address payable _mintFeeRecipient,
address _protocolRewards
) initializer ERC721Rewards(_protocolRewards, _mintFeeRecipient) {
zoraERC721TransferHelper = _zoraERC721TransferHelper;
factoryUpgradeGate = _factoryUpgradeGate;
ZORA_MINT_FEE = _mintFeeAmount;
ZORA_MINT_FEE_RECIPIENT = _mintFeeRecipient;
}
/// @dev Create a new drop contract
/// @param _contractName Contract name
/// @param _contractSymbol Contract symbol
/// @param _initialOwner User that owns and can mint the edition, gets royalty and sales payouts and can update the base url if needed.
/// @param _fundsRecipient Wallet/user that receives funds from sale
/// @param _editionSize Number of editions that can be minted in total. If type(uint64).max, unlimited editions can be minted as an open edition.
/// @param _royaltyBPS BPS of the royalty set on the contract. Can be 0 for no royalty.
/// @param _setupCalls Bytes-encoded list of setup multicalls
/// @param _metadataRenderer Renderer contract to use
/// @param _metadataRendererInit Renderer data initial contract
/// @param _createReferral The platform where the collection was created
function initialize(
string memory _contractName,
string memory _contractSymbol,
address _initialOwner,
address payable _fundsRecipient,
uint64 _editionSize,
uint16 _royaltyBPS,
bytes[] calldata _setupCalls,
IMetadataRenderer _metadataRenderer,
bytes memory _metadataRendererInit,
address _createReferral
) public initializer {
// Setup ERC721A
__ERC721A_init(_contractName, _contractSymbol);
// Setup access control
__AccessControl_init();
// Setup re-entracy guard
__ReentrancyGuard_init();
// Setup the owner role
_setupRole(DEFAULT_ADMIN_ROLE, _initialOwner);
// Set ownership to original sender of contract call
_setOwner(_initialOwner);
if (_setupCalls.length > 0) {
// Setup temporary role
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
// Execute setupCalls
multicall(_setupCalls);
// Remove temporary role
_revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
if (config.royaltyBPS > MAX_ROYALTY_BPS) {
revert Setup_RoyaltyPercentageTooHigh(MAX_ROYALTY_BPS);
}
// Setup config variables
config.editionSize = _editionSize;
config.metadataRenderer = _metadataRenderer;
config.royaltyBPS = _royaltyBPS;
config.fundsRecipient = _fundsRecipient;
if (_createReferral != address(0)) {
_setCreateReferral(_createReferral);
}
_metadataRenderer.initializeWithData(_metadataRendererInit);
}
/// @dev Getter for admin role associated with the contract to handle metadata
/// @return boolean if address is admin
function isAdmin(address user) external view returns (bool) {
return hasRole(DEFAULT_ADMIN_ROLE, user);
}
/// @notice Connects this contract to the factory upgrade gate
/// @param newImplementation proposed new upgrade implementation
/// @dev Only can be called by admin
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {
if (!factoryUpgradeGate.isValidUpgradePath({_newImpl: newImplementation, _currentImpl: _getImplementation()})) {
revert Admin_InvalidUpgradeAddress(newImplementation);
}
}
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | burn() |
// | ------------------>
// | |
// | |----.
// | | | burn token
// | |<---'
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @param tokenId Token ID to burn
/// @notice User burn function for token id
function burn(uint256 tokenId) public {
_burn(tokenId, true);
}
/// @dev Get royalty information for token
/// @param _salePrice Sale price for the token
function royaltyInfo(uint256, uint256 _salePrice) external view override returns (address receiver, uint256 royaltyAmount) {
if (config.fundsRecipient == address(0)) {
return (config.fundsRecipient, 0);
}
return (config.fundsRecipient, (_salePrice * config.royaltyBPS) / 10_000);
}
/// @notice Sale details
/// @return IERC721Drop.SaleDetails sale information details
function saleDetails() external view returns (IERC721Drop.SaleDetails memory) {
return
IERC721Drop.SaleDetails({
publicSaleActive: _publicSaleActive(),
presaleActive: _presaleActive(),
publicSalePrice: salesConfig.publicSalePrice,
publicSaleStart: salesConfig.publicSaleStart,
publicSaleEnd: salesConfig.publicSaleEnd,
presaleStart: salesConfig.presaleStart,
presaleEnd: salesConfig.presaleEnd,
presaleMerkleRoot: salesConfig.presaleMerkleRoot,
totalMinted: _totalMinted(),
maxSupply: config.editionSize,
maxSalePurchasePerAddress: salesConfig.maxSalePurchasePerAddress
});
}
/// @dev Number of NFTs the user has minted per address
/// @param minter to get counts for
function mintedPerAddress(address minter) external view override returns (IERC721Drop.AddressMintDetails memory) {
return
IERC721Drop.AddressMintDetails({
presaleMints: presaleMintsByAddress[minter],
publicMints: _numberMinted(minter) - presaleMintsByAddress[minter],
totalMints: _numberMinted(minter)
});
}
/// @dev Setup auto-approval for Zora v3 access to sell NFT
/// Still requires approval for module
/// @param nftOwner owner of the nft
/// @param operator operator wishing to transfer/burn/etc the NFTs
function isApprovedForAll(address nftOwner, address operator) public view override(IERC721Upgradeable, ERC721AUpgradeable) returns (bool) {
if (operator == zoraERC721TransferHelper) {
return true;
}
return super.isApprovedForAll(nftOwner, operator);
}
/// @notice ZORA fee is fixed now per mint
/// @dev Gets the zora fee for amount of withdraw
function zoraFeeForAmount(uint256 quantity) public view returns (address payable recipient, uint256 fee) {
recipient = ZORA_MINT_FEE_RECIPIENT;
fee = ZORA_MINT_FEE * quantity;
}
/**
*** ---------------------------------- ***
*** ***
*** PUBLIC MINTING FUNCTIONS ***
*** ***
*** ---------------------------------- ***
***/
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | purchase() |
// | ---------------------------->
// | |
// | |
// ___________________________________________________________
// ! ALT / drop has no tokens left for caller to mint? !
// !_____/ | | !
// ! | revert Mint_SoldOut() | !
// ! | <---------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// ___________________________________________________________
// ! ALT / public sale isn't active? | !
// !_____/ | | !
// ! | revert Sale_Inactive() | !
// ! | <---------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// ___________________________________________________________
// ! ALT / inadequate funds sent? | !
// !_____/ | | !
// ! | revert Purchase_WrongPrice()| !
// ! | <---------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | mint tokens
// | |<---'
// | |
// | |----.
// | | | emit IERC721Drop.Sale()
// | |<---'
// | |
// | return first minted token ID|
// | <----------------------------
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/**
@dev This allows the user to purchase a edition edition
at the given price in the contract.
*/
/// @notice Purchase a quantity of tokens
/// @param quantity quantity to purchase
/// @return tokenId of the first token minted
function purchase(uint256 quantity) external payable nonReentrant onlyPublicSaleActive returns (uint256) {
return _handleMintWithRewards(msg.sender, quantity, "", address(0));
}
/// @notice Purchase a quantity of tokens with a comment
/// @param quantity quantity to purchase
/// @param comment comment to include in the IERC721Drop.Sale event
/// @return tokenId of the first token minted
function purchaseWithComment(uint256 quantity, string calldata comment) external payable nonReentrant onlyPublicSaleActive returns (uint256) {
return _handleMintWithRewards(msg.sender, quantity, comment, address(0));
}
/// @notice Purchase a quantity of tokens to a specified recipient, with an optional comment
/// @param recipient recipient of the tokens
/// @param quantity quantity to purchase
/// @param comment optional comment to include in the IERC721Drop.Sale event (leave blank for no comment)
/// @return tokenId of the first token minted
function purchaseWithRecipient(
address recipient,
uint256 quantity,
string calldata comment
) external payable nonReentrant onlyPublicSaleActive returns (uint256) {
return _handleMintWithRewards(recipient, quantity, comment, address(0));
}
/// @notice Mint a quantity of tokens with a comment that will pay out rewards
/// @param recipient recipient of the tokens
/// @param quantity quantity to purchase
/// @param comment comment to include in the IERC721Drop.Sale event
/// @param mintReferral The finder of the mint
/// @return tokenId of the first token minted
function mintWithRewards(
address recipient,
uint256 quantity,
string calldata comment,
address mintReferral
) external payable nonReentrant canMintTokens(quantity) onlyPublicSaleActive returns (uint256) {
return _handleMintWithRewards(recipient, quantity, comment, mintReferral);
}
function _handleMintWithRewards(address recipient, uint256 quantity, string memory comment, address mintReferral) internal returns (uint256) {
_mintSupplyRoyalty(quantity);
_requireCanPurchaseQuantity(recipient, quantity);
uint256 salePrice = salesConfig.publicSalePrice;
_handleRewards(
msg.value,
quantity,
salePrice,
config.fundsRecipient != address(0) ? config.fundsRecipient : address(this),
createReferral,
mintReferral
);
_mintNFTs(recipient, quantity);
uint256 firstMintedTokenId = _lastMintedTokenId() - quantity;
_emitSaleEvents(_msgSender(), recipient, quantity, salePrice, firstMintedTokenId, comment);
return firstMintedTokenId;
}
/// @notice Function to mint NFTs
/// @dev (important: Does not enforce max supply limit, enforce that limit earlier)
/// @dev This batches in size of 8 as per recommended by ERC721A creators
/// @param to address to mint NFTs to
/// @param quantity number of NFTs to mint
function _mintNFTs(address to, uint256 quantity) internal {
do {
uint256 toMint = quantity > MAX_MINT_BATCH_SIZE ? MAX_MINT_BATCH_SIZE : quantity;
_mint({to: to, quantity: toMint});
quantity -= toMint;
} while (quantity > 0);
}
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | purchasePresale() |
// | ---------------------------------->
// | |
// | |
// _________________________________________________________________
// ! ALT / drop has no tokens left for caller to mint? !
// !_____/ | | !
// ! | revert Mint_SoldOut() | !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// _________________________________________________________________
// ! ALT / presale sale isn't active? | !
// !_____/ | | !
// ! | revert Presale_Inactive() | !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// _________________________________________________________________
// ! ALT / merkle proof unapproved for caller? | !
// !_____/ | | !
// ! | revert Presale_MerkleNotApproved()| !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// _________________________________________________________________
// ! ALT / inadequate funds sent? | !
// !_____/ | | !
// ! | revert Purchase_WrongPrice() | !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | mint tokens
// | |<---'
// | |
// | |----.
// | | | emit IERC721Drop.Sale()
// | |<---'
// | |
// | return first minted token ID |
// | <----------------------------------
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @notice Merkle-tree based presale purchase function
/// @param quantity quantity to purchase
/// @param maxQuantity max quantity that can be purchased via merkle proof #
/// @param pricePerToken price that each token is purchased at
/// @param merkleProof proof for presale mint
function purchasePresale(uint256 quantity, uint256 maxQuantity, uint256 pricePerToken, bytes32[] calldata merkleProof) external payable returns (uint256) {
return purchasePresaleWithRewards(quantity, maxQuantity, pricePerToken, merkleProof, "", address(0));
}
/// @notice Merkle-tree based presale purchase function with a comment
/// @param quantity quantity to purchase
/// @param maxQuantity max quantity that can be purchased via merkle proof #
/// @param pricePerToken price that each token is purchased at
/// @param merkleProof proof for presale mint
/// @param comment comment to include in the IERC721Drop.Sale event
function purchasePresaleWithComment(
uint256 quantity,
uint256 maxQuantity,
uint256 pricePerToken,
bytes32[] calldata merkleProof,
string calldata comment
) external payable nonReentrant onlyPresaleActive returns (uint256) {
return purchasePresaleWithRewards(quantity, maxQuantity, pricePerToken, merkleProof, comment, address(0));
}
/// @notice Merkle-tree based presale purchase function with a comment and protocol rewards
/// @param quantity quantity to purchase
/// @param maxQuantity max quantity that can be purchased via merkle proof #
/// @param pricePerToken price that each token is purchased at
/// @param merkleProof proof for presale mint
/// @param comment comment to include in the IERC721Drop.Sale event
/// @param mintReferral The facilitator of the mint
function purchasePresaleWithRewards(
uint256 quantity,
uint256 maxQuantity,
uint256 pricePerToken,
bytes32[] calldata merkleProof,
string memory comment,
address mintReferral
) public payable nonReentrant onlyPresaleActive returns (uint256) {
return _handlePurchasePresaleWithRewards(quantity, maxQuantity, pricePerToken, merkleProof, comment, mintReferral);
}
function _handlePurchasePresaleWithRewards(
uint256 quantity,
uint256 maxQuantity,
uint256 pricePerToken,
bytes32[] calldata merkleProof,
string memory comment,
address mintReferral
) internal returns (uint256) {
_mintSupplyRoyalty(quantity);
_requireCanMintQuantity(quantity);
address msgSender = _msgSender();
_requireMerkleApproval(msgSender, maxQuantity, pricePerToken, merkleProof);
_requireCanPurchasePresale(msgSender, quantity, maxQuantity);
_handleRewards(
msg.value,
quantity,
pricePerToken,
config.fundsRecipient != address(0) ? config.fundsRecipient : address(this),
createReferral,
mintReferral
);
_mintNFTs(msgSender, quantity);
uint256 firstMintedTokenId = _lastMintedTokenId() - quantity;
_emitSaleEvents(msgSender, msgSender, quantity, pricePerToken, firstMintedTokenId, comment);
return firstMintedTokenId;
}
/**
*** ---------------------------------- ***
*** ***
*** ADMIN MINTING FUNCTIONS ***
*** ***
*** ---------------------------------- ***
***/
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | adminMint() |
// | ---------------------------------->
// | |
// | |
// _________________________________________________________________
// ! ALT / caller is not admin or minter role? | !
// !_____/ | | !
// ! | revert Access_MissingRoleOrAdmin()| !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// _________________________________________________________________
// ! ALT / drop has no tokens left for caller to mint? !
// !_____/ | | !
// ! | revert Mint_SoldOut() | !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | mint tokens
// | |<---'
// | |
// | return last minted token ID |
// | <----------------------------------
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @notice Mint admin
/// @param recipient recipient to mint to
/// @param quantity quantity to mint
function adminMint(address recipient, uint256 quantity) external onlyRoleOrAdmin(MINTER_ROLE) canMintTokens(quantity) returns (uint256) {
_mintNFTs(recipient, quantity);
return _lastMintedTokenId();
}
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | adminMintAirdrop() |
// | ---------------------------------->
// | |
// | |
// _________________________________________________________________
// ! ALT / caller is not admin or minter role? | !
// !_____/ | | !
// ! | revert Access_MissingRoleOrAdmin()| !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// _________________________________________________________________
// ! ALT / drop has no tokens left for recipients to mint? !
// !_____/ | | !
// ! | revert Mint_SoldOut() | !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |
// | _____________________________________
// | ! LOOP / for all recipients !
// | !______/ | !
// | ! |----. !
// | ! | | mint tokens !
// | ! |<---' !
// | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | return last minted token ID |
// | <----------------------------------
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @dev This mints multiple editions to the given list of addresses.
/// @param recipients list of addresses to send the newly minted editions to
function adminMintAirdrop(address[] calldata recipients) external override onlyRoleOrAdmin(MINTER_ROLE) canMintTokens(recipients.length) returns (uint256) {
uint256 atId = _currentIndex;
uint256 startAt = atId;
unchecked {
for (uint256 endAt = atId + recipients.length; atId < endAt; atId++) {
_mintNFTs(recipients[atId - startAt], 1);
}
}
return _lastMintedTokenId();
}
/**
*** ---------------------------------- ***
*** ***
*** ADMIN CONFIGURATION FUNCTIONS ***
*** ***
*** ---------------------------------- ***
***/
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | setOwner() |
// | ------------------------->
// | |
// | |
// ________________________________________________________
// ! ALT / caller is not admin? | !
// !_____/ | | !
// ! | revert Access_OnlyAdmin()| !
// ! | <------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | set owner
// | |<---'
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @dev Set new owner for royalties / opensea
/// @param newOwner new owner to set
function setOwner(address newOwner) public onlyAdmin {
_setOwner(newOwner);
}
/// @notice Admin function to set the NFT transfer hook, useful for metadata and non-transferrable NFTs.
/// @dev Set to 0 to disable, address to enable transfer hook.
/// @param newTransferHook new transfer hook to receive before token transfer events
function setTransferHook(address newTransferHook) public onlyAdmin {
if (newTransferHook != address(0) && !ITransferHookExtension(newTransferHook).supportsInterface(type(ITransferHookExtension).interfaceId)) {
revert InvalidTransferHook();
}
_setTransferHook(newTransferHook);
}
/// @notice Handles the internal before token transfer hook
/// @param from address transfer is coming from
/// @param to address transfer is going to
/// @param startTokenId token id for transfer
/// @param quantity number of transfers
function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual override {
TransferHookStorage storage transferHookStorage = _getTransferHookStorage();
if (transferHookStorage.transferHookExtension != address(0)) {
ITransferHookExtension(transferHookStorage.transferHookExtension).beforeTokenTransfers({
from: from,
to: to,
operator: msg.sender,
startTokenId: startTokenId,
quantity: quantity
});
}
super._beforeTokenTransfers(from, to, startTokenId, quantity);
}
/// @notice Set a new metadata renderer
/// @param newRenderer new renderer address to use
/// @param setupRenderer data to setup new renderer with
function setMetadataRenderer(IMetadataRenderer newRenderer, bytes memory setupRenderer) external onlyAdmin {
config.metadataRenderer = newRenderer;
if (setupRenderer.length > 0) {
newRenderer.initializeWithData(setupRenderer);
}
emit UpdatedMetadataRenderer({sender: _msgSender(), renderer: newRenderer});
_notifyMetadataUpdate();
}
/// @notice Calls the metadata renderer contract to make an update and uses the EIP4906 event to notify
/// @param data raw calldata to call the metadata renderer contract with.
/// @dev Only accessible via an admin role
function callMetadataRenderer(bytes memory data) public onlyAdmin returns (bytes memory) {
(bool success, bytes memory response) = address(config.metadataRenderer).call(data);
if (!success) {
revert ExternalMetadataRenderer_CallFailed();
}
_notifyMetadataUpdate();
return response;
}
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | setSalesConfiguration() |
// | ---------------------------------->
// | |
// | |
// _________________________________________________________________
// ! ALT / caller is not admin? | !
// !_____/ | | !
// ! | revert Access_MissingRoleOrAdmin()| !
// ! | <---------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | set funds recipient
// | |<---'
// | |
// | |----.
// | | | emit FundsRecipientChanged()
// | |<---'
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @dev This sets the sales configuration
/// @param publicSalePrice New public sale price
/// @param maxSalePurchasePerAddress Max # of purchases (public) per address allowed
/// @param publicSaleStart unix timestamp when the public sale starts
/// @param publicSaleEnd unix timestamp when the public sale ends (set to 0 to disable)
/// @param presaleStart unix timestamp when the presale starts
/// @param presaleEnd unix timestamp when the presale ends
/// @param presaleMerkleRoot merkle root for the presale information
function setSaleConfiguration(
uint104 publicSalePrice,
uint32 maxSalePurchasePerAddress,
uint64 publicSaleStart,
uint64 publicSaleEnd,
uint64 presaleStart,
uint64 presaleEnd,
bytes32 presaleMerkleRoot
) external onlyRoleOrAdmin(SALES_MANAGER_ROLE) {
salesConfig.publicSalePrice = publicSalePrice;
salesConfig.maxSalePurchasePerAddress = maxSalePurchasePerAddress;
salesConfig.publicSaleStart = publicSaleStart;
salesConfig.publicSaleEnd = publicSaleEnd;
salesConfig.presaleStart = presaleStart;
salesConfig.presaleEnd = presaleEnd;
salesConfig.presaleMerkleRoot = presaleMerkleRoot;
emit SalesConfigChanged(_msgSender());
}
// ,-.
// `-'
// /|\
// | ,----------.
// / \ |ERC721Drop|
// Caller `----+-----'
// | setOwner() |
// | ------------------------->
// | |
// | |
// ________________________________________________________
// ! ALT / caller is not admin or SALES_MANAGER_ROLE? !
// !_____/ | | !
// ! | revert Access_OnlyAdmin()| !
// ! | <------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | |
// | |----.
// | | | set sales configuration
// | |<---'
// | |
// | |----.
// | | | emit SalesConfigChanged()
// | |<---'
// Caller ,----+-----.
// ,-. |ERC721Drop|
// `-' `----------'
// /|\
// |
// / \
/// @notice Set a different funds recipient
/// @param newRecipientAddress new funds recipient address
function setFundsRecipient(address payable newRecipientAddress) external onlyRoleOrAdmin(SALES_MANAGER_ROLE) {
// TODO(iain): funds recipient cannot be 0?
config.fundsRecipient = newRecipientAddress;
emit FundsRecipientChanged(newRecipientAddress, _msgSender());
}
// ,-. ,-. ,-.
// `-' `-' `-'
// /|\ /|\ /|\
// | | | ,----------.
// / \ / \ / \ |ERC721Drop|
// Caller FeeRecipient FundsRecipient `----+-----'
// | | withdraw() | |
// | ------------------------------------------------------------------------->
// | | | |
// | | | |
// ________________________________________________________________________________________________________
// ! ALT / caller is not admin or manager? | | !
// !_____/ | | | | !
// ! | revert Access_WithdrawNotAllowed() | !
// ! | <------------------------------------------------------------------------- !
// !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | | | |
// | | send fee amount |
// | | <----------------------------------------------------
// | | | |
// | | | |
// | | | ____________________________________________________________
// | | | ! ALT / send unsuccesful? !
// | | | !_____/ | !
// | | | ! |----. !
// | | | ! | | revert Withdraw_FundsSendFailure() !
// | | | ! |<---' !
// | | | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | | | !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | | | |
// | | foundry.toml | send remaining funds amount|
// | | | <---------------------------
// | | | |
// | | | |
// | | | ____________________________________________________________
// | | | ! ALT / send unsuccesful? !
// | | | !_____/ | !
// | | | ! |----. !
// | | | ! | | revert Withdraw_FundsSendFailure() !
// | | | ! |<---' !
// | | | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// | | | !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!
// Caller FeeRecipient FundsRecipient ,----+-----.
// ,-. ,-. ,-. |ERC721Drop|
// `-' `-' `-' `----------'
// /|\ /|\ /|\
// | | |
// / \ / \ / \
/// @notice This withdraws ETH from the contract to the contract owner.
function withdraw() external nonReentrant {
address sender = _msgSender();
_verifyWithdrawAccess(sender);
uint256 funds = address(this).balance;
// Payout recipient
(bool successFunds, ) = config.fundsRecipient.call{value: funds, gas: FUNDS_SEND_GAS_LIMIT}("");
if (!successFunds) {
revert Withdraw_FundsSendFailure();
}
// Emit event for indexing
emit FundsWithdrawn(_msgSender(), config.fundsRecipient, funds, address(0), 0);
}