@artblocks/contracts 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.env.example +8 -1
  2. package/README.md +44 -165
  3. package/artifacts/contracts/AdminACLV1.sol/AdminACLV1.json +2 -2
  4. package/artifacts/contracts/DependencyRegistryV0.sol/DependencyRegistryV0.json +2 -2
  5. package/artifacts/contracts/GenArt721CoreV3.sol/GenArt721CoreV3.json +2 -2
  6. package/artifacts/contracts/GenArt721CoreV3_Engine.sol/GenArt721CoreV3_Engine.json +2 -2
  7. package/artifacts/contracts/GenArt721CoreV3_Engine_Flex.sol/GenArt721CoreV3_Engine_Flex.json +2229 -0
  8. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV0.sol/MinterDAExpV0.json +2 -2
  9. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV1.sol/MinterDAExpV1.json +2 -2
  10. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV2.sol/MinterDAExpV2.json +2 -2
  11. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV3.sol/MinterDAExpV3.json +2 -2
  12. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExpSettlement/MinterDAExpSettlementV0.sol/MinterDAExpSettlementV0.json +2 -2
  13. package/artifacts/contracts/{minter-suite/Minters → archive/minter-suite/Minters/MinterDAExpSettlement}/MinterDAExpSettlementV1.sol/MinterDAExpSettlementV1.json +3 -3
  14. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV0.sol/MinterDALinV0.json +2 -2
  15. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV1.sol/MinterDALinV1.json +2 -2
  16. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV2.sol/MinterDALinV2.json +2 -2
  17. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV3.sol/MinterDALinV3.json +2 -2
  18. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV0.sol/MinterHolderV0.json +2 -2
  19. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV1.sol/MinterHolderV1.json +2 -2
  20. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV2.sol/MinterHolderV2.json +2 -2
  21. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV3.sol/MinterHolderV3.json +2 -2
  22. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV0.sol/MinterMerkleV0.json +2 -2
  23. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV1.sol/MinterMerkleV1.json +2 -2
  24. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV2.sol/MinterMerkleV2.json +2 -2
  25. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV3.sol/MinterMerkleV3.json +2 -2
  26. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV4.sol/MinterMerkleV4.json +2 -2
  27. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV0.sol/MinterSetPriceV0.json +2 -2
  28. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV1.sol/MinterSetPriceV1.json +2 -2
  29. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV2.sol/MinterSetPriceV2.json +2 -2
  30. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV3.sol/MinterSetPriceV3.json +2 -2
  31. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V0.sol/MinterSetPriceERC20V0.json +2 -2
  32. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V1.sol/MinterSetPriceERC20V1.json +2 -2
  33. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V2.sol/MinterSetPriceERC20V2.json +2 -2
  34. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V3.sol/MinterSetPriceERC20V3.json +2 -2
  35. package/artifacts/contracts/explorations/GenArt721CoreV3_Explorations.sol/GenArt721CoreV3_Explorations.json +2 -2
  36. package/artifacts/contracts/interfaces/0.8.x/IFilteredMinterDAExpSettlementV1.sol/IFilteredMinterDAExpSettlementV1.json +767 -0
  37. package/artifacts/contracts/interfaces/0.8.x/IFilteredMinterSEAV0.sol/IFilteredMinterSEAV0.json +1032 -0
  38. package/artifacts/contracts/interfaces/0.8.x/IGenArt721CoreContractV3.sol/IGenArt721CoreContractV3.json +149 -0
  39. package/artifacts/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Base.sol/IGenArt721CoreContractV3_Base.json +149 -0
  40. package/artifacts/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Engine.sol/IGenArt721CoreContractV3_Engine.json +130 -0
  41. package/artifacts/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Engine_Flex.sol/IGenArt721CoreContractV3_Engine_Flex.json +1066 -0
  42. package/artifacts/contracts/interfaces/0.8.x/IMinterFilterV0.sol/IMinterFilterV0.json +6 -0
  43. package/artifacts/contracts/interfaces/0.8.x/IRandomizerPolyptychV0.sol/IRandomizerPolyptychV0.json +87 -0
  44. package/artifacts/contracts/interfaces/0.8.x/IRandomizerV2.sol/IRandomizerV2.json +1 -1
  45. package/artifacts/contracts/interfaces/0.8.x/IWETH.sol/IWETH.json +55 -0
  46. package/artifacts/contracts/libs/integration-refs/weth9/weth9.sol/WETH9_.json +295 -0
  47. package/artifacts/contracts/minter-suite/MinterFilter/MinterFilterV0.sol/MinterFilterV0.json +8 -2
  48. package/artifacts/contracts/minter-suite/MinterFilter/MinterFilterV1.sol/MinterFilterV1.json +34 -2
  49. package/artifacts/contracts/minter-suite/Minters/MinterDAExpSettlementV2.sol/MinterDAExpSettlementV2.json +1127 -0
  50. package/artifacts/contracts/minter-suite/Minters/MinterDAExpV4.sol/MinterDAExpV4.json +15 -2
  51. package/artifacts/contracts/minter-suite/Minters/MinterDALinV4.sol/MinterDALinV4.json +15 -2
  52. package/artifacts/contracts/minter-suite/Minters/MinterHolderV4.sol/MinterHolderV4.json +15 -2
  53. package/artifacts/contracts/minter-suite/Minters/MinterMerkleV5.sol/MinterMerkleV5.json +15 -2
  54. package/artifacts/contracts/minter-suite/Minters/MinterPolyptychV0.sol/IGenArt721CoreContractV3WithRandomizer.json +792 -0
  55. package/artifacts/contracts/minter-suite/Minters/MinterPolyptychV0.sol/MinterPolyptychV0.json +1268 -0
  56. package/artifacts/contracts/minter-suite/Minters/MinterSEAV0.sol/MinterSEAV0.json +1310 -0
  57. package/artifacts/contracts/minter-suite/Minters/MinterSetPriceERC20V4.sol/MinterSetPriceERC20V4.json +15 -2
  58. package/artifacts/contracts/minter-suite/Minters/MinterSetPriceV4.sol/MinterSetPriceV4.json +15 -2
  59. package/artifacts/contracts/mock/DeadReceiverBidderMock.sol/DeadReceiverBidderMock.json +33 -0
  60. package/artifacts/contracts/mock/GasLimitReceiverBidderMock.sol/GasLimitReceiverBidderMock.json +46 -0
  61. package/artifacts/contracts/mock/GasLimitReceiverMock.sol/GasLimitReceiverMock.json +28 -0
  62. package/artifacts/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol/GenArt721CoreV3_Engine_IncorrectCoreType.json +2 -2
  63. package/artifacts/contracts/mock/RandomizerV2_NoAssignMock.sol/RandomizerV2_NoAssignMock.json +3 -3
  64. package/artifacts/contracts/mock/ReentrancySEAMock.sol/ReentrancySEAAutoBidderMock.json +51 -0
  65. package/artifacts/contracts/{BasicRandomizer.sol → randomizer/BasicRandomizer.sol}/BasicRandomizer.json +3 -3
  66. package/artifacts/contracts/randomizer/BasicRandomizerBase_v0_0_0.sol/RandomizerBase.json +10 -0
  67. package/artifacts/contracts/randomizer/PolyptychRandomizerV0.sol/BasicPolyptychRandomizerV0.json +229 -0
  68. package/artifacts/contracts/randomizer/RandomizerV2.sol/BasicRandomizerV2.json +102 -0
  69. package/artifacts/contracts/royalty-registry/GenArt721RoyaltyOverride.sol/GenArt721RoyaltyOverride.json +2 -2
  70. package/contracts/DependencyRegistryV0.sol +74 -113
  71. package/contracts/GenArt721CoreV3.sol +124 -171
  72. package/contracts/GenArt721CoreV3_Engine.sol +136 -198
  73. package/contracts/GenArt721CoreV3_Engine_Flex.sol +2274 -0
  74. package/contracts/{minter-suite/Minters → archive/minter-suite/Minters/MinterDAExpSettlement}/MinterDAExpSettlementV1.sol +4 -4
  75. package/contracts/explorations/GenArt721CoreV3_Explorations.sol +147 -176
  76. package/contracts/interfaces/0.8.x/IFilteredMinterDAExpSettlementV1.sol +21 -0
  77. package/contracts/interfaces/0.8.x/IFilteredMinterSEAV0.sol +145 -0
  78. package/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Base.sol +39 -0
  79. package/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Engine.sol +0 -3
  80. package/contracts/interfaces/0.8.x/IGenArt721CoreContractV3_Engine_Flex.sol +132 -0
  81. package/contracts/interfaces/0.8.x/IMinterFilterV0.sol +6 -0
  82. package/contracts/interfaces/0.8.x/IRandomizerPolyptychV0.sol +31 -0
  83. package/contracts/interfaces/0.8.x/IRandomizerV2.sol +5 -2
  84. package/contracts/interfaces/0.8.x/IWETH.sol +11 -0
  85. package/contracts/libs/integration-refs/weth9/weth9.sol +763 -0
  86. package/contracts/minter-suite/MinterFilter/MinterFilterV1.sol +53 -47
  87. package/contracts/minter-suite/Minters/MinterDAExpSettlementV2.sol +1304 -0
  88. package/contracts/minter-suite/Minters/MinterDAExpV4.sol +47 -24
  89. package/contracts/minter-suite/Minters/MinterDALinV4.sol +46 -21
  90. package/contracts/minter-suite/Minters/MinterHolderV4.sol +54 -25
  91. package/contracts/minter-suite/Minters/MinterMerkleV5.sol +47 -20
  92. package/contracts/minter-suite/Minters/MinterPolyptychV0.sol +998 -0
  93. package/contracts/minter-suite/Minters/MinterSEAV0.sol +1224 -0
  94. package/contracts/minter-suite/Minters/MinterSetPriceERC20V4.sol +45 -16
  95. package/contracts/minter-suite/Minters/MinterSetPriceV4.sol +40 -13
  96. package/contracts/mock/DeadReceiverBidderMock.sol +23 -0
  97. package/contracts/mock/GasLimitReceiverBidderMock.sol +23 -0
  98. package/contracts/mock/GasLimitReceiverMock.sol +19 -0
  99. package/contracts/mock/RandomizerV2_NoAssignMock.sol +2 -11
  100. package/contracts/mock/ReentrancySEAMock.sol +36 -0
  101. package/contracts/{BasicRandomizer.sol → randomizer/BasicRandomizer.sol} +1 -1
  102. package/contracts/randomizer/BasicRandomizerBase_v0_0_0.sol +23 -0
  103. package/contracts/randomizer/PolyptychRandomizerV0.sol +117 -0
  104. package/contracts/{BasicRandomizerV2.sol → randomizer/RandomizerV2.sol} +7 -15
  105. package/package.json +32 -13
  106. package/artifacts/contracts/BasicRandomizerV2.sol/BasicRandomizerV2.json +0 -102
@@ -0,0 +1,1224 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-only
2
+ // Created By: Art Blocks Inc.
3
+
4
+ pragma solidity 0.8.17;
5
+
6
+ import {IWETH} from "../../interfaces/0.8.x/IWETH.sol";
7
+
8
+ import "../../interfaces/0.8.x/IGenArt721CoreContractV3_Base.sol";
9
+ import "../../interfaces/0.8.x/IMinterFilterV0.sol";
10
+ import "../../interfaces/0.8.x/IFilteredMinterSEAV0.sol";
11
+ import "./MinterBase_v0_1_1.sol";
12
+
13
+ import "@openzeppelin-4.7/contracts/token/ERC721/IERC721.sol";
14
+ import "@openzeppelin-4.7/contracts/security/ReentrancyGuard.sol";
15
+ import "@openzeppelin-4.7/contracts/utils/math/SafeCast.sol";
16
+ import "@openzeppelin-4.7/contracts/utils/math/Math.sol";
17
+
18
+ /**
19
+ * @title Filtered Minter contract that allows tokens to be minted with ETH.
20
+ * Pricing is achieved using an automated serial English Auction mechanism.
21
+ * This is designed to be used with GenArt721CoreContractV3 flagship or
22
+ * engine contracts.
23
+ * @author Art Blocks Inc.
24
+ * @notice This contract was inspired by the release mechanism implemented by
25
+ * nouns.wtf, and we thank them for their pioneering work in this area.
26
+ /*********************************
27
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
28
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
29
+ * ░░░░░░█████████░░█████████░░░ *
30
+ * ░░░░░░██░░░████░░██░░░████░░░ *
31
+ * ░░██████░░░████████░░░████░░░ *
32
+ * ░░██░░██░░░████░░██░░░████░░░ *
33
+ * ░░██░░██░░░████░░██░░░████░░░ *
34
+ * ░░░░░░█████████░░█████████░░░ *
35
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
36
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
37
+ *********************************
38
+ * @notice Token Ownership:
39
+ * This minter contract may own up to two tokens at a time for a given project.
40
+ * The first possible token owned is the token that is currently being
41
+ * auctioned. During the auction, the token is owned by the minter contract.
42
+ * Once the auction ends, the token is transferred to the winning bidder via a
43
+ * call to "settle" the auction.
44
+ * The second possible token owned is the token that will be auctioned next.
45
+ * This token is minted to and owned by the minter contract whenever possible
46
+ * (i.e. when the project's max invocations has not been reached) when an
47
+ * artist configures their project on this minter, or when a new auction is
48
+ * started. The purpose of this token is to allow users to have a preview of
49
+ * the next token that will be auctioned, even before the auction has started.
50
+ * @notice Privileged Roles and Ownership:
51
+ * This contract is designed to be managed, with limited powers.
52
+ * Privileged roles and abilities are controlled by the core contract's Admin
53
+ * ACL contract and a project's artist. Both of these roles hold extensive
54
+ * power and can modify minter details.
55
+ * Care must be taken to ensure that the admin ACL contract and artist
56
+ * addresses are secure behind a multi-sig or other access control mechanism.
57
+ * ----------------------------------------------------------------------------
58
+ * The following functions are restricted to the core contract's Admin ACL
59
+ * contract:
60
+ * - updateAllowableAuctionDurationSeconds
61
+ * - updateMinterMinBidIncrementPercentage
62
+ * - updateMinterTimeBufferSeconds
63
+ * - ejectNextTokenTo
64
+ * ----------------------------------------------------------------------------
65
+ * The following functions are restricted to a project's artist:
66
+ * - setProjectMaxInvocations
67
+ * - manuallyLimitProjectMaxInvocations
68
+ * - configureFutureAuctions
69
+ * - tryPopulateNextToken
70
+ * ----------------------------------------------------------------------------
71
+ * The following functions are restricted to a project's artist or the core
72
+ * contract's Admin ACL contract:
73
+ * - resetAuctionDetails
74
+ * ----------------------------------------------------------------------------
75
+ * Additional admin and artist privileged roles may be described on other
76
+ * contracts that this minter integrates with.
77
+ *
78
+ * @dev Note that while this minter makes use of `block.timestamp` and it is
79
+ * technically possible that this value is manipulated by block producers, such
80
+ * manipulation will not have material impact on the ability for collectors to
81
+ * place a bid before auction end time. This is due to the admin-configured
82
+ * `minterTimeBufferSeconds` parameter, which will used to ensure that
83
+ * collectors have sufficient time to place a bid after the final bid and
84
+ * before the auction end time.
85
+ */
86
+ contract MinterSEAV0 is ReentrancyGuard, MinterBase, IFilteredMinterSEAV0 {
87
+ using SafeCast for uint256;
88
+
89
+ /// Core contract address this minter interacts with
90
+ address public immutable genArt721CoreAddress;
91
+
92
+ /// The core contract integrates with V3 contracts
93
+ IGenArt721CoreContractV3_Base private immutable genArtCoreContract_Base;
94
+
95
+ /// Minter filter address this minter interacts with
96
+ address public immutable minterFilterAddress;
97
+
98
+ /// Minter filter this minter may interact with.
99
+ IMinterFilterV0 private immutable minterFilter;
100
+
101
+ /// minterType for this minter
102
+ string public constant minterType = "MinterSEAV0";
103
+
104
+ /// minter version for this minter
105
+ string public constant minterVersion = "v0.0.1";
106
+
107
+ /// The public WETH contract address
108
+ /// @dev WETH is used as fallback payment method when ETH transfers are
109
+ /// failing during bidding process (e.g. receive function is not payable)
110
+ IWETH public immutable weth;
111
+
112
+ uint256 constant ONE_MILLION = 1_000_000;
113
+
114
+ // project-specific parameters
115
+ struct ProjectConfig {
116
+ bool maxHasBeenInvoked;
117
+ // max uint24 ~= 1.6e7, > max possible project invocations of 1e6
118
+ uint24 maxInvocations;
119
+ // time after which new auctions may be started
120
+ // note: new auctions must always be started with a new bid, at which
121
+ // point the auction will actually start
122
+ // @dev this is a project-level constraint, and individual auctions
123
+ // will each have their own start time defined in `activeAuction`
124
+ // max uint64 ~= 1.8e19 sec ~= 570 billion years
125
+ uint64 timestampStart;
126
+ // duration of each new auction, before any extensions due to late bids
127
+ uint32 auctionDurationSeconds;
128
+ // next token number to be auctioned, owned by minter
129
+ // @dev store token number to enable storage packing, as token ID can
130
+ // be derived from this value in combination with project ID
131
+ // max uint24 ~= 1.6e7, > max possible project invocations of 1e6
132
+ uint24 nextTokenNumber;
133
+ // bool to indicate if next token number has been populated, or is
134
+ // still default value of 0
135
+ // @dev required to handle edge case where next token number is 0
136
+ bool nextTokenNumberIsPopulated;
137
+ // reserve price, i.e. minimum starting bid price, in wei
138
+ // @dev for configured auctions, this will be gt 0, so it may be used
139
+ // to determine if an auction is configured
140
+ uint256 basePrice;
141
+ // active auction for project
142
+ Auction activeAuction;
143
+ }
144
+
145
+ mapping(uint256 => ProjectConfig) public projectConfig;
146
+
147
+ // minter-wide, admin-configurable parameters
148
+ // ----------------------------------------
149
+ // minimum inital auction length, in seconds; configurable by admin
150
+ // max uint32 ~= 4.3e9 sec ~= 136 years
151
+ // @dev enforced only when artist configures a project
152
+ // @dev default to 10 minutes
153
+ uint32 minAuctionDurationSeconds = 600;
154
+ // maximum inital auction length, in seconds; configurable by admin
155
+ // @dev enforced only when artist configures a project
156
+ // @dev default to 1 month (1/12 of a year)
157
+ uint32 maxAuctionDurationSeconds = 2_629_746;
158
+ // the minimum percent increase for new bids above the current bid
159
+ // configureable by admin
160
+ // max uint8 ~= 255, > 100 percent
161
+ // @dev used when determining the increment percentage for any new bid on
162
+ // the minter, across all projects
163
+ uint8 minterMinBidIncrementPercentage = 5;
164
+ // minimum time remaining in auction after a new bid is placed
165
+ // configureable by admin
166
+ // max uint32 ~= 4.3e9 sec ~= 136 years
167
+ // @dev used when determining the buffer time for any new bid on the
168
+ // minter, across all projects
169
+ uint32 minterTimeBufferSeconds = 120;
170
+ // gas limit for refunding ETH to bidders
171
+ // configurable by admin, default to 30,000
172
+ // max uint16 = 65,535 to ensure bid refund gas limit remains reasonable
173
+ uint16 minterRefundGasLimit = 30_000;
174
+
175
+ // modifier-like internal functions
176
+ // @dev we use internal functions instead of modifiers to reduce contract
177
+ // bytecode size
178
+ // ----------------------------------------
179
+ // function to restrict access to only AdminACL allowed calls
180
+ // @dev defers to the ACL contract used on the core contract
181
+ function _onlyCoreAdminACL(bytes4 _selector) internal {
182
+ require(
183
+ genArtCoreContract_Base.adminACLAllowed(
184
+ msg.sender,
185
+ address(this),
186
+ _selector
187
+ ),
188
+ "Only Core AdminACL allowed"
189
+ );
190
+ }
191
+
192
+ // function to restrict access to only the artist of a project
193
+ function _onlyArtist(uint256 _projectId) internal view {
194
+ require(
195
+ (msg.sender ==
196
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)),
197
+ "Only Artist"
198
+ );
199
+ }
200
+
201
+ // function to restrict access to only the artist of a project or
202
+ // AdminACL allowed calls
203
+ // @dev defers to the ACL contract used on the core contract
204
+ function _onlyCoreAdminACLOrArtist(
205
+ uint256 _projectId,
206
+ bytes4 _selector
207
+ ) internal {
208
+ require(
209
+ (msg.sender ==
210
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)) ||
211
+ (
212
+ genArtCoreContract_Base.adminACLAllowed(
213
+ msg.sender,
214
+ address(this),
215
+ _selector
216
+ )
217
+ ),
218
+ "Only Artist or Admin ACL"
219
+ );
220
+ }
221
+
222
+ // function to require that a value is non-zero
223
+ function _onlyNonZero(uint256 _value) internal pure {
224
+ require(_value > 0, "Only non-zero");
225
+ }
226
+
227
+ /**
228
+ * @notice Initializes contract to be a Filtered Minter for
229
+ * `_minterFilter`, integrated with Art Blocks core contract
230
+ * at address `_genArt721Address`.
231
+ * @param _genArt721Address Art Blocks core contract address for
232
+ * which this contract will be a minter.
233
+ * @param _minterFilter Minter filter for which
234
+ * this will a filtered minter.
235
+ * @param _wethAddress The WETH contract address to use for fallback
236
+ * payment method when ETH transfers are failing during bidding process
237
+ */
238
+ constructor(
239
+ address _genArt721Address,
240
+ address _minterFilter,
241
+ address _wethAddress
242
+ ) ReentrancyGuard() MinterBase(_genArt721Address) {
243
+ genArt721CoreAddress = _genArt721Address;
244
+ genArtCoreContract_Base = IGenArt721CoreContractV3_Base(
245
+ _genArt721Address
246
+ );
247
+ minterFilterAddress = _minterFilter;
248
+ minterFilter = IMinterFilterV0(_minterFilter);
249
+ require(
250
+ minterFilter.genArt721CoreAddress() == _genArt721Address,
251
+ "Illegal contract pairing"
252
+ );
253
+ weth = IWETH(_wethAddress);
254
+ }
255
+
256
+ /**
257
+ * @notice Syncs local maximum invocations of project `_projectId` based on
258
+ * the value currently defined in the core contract.
259
+ * @param _projectId Project ID to set the maximum invocations for.
260
+ * @dev this enables gas reduction after maxInvocations have been reached -
261
+ * core contracts shall still enforce a maxInvocation check during mint.
262
+ */
263
+ function setProjectMaxInvocations(uint256 _projectId) public {
264
+ _onlyArtist(_projectId);
265
+ uint256 maxInvocations;
266
+ uint256 invocations;
267
+ (invocations, maxInvocations, , , , ) = genArtCoreContract_Base
268
+ .projectStateData(_projectId);
269
+
270
+ // update storage with results
271
+ projectConfig[_projectId].maxInvocations = uint24(maxInvocations);
272
+
273
+ // must ensure maxHasBeenInvoked is correctly set after manually syncing the
274
+ // local maxInvocations value with the core contract's maxInvocations value.
275
+ // This synced value of maxInvocations from the core contract will always be greater
276
+ // than or equal to the previous value of maxInvocations stored locally.
277
+ projectConfig[_projectId].maxHasBeenInvoked =
278
+ invocations == maxInvocations;
279
+
280
+ emit ProjectMaxInvocationsLimitUpdated(_projectId, maxInvocations);
281
+
282
+ // for convenience, try to mint and assign a token to the project's
283
+ // next slot
284
+ _tryMintTokenToNextSlot(_projectId);
285
+ }
286
+
287
+ /**
288
+ * @notice Manually sets the local maximum invocations of project `_projectId`
289
+ * with the provided `_maxInvocations`, checking that `_maxInvocations` is less
290
+ * than or equal to the value of project `_project_id`'s maximum invocations that is
291
+ * set on the core contract.
292
+ * @dev Note that a `_maxInvocations` of 0 can only be set if the current `invocations`
293
+ * value is also 0 and this would also set `maxHasBeenInvoked` to true, correctly short-circuiting
294
+ * this minter's purchase function, avoiding extra gas costs from the core contract's maxInvocations check.
295
+ * @param _projectId Project ID to set the maximum invocations for.
296
+ * @param _maxInvocations Maximum invocations to set for the project.
297
+ */
298
+ function manuallyLimitProjectMaxInvocations(
299
+ uint256 _projectId,
300
+ uint256 _maxInvocations
301
+ ) external {
302
+ _onlyArtist(_projectId);
303
+ // CHECKS
304
+ // ensure that the manually set maxInvocations is not greater than what is set on the core contract
305
+ uint256 maxInvocations;
306
+ uint256 invocations;
307
+ (invocations, maxInvocations, , , , ) = genArtCoreContract_Base
308
+ .projectStateData(_projectId);
309
+ require(
310
+ _maxInvocations <= maxInvocations,
311
+ "Cannot increase project max invocations above core contract set project max invocations"
312
+ );
313
+ require(
314
+ _maxInvocations >= invocations,
315
+ "Cannot set project max invocations to less than current invocations"
316
+ );
317
+ // EFFECTS
318
+ // update storage with results
319
+ projectConfig[_projectId].maxInvocations = uint24(_maxInvocations);
320
+ // We need to ensure maxHasBeenInvoked is correctly set after manually
321
+ // setting the local maxInvocations value.
322
+ projectConfig[_projectId].maxHasBeenInvoked =
323
+ invocations == _maxInvocations;
324
+
325
+ emit ProjectMaxInvocationsLimitUpdated(_projectId, _maxInvocations);
326
+
327
+ // for convenience, try to mint and assign a token to the project's
328
+ // next slot
329
+ _tryMintTokenToNextSlot(_projectId);
330
+ }
331
+
332
+ /**
333
+ * @notice Sets the minimum and maximum values that are settable for
334
+ * `durationSeconds` for all project configurations.
335
+ * Note that the auction duration is the initial duration of the auction,
336
+ * and does not include any extensions that may occur due to new bids being
337
+ * placed near the end of an auction.
338
+ * @param _minAuctionDurationSeconds Minimum auction duration in seconds.
339
+ * @param _maxAuctionDurationSeconds Maximum auction duration in seconds.
340
+ */
341
+ function updateAllowableAuctionDurationSeconds(
342
+ uint32 _minAuctionDurationSeconds,
343
+ uint32 _maxAuctionDurationSeconds
344
+ ) external {
345
+ _onlyCoreAdminACL(this.updateAllowableAuctionDurationSeconds.selector);
346
+ // CHECKS
347
+ _onlyNonZero(_minAuctionDurationSeconds);
348
+ require(
349
+ _maxAuctionDurationSeconds > _minAuctionDurationSeconds,
350
+ "Only max gt min"
351
+ );
352
+ // EFFECTS
353
+ minAuctionDurationSeconds = _minAuctionDurationSeconds;
354
+ maxAuctionDurationSeconds = _maxAuctionDurationSeconds;
355
+ emit AuctionDurationSecondsRangeUpdated(
356
+ _minAuctionDurationSeconds,
357
+ _maxAuctionDurationSeconds
358
+ );
359
+ }
360
+
361
+ /**
362
+ * @notice Sets the minter-wide minimum bid increment percentage. New bids
363
+ * must be this percent higher than the current top bid to be successful.
364
+ * This value should be configured by admin such that appropriate price
365
+ * discovery is able to be reached, but gas fees associated with bidding
366
+ * wars do not dominate the economics of an auction.
367
+ * @dev the input value is considered to be a percentage, so that a value
368
+ * of 5 represents 5%.
369
+ */
370
+ function updateMinterMinBidIncrementPercentage(
371
+ uint8 _minterMinBidIncrementPercentage
372
+ ) external {
373
+ _onlyCoreAdminACL(this.updateMinterMinBidIncrementPercentage.selector);
374
+ // CHECKS
375
+ _onlyNonZero(_minterMinBidIncrementPercentage);
376
+ // EFFECTS
377
+ minterMinBidIncrementPercentage = _minterMinBidIncrementPercentage;
378
+ emit MinterMinBidIncrementPercentageUpdated(
379
+ _minterMinBidIncrementPercentage
380
+ );
381
+ }
382
+
383
+ /**
384
+ * @notice Sets the minter-wide time buffer in seconds. The time buffer is
385
+ * the minimum amount of time that must pass between the final bid and the
386
+ * the end of an auction. Auctions are extended if a new bid is placed
387
+ * within this time buffer of the auction end time.
388
+ */
389
+ function updateMinterTimeBufferSeconds(
390
+ uint32 _minterTimeBufferSeconds
391
+ ) external {
392
+ _onlyCoreAdminACL(this.updateMinterTimeBufferSeconds.selector);
393
+ // CHECKS
394
+ _onlyNonZero(_minterTimeBufferSeconds);
395
+ // EFFECTS
396
+ minterTimeBufferSeconds = _minterTimeBufferSeconds;
397
+ emit MinterTimeBufferUpdated(_minterTimeBufferSeconds);
398
+ }
399
+
400
+ /**
401
+ * @notice Sets the gas limit during ETH refunds when a collector is
402
+ * outbid. This value should be set to a value that is high enough to
403
+ * ensure that refunds are successful for commonly used wallets, but low
404
+ * enough to avoid excessive abuse of refund gas allowance during a new
405
+ * bid.
406
+ * @dev max gas limit is 63,535, which is considered a future-safe upper
407
+ * bound.
408
+ * @param _minterRefundGasLimit Gas limit to set for refunds. Must be between
409
+ * 5,000 and max uint16 (63,535).
410
+ */
411
+ function updateRefundGasLimit(uint16 _minterRefundGasLimit) external {
412
+ _onlyCoreAdminACL(this.updateRefundGasLimit.selector);
413
+ // CHECKS
414
+ // @dev max gas limit implicitly checked by using uint16 input arg
415
+ // @dev min gas limit is based on rounding up current cost to send ETH
416
+ // to a Gnosis Safe wallet, which accesses cold address and emits event
417
+ require(_minterRefundGasLimit >= 7_000, "Only gte 7_000");
418
+ // EFFECTS
419
+ minterRefundGasLimit = _minterRefundGasLimit;
420
+ emit MinterRefundGasLimitUpdated(_minterRefundGasLimit);
421
+ }
422
+
423
+ /**
424
+ * @notice Warning: Disabling purchaseTo is not supported on this minter.
425
+ * This method exists purely for interface-conformance purposes.
426
+ */
427
+ function togglePurchaseToDisabled(uint256 _projectId) external view {
428
+ _onlyArtist(_projectId);
429
+ revert("Action not supported");
430
+ }
431
+
432
+ /**
433
+ * @notice Sets auction details for project `_projectId`.
434
+ * If project does not have a "next token" assigned, this function attempts
435
+ * to mint a token and assign it to the project's next token slot.
436
+ * @param _projectId Project ID to set future auction details for.
437
+ * @param _timestampStart Timestamp after which new auctions may be
438
+ * started. Note that this is not the timestamp of the auction start, but
439
+ * rather the timestamp after which a auction may be started. Also note
440
+ * that the passed value here must either be in the future, or `0` (which
441
+ * indicates that auctions are immediately startable).
442
+ * @param _auctionDurationSeconds Duration of new auctions, in seconds,
443
+ * before any extensions due to bids being placed inside buffer period near
444
+ * the end of an auction.
445
+ * @param _basePrice reserve price (minimum starting bid price), in wei.
446
+ * Must be greater than 0, but may be as low as 1 wei.
447
+ * @dev `_basePrice` of zero not allowed so we can use zero as a gas-
448
+ * efficient indicator of whether auctions have been configured for a
449
+ * project.
450
+ */
451
+ function configureFutureAuctions(
452
+ uint256 _projectId,
453
+ uint256 _timestampStart,
454
+ uint256 _auctionDurationSeconds,
455
+ uint256 _basePrice
456
+ ) external {
457
+ _onlyArtist(_projectId);
458
+ // CHECKS
459
+ _onlyNonZero(_basePrice);
460
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
461
+ require(
462
+ _timestampStart == 0 || block.timestamp < _timestampStart,
463
+ "Only future start times or 0"
464
+ );
465
+ require(
466
+ (_auctionDurationSeconds >= minAuctionDurationSeconds) &&
467
+ (_auctionDurationSeconds <= maxAuctionDurationSeconds),
468
+ "Auction duration out of range"
469
+ );
470
+ // EFFECTS
471
+ _projectConfig.timestampStart = _timestampStart.toUint64();
472
+ _projectConfig.auctionDurationSeconds = _auctionDurationSeconds
473
+ .toUint32();
474
+ _projectConfig.basePrice = _basePrice;
475
+
476
+ emit ConfiguredFutureAuctions(
477
+ _projectId,
478
+ _timestampStart.toUint64(),
479
+ _auctionDurationSeconds.toUint32(),
480
+ _basePrice
481
+ );
482
+
483
+ // sync local max invocations if not initially populated
484
+ // @dev if local max invocations and maxHasBeenInvoked are both
485
+ // initial values, we know they have not been populated.
486
+ if (
487
+ _projectConfig.maxInvocations == 0 &&
488
+ _projectConfig.maxHasBeenInvoked == false
489
+ ) {
490
+ setProjectMaxInvocations(_projectId);
491
+ // @dev setProjectMaxInvocations function calls
492
+ // _tryMintTokenToNextSlot, so we do not call it here.
493
+ } else {
494
+ // for convenience, try to mint to next token slot
495
+ _tryMintTokenToNextSlot(_projectId);
496
+ }
497
+ }
498
+
499
+ /**
500
+ * @notice Resets future auction configuration for project `_projectId`,
501
+ * zero-ing out all details having to do with future auction parameters.
502
+ * This is not intended to be used in normal operation, but rather only in
503
+ * case of the need to halt the creation of any future auctions.
504
+ * Does not affect any project max invocation details.
505
+ * Does not affect any project next token details (i.e. if a next token is
506
+ * assigned, it will remain assigned and held by the minter until auction
507
+ * details are reconfigured).
508
+ * @param _projectId Project ID to reset future auction configuration
509
+ * details for.
510
+ */
511
+ function resetAuctionDetails(uint256 _projectId) external {
512
+ _onlyCoreAdminACLOrArtist(
513
+ _projectId,
514
+ this.resetAuctionDetails.selector
515
+ );
516
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
517
+ // reset to initial values
518
+ _projectConfig.timestampStart = 0;
519
+ _projectConfig.auctionDurationSeconds = 0;
520
+ _projectConfig.basePrice = 0;
521
+ // @dev do not affect next token or max invocations
522
+
523
+ emit ResetAuctionDetails(_projectId);
524
+ }
525
+
526
+ /**
527
+ * @notice Admin-only function that ejects a project's "next token" from
528
+ * the minter and sends it to the input `_to` address.
529
+ * This function is only intended for use in the edge case where the minter
530
+ * has a "next token" assigned to a project, but the project has been reset
531
+ * via `resetAuctionDetails`, and the artist does not want an auction to be
532
+ * started for the "next token". This function also protects against the
533
+ * unforseen case where the minter is in an unexpected state where it has a
534
+ * "next token" assigned to a project, but for some reason the project is
535
+ * unable to begin a new auction due to a bug.
536
+ * @dev only a single token may be actively assigned to a project's "next
537
+ * token" slot at any given time. This function will eject the token, and
538
+ * no further tokens will be assigned to the project's "next token" slot,
539
+ * unless the project is subsequently reconfigured via an artist call to
540
+ * `configureFutureAuctions`.
541
+ * @param _projectId Project ID to eject next token for.
542
+ * @param _to Address to send the ejected token to.
543
+ */
544
+ function ejectNextTokenTo(uint256 _projectId, address _to) external {
545
+ _onlyCoreAdminACL(this.ejectNextTokenTo.selector);
546
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
547
+ // CHECKS
548
+ // only if project is not configured (i.e. artist called
549
+ // `resetAuctionDetails`)
550
+ // @dev we use `basePrice` as a gas-efficient indicator of whether
551
+ // auctions have been configured for a project.
552
+ require(_projectConfig.basePrice == 0, "Only unconfigured projects");
553
+ // only if minter has a next token assigned
554
+ require(
555
+ _projectConfig.nextTokenNumberIsPopulated == true,
556
+ "No next token"
557
+ );
558
+ // EFFECTS
559
+ _projectConfig.nextTokenNumberIsPopulated = false;
560
+ // INTERACTIONS
561
+ // @dev overflow automatically handled by Sol ^0.8.0
562
+ uint256 nextTokenId = (_projectId * ONE_MILLION) +
563
+ _projectConfig.nextTokenNumber;
564
+ IERC721(genArt721CoreAddress).transferFrom(
565
+ address(this),
566
+ _to,
567
+ nextTokenId
568
+ );
569
+ emit ProjectNextTokenEjected(_projectId);
570
+ }
571
+
572
+ /**
573
+ * @notice Emergency, Artist-only function that attempts to mint a new
574
+ * token and set it as the the next token to be auctioned for project
575
+ * `_projectId`.
576
+ * Note: This function is only included for emergency, unforseen use cases,
577
+ * and should not be used in normal operation. It is here only for
578
+ * redundant protection against an unforseen edge case where the minter
579
+ * does not have a populated "next token", but there are still invocations
580
+ * remaining on the project.
581
+ * This function reverts if the project is not configured on this minter.
582
+ * This function returns early and does not modify state when:
583
+ * - the minter already has a populated "next token" for the project
584
+ * - the project has reached its maximum invocations on the core contract
585
+ * or minter
586
+ * @dev This function is gated to only the project's artist to prevent
587
+ * early minting of tokens by other users.
588
+ * @param _projectId The project ID
589
+ */
590
+ function tryPopulateNextToken(uint256 _projectId) public nonReentrant {
591
+ _onlyArtist(_projectId);
592
+ // CHECKS
593
+ // revert if project is not configured on this minter
594
+ require(
595
+ _projectIsConfigured(projectConfig[_projectId]),
596
+ "Project not configured"
597
+ );
598
+ // INTERACTIONS
599
+ // attempt to mint new token to this minter contract, only if max
600
+ // invocations has not been reached
601
+ _tryMintTokenToNextSlot(_projectId);
602
+ }
603
+
604
+ /**
605
+ * @notice Settles any complete auction for token `_settleTokenId` (if
606
+ * applicable), then attempts to create a bid for token
607
+ * `_bidTokenId` with bid amount and bidder address equal to
608
+ * `msg.value` and `msg.sender`, respectively.
609
+ * Intended to gracefully handle the case where a user is front-run by
610
+ * one or more transactions to settle and/or initialize a new auction,
611
+ * potentially still placing a bid on the auction for the token ID if the
612
+ * bid value is sufficiently higher than the current highest bid.
613
+ * Note that the use of `_targetTokenId` is to prevent the possibility of
614
+ * transactions that are stuck in the pending pool for long periods of time
615
+ * from unintentionally bidding on auctions for future tokens.
616
+ * Note that calls to `settleAuction` and `createBid` are possible
617
+ * to be called in separate transactions, but this function is provided for
618
+ * convenience and executes both of those functions in a single
619
+ * transaction, while handling front-running as gracefully as possible.
620
+ * @param _settleTokenId Token ID to settle auction for.
621
+ * @dev this function is not non-reentrant, but the underlying calls are
622
+ * to non-reentrant functions.
623
+ */
624
+ function settleAuctionAndCreateBid(
625
+ uint256 _settleTokenId,
626
+ uint256 _bidTokenId
627
+ ) external payable {
628
+ // ensure tokens are in the same project
629
+ require(
630
+ _settleTokenId / ONE_MILLION == _bidTokenId / ONE_MILLION,
631
+ "Only tokens in same project"
632
+ );
633
+ // settle completed auction, if applicable
634
+ settleAuction(_settleTokenId);
635
+ // attempt to bid on next token
636
+ createBid_l34(_bidTokenId);
637
+ }
638
+
639
+ /**
640
+ * @notice Settles a completed auction for `_tokenId`, if it exists and is
641
+ * not yet settled.
642
+ * Returns early (does not modify state) if
643
+ * - there is no initialized auction for the project
644
+ * - there is an auction that has already been settled for the project
645
+ * - there is an auction for a different token ID on the project
646
+ * (likely due to a front-run)
647
+ * This function reverts if the auction for `_tokenId` exists, but has not
648
+ * yet ended.
649
+ * @param _tokenId Token ID to settle auction for.
650
+ */
651
+ function settleAuction(uint256 _tokenId) public nonReentrant {
652
+ uint256 _projectId = _tokenId / ONE_MILLION;
653
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
654
+ Auction storage _auction = _projectConfig.activeAuction;
655
+ // CHECKS
656
+ // @dev this check is not strictly necessary, but is included for
657
+ // clear error messaging
658
+ require(_auction.initialized, "Auction not initialized");
659
+ if (_auction.settled || (_auction.tokenId != _tokenId)) {
660
+ // auction already settled or is for a different token ID, so
661
+ // return early and do not modify state
662
+ return;
663
+ }
664
+ // @dev important that the following check is after the early return
665
+ // block above to maintain desired behavior
666
+ require(block.timestamp > _auction.endTime, "Auction not yet ended");
667
+ // EFFECTS
668
+ _auction.settled = true;
669
+ // INTERACTIONS
670
+ // send token to the winning bidder
671
+ IERC721(genArt721CoreAddress).transferFrom(
672
+ address(this),
673
+ _auction.currentBidder,
674
+ _tokenId
675
+ );
676
+ // distribute revenues from auction
677
+ splitRevenuesETH(_projectId, _auction.currentBid, genArt721CoreAddress);
678
+
679
+ emit AuctionSettled(
680
+ _tokenId,
681
+ _auction.currentBidder,
682
+ _auction.currentBid
683
+ );
684
+ }
685
+
686
+ /**
687
+ * @notice Enters a bid for token `_tokenId`.
688
+ * If an auction for token `_tokenId` does not exist, an auction will be
689
+ * initialized as long as any existing auction for the project has been
690
+ * settled.
691
+ * In order to successfully place the bid, the token bid must be:
692
+ * - greater than or equal to a project's minimum bid price if a new
693
+ * auction is initialized
694
+ * - sufficiently greater than the current highest bid, according to the
695
+ * minter's bid increment percentage `minterMinBidIncrementPercentage`,
696
+ * if an auction for the token already exists
697
+ * If the bid is unsuccessful, the transaction will revert.
698
+ * If the bid is successful, but outbid by another bid before the auction
699
+ * ends, the funds will be noncustodially returned to the bidder's address,
700
+ * `msg.sender`. A fallback method of sending funds back to the bidder via
701
+ * WETH is used if the bidder address is not accepting ETH (preventing
702
+ * denial of service attacks) within an admin-configured gas limit of
703
+ * `minterRefundGasLimit`.
704
+ * Note that the use of `_tokenId` is to prevent the possibility of
705
+ * transactions that are stuck in the pending pool for long periods of time
706
+ * from unintentionally bidding on auctions for future tokens.
707
+ * If a new auction is initialized during this call, the project's next
708
+ * token will be attempted to be minted to this minter contract, preparing
709
+ * it for the next auction. If the project's next token cannot be minted
710
+ * due to e.g. reaching the maximum invocations on the core contract or
711
+ * minter, the project's next token will not be minted.
712
+ * @param _tokenId Token ID being bidded on
713
+ */
714
+ function createBid(uint256 _tokenId) external payable {
715
+ createBid_l34(_tokenId);
716
+ }
717
+
718
+ /**
719
+ * @notice gas-optimized version of createBid(uint256).
720
+ * @dev nonReentrant modifier is used to prevent reentrancy attacks, e.g.
721
+ * an an auto-bidder that would be able to atomically outbid a user's
722
+ * new bid via a reentrant call to createBid.
723
+ */
724
+ function createBid_l34(uint256 _tokenId) public payable nonReentrant {
725
+ uint256 _projectId = _tokenId / ONE_MILLION;
726
+ // CHECKS
727
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
728
+ Auction storage _auction = _projectConfig.activeAuction;
729
+
730
+ // if no auction exists, or current auction is already settled, attempt
731
+ // to initialize a new auction for the input token ID and immediately
732
+ // return
733
+ if ((!_auction.initialized) || _auction.settled) {
734
+ _initializeAuctionWithBid(_projectId, _tokenId);
735
+ return;
736
+ }
737
+ // @dev this branch is guaranteed to have an initialized auction that
738
+ // not settled, so no need to check for initialized or not settled
739
+
740
+ // ensure bids for a specific token ID are only applied to the auction
741
+ // for that token ID.
742
+ require(
743
+ _auction.tokenId == _tokenId,
744
+ "Token ID does not match auction"
745
+ );
746
+
747
+ // ensure auction is not already ended
748
+ require(_auction.endTime > block.timestamp, "Auction already ended");
749
+
750
+ // require bid to be sufficiently greater than current highest bid
751
+ // @dev no overflow enforced automatically by solidity ^8.0.0
752
+ require(
753
+ msg.value >=
754
+ (_auction.currentBid *
755
+ (100 + minterMinBidIncrementPercentage)) /
756
+ 100,
757
+ "Bid is too low"
758
+ );
759
+
760
+ // EFFECTS
761
+ // record previous highest bid details for refunding
762
+ uint256 previousBid = _auction.currentBid;
763
+ address payable previousBidder = _auction.currentBidder;
764
+
765
+ // update auction state
766
+ _auction.currentBid = msg.value;
767
+ _auction.currentBidder = payable(msg.sender);
768
+ uint256 minEndTime = block.timestamp + minterTimeBufferSeconds;
769
+ if (_auction.endTime < minEndTime) {
770
+ _auction.endTime = minEndTime.toUint64();
771
+ }
772
+
773
+ // INTERACTIONS
774
+ // refund previous highest bidder
775
+ _safeTransferETHWithFallback(previousBidder, previousBid);
776
+
777
+ emit AuctionBid(_tokenId, msg.sender, msg.value);
778
+ }
779
+
780
+ /**
781
+ * @notice Inactive function - see `createBid` or
782
+ * `settleAuctionAndCreateBid`
783
+ */
784
+ function purchase(
785
+ uint256 /*_projectId*/
786
+ ) external payable returns (uint256 /*tokenId*/) {
787
+ revert("Inactive function");
788
+ }
789
+
790
+ /**
791
+ * @notice Inactive function - see `createBid` or
792
+ * `settleAuctionAndCreateBid`
793
+ */
794
+ function purchaseTo(
795
+ address /*_to*/,
796
+ uint256 /*_projectId*/
797
+ ) external payable returns (uint256 /*tokenId*/) {
798
+ revert("Inactive function");
799
+ }
800
+
801
+ /**
802
+ * @notice View function to return the current minter-level configuration
803
+ * details.
804
+ * @return minAuctionDurationSeconds_ Minimum auction duration in seconds
805
+ * @return maxAuctionDurationSeconds_ Maximum auction duration in seconds
806
+ * @return minterMinBidIncrementPercentage_ Minimum bid increment percentage
807
+ * @return minterTimeBufferSeconds_ Buffer time in seconds
808
+ * @return minterRefundGasLimit_ Gas limit for refunding ETH
809
+ */
810
+ function minterConfigurationDetails()
811
+ external
812
+ view
813
+ returns (
814
+ uint32 minAuctionDurationSeconds_,
815
+ uint32 maxAuctionDurationSeconds_,
816
+ uint8 minterMinBidIncrementPercentage_,
817
+ uint32 minterTimeBufferSeconds_,
818
+ uint16 minterRefundGasLimit_
819
+ )
820
+ {
821
+ minAuctionDurationSeconds_ = minAuctionDurationSeconds;
822
+ maxAuctionDurationSeconds_ = maxAuctionDurationSeconds;
823
+ minterMinBidIncrementPercentage_ = minterMinBidIncrementPercentage;
824
+ minterTimeBufferSeconds_ = minterTimeBufferSeconds;
825
+ minterRefundGasLimit_ = minterRefundGasLimit;
826
+ }
827
+
828
+ /**
829
+ * @notice projectId => has project reached its maximum number of
830
+ * invocations? Note that this returns a local cache of the core contract's
831
+ * state, and may be out of sync with the core contract. This is
832
+ * intentional, as it only enables gas optimization of mints after a
833
+ * project's maximum invocations has been reached. A false negative will
834
+ * only result in a gas cost increase, since the core contract will still
835
+ * enforce a maxInvocation check during minting. A false positive is not
836
+ * possible because the V3 core contract only allows maximum invocations
837
+ * to be reduced, not increased. Based on this rationale, we intentionally
838
+ * do not do input validation in this method as to whether or not the input
839
+ * `_projectId` is an existing project ID.
840
+ *
841
+ */
842
+ function projectMaxHasBeenInvoked(
843
+ uint256 _projectId
844
+ ) external view returns (bool) {
845
+ return projectConfig[_projectId].maxHasBeenInvoked;
846
+ }
847
+
848
+ /**
849
+ * @notice projectId => project's maximum number of invocations.
850
+ * Optionally synced with core contract value, for gas optimization.
851
+ * Note that this returns a local cache of the core contract's
852
+ * state, and may be out of sync with the core contract. This is
853
+ * intentional, as it only enables gas optimization of mints after a
854
+ * project's maximum invocations has been reached.
855
+ * @dev A number greater than the core contract's project max invocations
856
+ * will only result in a gas cost increase, since the core contract will
857
+ * still enforce a maxInvocation check during minting. A number less than
858
+ * the core contract's project max invocations is only possible when the
859
+ * project's max invocations have not been synced on this minter, since the
860
+ * V3 core contract only allows maximum invocations to be reduced, not
861
+ * increased. When this happens, the minter will enable minting, allowing
862
+ * the core contract to enforce the max invocations check. Based on this
863
+ * rationale, we intentionally do not do input validation in this method as
864
+ * to whether or not the input `_projectId` is an existing project ID.
865
+ */
866
+ function projectMaxInvocations(
867
+ uint256 _projectId
868
+ ) external view returns (uint256) {
869
+ return uint256(projectConfig[_projectId].maxInvocations);
870
+ }
871
+
872
+ /**
873
+ * @notice projectId => project configuration details.
874
+ * Note that in the case of no auction being initialized for the project,
875
+ * the returned `auction` will be the default struct.
876
+ * @param _projectId The project ID
877
+ * @return maxInvocations The project's maximum number of invocations
878
+ * allowed on this minter
879
+ * @return timestampStart The project's start timestamp, after which new
880
+ * auctions may be created (one at a time)
881
+ * @return auctionDurationSeconds The project's default auction duration,
882
+ * before any extensions due to buffer time
883
+ * @return basePrice The project's minimum starting bid price
884
+ * @return nextTokenNumberIsPopulated Whether or not the project's next
885
+ * token number has been populated
886
+ * @return nextTokenNumber The project's next token number to be auctioned,
887
+ * dummy value of 0 if `nextTokenNumberIsPopulated` is false. Note that 0
888
+ * is a valid token number, so `nextTokenNumberIsPopulated` should be used
889
+ * to distinguish between a valid token number of 0 and a dummy value of 0.
890
+ * @return auction The project's active auction details. Will be the
891
+ * default struct (w/ `auction.initialized = false`) if no auction has been
892
+ * initialized for the project.
893
+ */
894
+ function projectConfigurationDetails(
895
+ uint256 _projectId
896
+ )
897
+ external
898
+ view
899
+ returns (
900
+ uint24 maxInvocations,
901
+ uint64 timestampStart,
902
+ uint32 auctionDurationSeconds,
903
+ uint256 basePrice,
904
+ bool nextTokenNumberIsPopulated,
905
+ uint24 nextTokenNumber,
906
+ Auction memory auction
907
+ )
908
+ {
909
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
910
+ maxInvocations = _projectConfig.maxInvocations;
911
+ timestampStart = _projectConfig.timestampStart;
912
+ auctionDurationSeconds = _projectConfig.auctionDurationSeconds;
913
+ basePrice = _projectConfig.basePrice;
914
+ nextTokenNumberIsPopulated = _projectConfig.nextTokenNumberIsPopulated;
915
+ nextTokenNumber = _projectConfig.nextTokenNumberIsPopulated
916
+ ? _projectConfig.nextTokenNumber
917
+ : 0;
918
+ auction = _projectConfig.activeAuction;
919
+ }
920
+
921
+ /**
922
+ * @notice projectId => active auction details.
923
+ * @dev reverts if no auction exists for the project.
924
+ */
925
+ function projectActiveAuctionDetails(
926
+ uint256 _projectId
927
+ ) external view returns (Auction memory auction) {
928
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
929
+ auction = _projectConfig.activeAuction;
930
+ // do not return uninitialized auctions (i.e. auctions that do not
931
+ // exist, and therefore are simply the default struct)
932
+ require(auction.initialized, "No auction exists on project");
933
+ return auction;
934
+ }
935
+
936
+ /**
937
+ * @notice Convenience function that returns either the current token ID
938
+ * being auctioned, or the next expected token ID to be auction if no
939
+ * auction is currently initialized or if the current auction has concluded
940
+ * (block.timestamp > auction.endTime).
941
+ * This is intended to be useful for frontends or scripts that intend to
942
+ * call `createBid` or `settleAuctionAndCreateBid`, which requires a
943
+ * target bid token ID to be passed in as an argument.
944
+ * The function reverts if a project does not have an active auction and
945
+ * the next expected token ID has not been populated.
946
+ * @param _projectId The project ID being queried
947
+ * @return The current token ID being auctioned, or the next token ID to be
948
+ * auctioned if a new auction is ready to be created.
949
+ */
950
+ function getTokenToBid(uint256 _projectId) external view returns (uint256) {
951
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
952
+ Auction storage _auction = _projectConfig.activeAuction;
953
+ // if project has an active token auction that is not settled, return
954
+ // that token ID
955
+ if (_auction.initialized && (_auction.endTime > block.timestamp)) {
956
+ return _auction.tokenId;
957
+ }
958
+ // otherwise, return the next expected token ID to be auctioned
959
+ return getNextTokenId(_projectId);
960
+ }
961
+
962
+ /**
963
+ * @notice View function that returns the next token ID to be auctioned
964
+ * by this minter for project `_projectId`.
965
+ * Reverts if the next token ID has not been populated for the project.
966
+ * @param _projectId The project ID being queried
967
+ * @return nextTokenId The next token ID to be auctioned by this minter
968
+ */
969
+ function getNextTokenId(
970
+ uint256 _projectId
971
+ ) public view returns (uint256 nextTokenId) {
972
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
973
+ if (!_projectConfig.nextTokenNumberIsPopulated) {
974
+ revert("Next token not populated");
975
+ }
976
+ // @dev overflow automatically checked in Solidity ^0.8.0
977
+ nextTokenId =
978
+ (_projectId * ONE_MILLION) +
979
+ _projectConfig.nextTokenNumber;
980
+ return nextTokenId;
981
+ }
982
+
983
+ /**
984
+ * @dev Internal function to initialize an auction for the next token ID
985
+ * on project `_projectId` with a bid of `msg.value` from `msg.sender`.
986
+ * This function reverts in any of the following cases:
987
+ * - project is not configured on this minter
988
+ * - project is configured but has not yet reached its start time
989
+ * - project has a current active auction that is not settled
990
+ * - insufficient bid amount (msg.value < basePrice)
991
+ * - no next token has been minted for the project (artist may need to
992
+ * call `tryPopulateNextToken`)
993
+ * - `_targetTokenId` does not match the next token ID for the project
994
+ * After initializing a new auction, this function attempts to mint a new
995
+ * token and assign it to the project's next token slot, in preparation for
996
+ * a future token auction. However, if the project has reached its maximum
997
+ * invocations on either the core contract or minter, the next token slot
998
+ * for the project will remain empty.
999
+ * @dev This should be executed in a nonReentrant context to provide redundant
1000
+ * protection against reentrancy.
1001
+ */
1002
+ function _initializeAuctionWithBid(
1003
+ uint256 _projectId,
1004
+ uint256 _targetTokenId
1005
+ ) internal {
1006
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1007
+ Auction storage _auction = _projectConfig.activeAuction;
1008
+ // CHECKS
1009
+ // ensure project auctions are configured
1010
+ // @dev base price of zero indicates auctions are not configured
1011
+ // because only base price of gt zero is allowed when configuring
1012
+ require(_projectIsConfigured(_projectConfig), "Project not configured");
1013
+ // only initialize new auctions if they meet the start time
1014
+ // requirement
1015
+ require(
1016
+ block.timestamp >= _projectConfig.timestampStart,
1017
+ "Only gte project start time"
1018
+ );
1019
+ // the following require statement is redundant based on how this
1020
+ // internal function is called, but it is included for protection
1021
+ // against future changes that could easily introduce a bug if this
1022
+ // check is not present
1023
+ // @dev no cover else branch of next line because unreachable
1024
+ require(
1025
+ (!_auction.initialized) || _auction.settled,
1026
+ "Existing auction not settled"
1027
+ );
1028
+ // require valid bid value
1029
+ require(
1030
+ msg.value >= _projectConfig.basePrice,
1031
+ "Insufficient initial bid"
1032
+ );
1033
+ // require next token number is populated
1034
+ // @dev this should only be encountered if the project has reached
1035
+ // its maximum invocations on either the core contract or minter
1036
+ require(
1037
+ _projectConfig.nextTokenNumberIsPopulated,
1038
+ "No next token, check max invocations"
1039
+ );
1040
+ // require next token number is the target token ID
1041
+ require(
1042
+ _projectConfig.nextTokenNumber == _targetTokenId % ONE_MILLION,
1043
+ "Incorrect target token ID"
1044
+ );
1045
+
1046
+ // EFFECTS
1047
+ // create new auction, overwriting previous auction if it exists
1048
+ uint64 endTime = (block.timestamp +
1049
+ _projectConfig.auctionDurationSeconds).toUint64();
1050
+ _projectConfig.activeAuction = Auction({
1051
+ tokenId: _targetTokenId,
1052
+ currentBid: msg.value,
1053
+ currentBidder: payable(msg.sender),
1054
+ endTime: endTime,
1055
+ settled: false,
1056
+ initialized: true
1057
+ });
1058
+ // mark next token number as not populated
1059
+ // @dev intentionally not setting nextTokenNumber to zero to avoid
1060
+ // unnecessary gas costs
1061
+ _projectConfig.nextTokenNumberIsPopulated = false;
1062
+
1063
+ // @dev we intentionally emit event here due to potential of early
1064
+ // return in INTERACTIONS section
1065
+ emit AuctionInitialized(_targetTokenId, msg.sender, msg.value, endTime);
1066
+
1067
+ // INTERACTIONS
1068
+ // attempt to mint new token to this minter contract, only if max
1069
+ // invocations has not been reached
1070
+ _tryMintTokenToNextSlot(_projectId);
1071
+ }
1072
+
1073
+ /**
1074
+ * @notice Internal function that attempts to mint a new token to the next
1075
+ * token slot for the project `_projectId`.
1076
+ * This function returns early and does not modify state if
1077
+ * - the project has reached its maximum invocations on either the core
1078
+ * contract or minter
1079
+ * - the project config's `nextTokenNumberIsPopulated` is already true
1080
+ * @param _projectId The ID of the project to mint a new token for.
1081
+ */
1082
+ function _tryMintTokenToNextSlot(uint256 _projectId) internal {
1083
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1084
+ if (_projectConfig.nextTokenNumberIsPopulated) {
1085
+ return;
1086
+ }
1087
+ // INTERACTIONS
1088
+ // attempt to mint new token to this minter contract, only if max
1089
+ // invocations has not been reached
1090
+ // we require up-to-date invocation data to properly handle last token
1091
+ (
1092
+ uint256 coreInvocations,
1093
+ uint256 coreMaxInvocations,
1094
+ ,
1095
+ ,
1096
+ ,
1097
+
1098
+ ) = genArtCoreContract_Base.projectStateData(_projectId);
1099
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
1100
+ uint256 minMaxInvocations = Math.min(
1101
+ coreMaxInvocations,
1102
+ localMaxInvocations
1103
+ );
1104
+ if (coreInvocations >= minMaxInvocations) {
1105
+ // we have reached the max invocations, so we do not mint a new
1106
+ // token as the "next token", and leave the next token number as
1107
+ // not populated
1108
+ return;
1109
+ }
1110
+ // @dev this is an effect after a trusted contract interaction
1111
+ _projectConfig.nextTokenNumberIsPopulated = true;
1112
+ // mint a new token to this project's "next token" slot
1113
+ // @dev this is an interaction with a trusted contract
1114
+ uint256 nextTokenId = minterFilter.mint(
1115
+ address(this),
1116
+ _projectId,
1117
+ address(this)
1118
+ );
1119
+ // update state to reflect new token number
1120
+ // @dev state changes after trusted contract interaction
1121
+ // @dev unchecked is safe because mod 1e6 is guaranteed to be less than
1122
+ // max uint24
1123
+ unchecked {
1124
+ _projectConfig.nextTokenNumber = uint24(nextTokenId % ONE_MILLION);
1125
+ }
1126
+ // update local maxHasBeenInvoked value if necessary
1127
+ uint256 tokenInvocation = (nextTokenId % ONE_MILLION) + 1;
1128
+ if (tokenInvocation == localMaxInvocations) {
1129
+ _projectConfig.maxHasBeenInvoked = true;
1130
+ }
1131
+ emit ProjectNextTokenUpdated(_projectId, nextTokenId);
1132
+ }
1133
+
1134
+ /**
1135
+ * @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and send it as WETH.
1136
+ */
1137
+ function _safeTransferETHWithFallback(address to, uint256 amount) internal {
1138
+ (bool success, ) = to.call{value: amount, gas: minterRefundGasLimit}(
1139
+ ""
1140
+ );
1141
+ if (!success) {
1142
+ weth.deposit{value: amount}();
1143
+ weth.transfer(to, amount);
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * @notice Determines if a project is configured or not on this minter.
1149
+ * Uses project config's `basePrice` to determine if project is configured,
1150
+ * because `basePrice` is the only required field for a project to be
1151
+ * non-zero when configured.
1152
+ * @param _projectConfig The project config to check.
1153
+ */
1154
+ function _projectIsConfigured(
1155
+ ProjectConfig storage _projectConfig
1156
+ ) internal view returns (bool) {
1157
+ return _projectConfig.basePrice > 0;
1158
+ }
1159
+
1160
+ /**
1161
+ * @notice Gets price info to become the leading bidder on a token auction.
1162
+ * If artist has not called `configureFutureAuctions` and there is no
1163
+ * active token auction accepting bids, `isConfigured` will be false, and a
1164
+ * dummy price of zero is assigned to `tokenPriceInWei`.
1165
+ * If there is an active auction accepting bids, `isConfigured` will be
1166
+ * true, and `tokenPriceInWei` will be the sum of the current bid value and
1167
+ * the minimum bid increment due to the minter's
1168
+ * `minterMinBidIncrementPercentage`.
1169
+ * If there is an auction that has ended (no longer accepting bids), but
1170
+ * the project is configured, `isConfigured` will be true, and
1171
+ * `tokenPriceInWei` will be the minimum initial bid price for the next
1172
+ * token auction.
1173
+ * Also returns currency symbol and address to be being used as payment,
1174
+ * which for this minter is ETH only.
1175
+ * @param _projectId Project ID to get price information for.
1176
+ * @return isConfigured true only if project auctions are configured.
1177
+ * @return tokenPriceInWei price in wei to become the leading bidder on a
1178
+ * token auction.
1179
+ * @return currencySymbol currency symbol for purchases of project on this
1180
+ * minter. This minter always returns "ETH"
1181
+ * @return currencyAddress currency address for purchases of project on
1182
+ * this minter. This minter always returns null address, reserved for ether
1183
+ */
1184
+ function getPriceInfo(
1185
+ uint256 _projectId
1186
+ )
1187
+ external
1188
+ view
1189
+ returns (
1190
+ bool isConfigured,
1191
+ uint256 tokenPriceInWei,
1192
+ string memory currencySymbol,
1193
+ address currencyAddress
1194
+ )
1195
+ {
1196
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1197
+ Auction storage _auction = _projectConfig.activeAuction;
1198
+ // base price of zero not allowed when configuring auctions, so use it
1199
+ // as indicator of whether auctions are configured for the project
1200
+ bool projectIsConfigured = _projectIsConfigured(_projectConfig);
1201
+ bool auctionIsAcceptingBids = (_auction.initialized &&
1202
+ block.timestamp < _auction.endTime);
1203
+ isConfigured = projectIsConfigured || auctionIsAcceptingBids;
1204
+ // only return non-zero price if auction is configured
1205
+ if (isConfigured) {
1206
+ if (auctionIsAcceptingBids) {
1207
+ // return current bid plus minimum bid increment
1208
+ // @dev overflow automatically checked in Solidity ^0.8.0
1209
+ tokenPriceInWei =
1210
+ (_auction.currentBid *
1211
+ (100 + minterMinBidIncrementPercentage)) /
1212
+ 100;
1213
+ } else {
1214
+ // return base (starting) price if if current auction is not
1215
+ // accepting bids (i.e. the minimum initial bid price for the
1216
+ // next token auction)
1217
+ tokenPriceInWei = _projectConfig.basePrice;
1218
+ }
1219
+ }
1220
+ // else leave tokenPriceInWei as default value of zero
1221
+ currencySymbol = "ETH";
1222
+ currencyAddress = address(0);
1223
+ }
1224
+ }