@artblocks/contracts 1.0.0 → 1.0.1

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 (68) hide show
  1. package/README.md +3 -9
  2. package/artifacts/contracts/AdminACLV1.sol/AdminACLV1.json +2 -2
  3. package/artifacts/contracts/BasicPolyptychRandomizerV0.sol/BasicPolyptychRandomizerV0.json +229 -0
  4. package/artifacts/contracts/BasicRandomizerV2.sol/BasicRandomizerV2.json +3 -3
  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/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV0.sol/MinterDAExpV0.json +2 -2
  8. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV1.sol/MinterDAExpV1.json +2 -2
  9. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV2.sol/MinterDAExpV2.json +2 -2
  10. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExp/MinterDAExpV3.sol/MinterDAExpV3.json +2 -2
  11. package/artifacts/contracts/archive/minter-suite/Minters/MinterDAExpSettlement/MinterDAExpSettlementV0.sol/MinterDAExpSettlementV0.json +2 -2
  12. package/artifacts/contracts/{minter-suite/Minters → archive/minter-suite/Minters/MinterDAExpSettlement}/MinterDAExpSettlementV1.sol/MinterDAExpSettlementV1.json +3 -3
  13. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV0.sol/MinterDALinV0.json +2 -2
  14. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV1.sol/MinterDALinV1.json +2 -2
  15. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV2.sol/MinterDALinV2.json +2 -2
  16. package/artifacts/contracts/archive/minter-suite/Minters/MinterDALin/MinterDALinV3.sol/MinterDALinV3.json +2 -2
  17. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV0.sol/MinterHolderV0.json +2 -2
  18. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV1.sol/MinterHolderV1.json +2 -2
  19. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV2.sol/MinterHolderV2.json +2 -2
  20. package/artifacts/contracts/archive/minter-suite/Minters/MinterHolder/MinterHolderV3.sol/MinterHolderV3.json +2 -2
  21. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV0.sol/MinterMerkleV0.json +2 -2
  22. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV1.sol/MinterMerkleV1.json +2 -2
  23. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV2.sol/MinterMerkleV2.json +2 -2
  24. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV3.sol/MinterMerkleV3.json +2 -2
  25. package/artifacts/contracts/archive/minter-suite/Minters/MinterMerkle/MinterMerkleV4.sol/MinterMerkleV4.json +2 -2
  26. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV0.sol/MinterSetPriceV0.json +2 -2
  27. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV1.sol/MinterSetPriceV1.json +2 -2
  28. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV2.sol/MinterSetPriceV2.json +2 -2
  29. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPrice/MinterSetPriceV3.sol/MinterSetPriceV3.json +2 -2
  30. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V0.sol/MinterSetPriceERC20V0.json +2 -2
  31. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V1.sol/MinterSetPriceERC20V1.json +2 -2
  32. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V2.sol/MinterSetPriceERC20V2.json +2 -2
  33. package/artifacts/contracts/archive/minter-suite/Minters/MinterSetPriceERC20/MinterSetPriceERC20V3.sol/MinterSetPriceERC20V3.json +2 -2
  34. package/artifacts/contracts/explorations/GenArt721CoreV3_Explorations.sol/GenArt721CoreV3_Explorations.json +2 -2
  35. package/artifacts/contracts/interfaces/0.8.x/IFilteredMinterDAExpSettlementV1.sol/IFilteredMinterDAExpSettlementV1.json +767 -0
  36. package/artifacts/contracts/interfaces/0.8.x/IMinterFilterV0.sol/IMinterFilterV0.json +6 -0
  37. package/artifacts/contracts/interfaces/0.8.x/IRandomizerPolyptychV0.sol/IRandomizerPolyptychV0.json +87 -0
  38. package/artifacts/contracts/interfaces/0.8.x/IRandomizerV2.sol/IRandomizerV2.json +1 -1
  39. package/artifacts/contracts/minter-suite/MinterFilter/MinterFilterV0.sol/MinterFilterV0.json +8 -2
  40. package/artifacts/contracts/minter-suite/MinterFilter/MinterFilterV1.sol/MinterFilterV1.json +34 -2
  41. package/artifacts/contracts/minter-suite/Minters/MinterDAExpSettlementV2.sol/MinterDAExpSettlementV2.json +1127 -0
  42. package/artifacts/contracts/minter-suite/Minters/MinterDAExpV4.sol/MinterDAExpV4.json +15 -2
  43. package/artifacts/contracts/minter-suite/Minters/MinterDALinV4.sol/MinterDALinV4.json +15 -2
  44. package/artifacts/contracts/minter-suite/Minters/MinterHolderV4.sol/MinterHolderV4.json +15 -2
  45. package/artifacts/contracts/minter-suite/Minters/MinterMerkleV5.sol/MinterMerkleV5.json +15 -2
  46. package/artifacts/contracts/minter-suite/Minters/MinterPolyptychV0.sol/IGenArt721CoreContractV3WithRandomizer.json +662 -0
  47. package/artifacts/contracts/minter-suite/Minters/MinterPolyptychV0.sol/MinterPolyptychV0.json +1268 -0
  48. package/artifacts/contracts/minter-suite/Minters/MinterSetPriceERC20V4.sol/MinterSetPriceERC20V4.json +15 -2
  49. package/artifacts/contracts/minter-suite/Minters/MinterSetPriceV4.sol/MinterSetPriceV4.json +15 -2
  50. package/artifacts/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol/GenArt721CoreV3_Engine_IncorrectCoreType.json +2 -2
  51. package/artifacts/contracts/mock/RandomizerV2_NoAssignMock.sol/RandomizerV2_NoAssignMock.json +3 -3
  52. package/contracts/BasicPolyptychRandomizerV0.sol +126 -0
  53. package/contracts/BasicRandomizerV2.sol +3 -3
  54. package/contracts/{minter-suite/Minters → archive/minter-suite/Minters/MinterDAExpSettlement}/MinterDAExpSettlementV1.sol +4 -4
  55. package/contracts/interfaces/0.8.x/IFilteredMinterDAExpSettlementV1.sol +21 -0
  56. package/contracts/interfaces/0.8.x/IMinterFilterV0.sol +6 -0
  57. package/contracts/interfaces/0.8.x/IRandomizerPolyptychV0.sol +31 -0
  58. package/contracts/interfaces/0.8.x/IRandomizerV2.sol +5 -2
  59. package/contracts/minter-suite/MinterFilter/MinterFilterV1.sol +18 -0
  60. package/contracts/minter-suite/Minters/MinterDAExpSettlementV2.sol +1312 -0
  61. package/contracts/minter-suite/Minters/MinterDAExpV4.sol +32 -4
  62. package/contracts/minter-suite/Minters/MinterDALinV4.sol +32 -5
  63. package/contracts/minter-suite/Minters/MinterHolderV4.sol +35 -6
  64. package/contracts/minter-suite/Minters/MinterMerkleV5.sol +35 -6
  65. package/contracts/minter-suite/Minters/MinterPolyptychV0.sol +998 -0
  66. package/contracts/minter-suite/Minters/MinterSetPriceERC20V4.sol +35 -6
  67. package/contracts/minter-suite/Minters/MinterSetPriceV4.sol +32 -4
  68. package/package.json +8 -2
@@ -0,0 +1,1312 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-only
2
+ // Created By: Art Blocks Inc.
3
+
4
+ import "../../interfaces/0.8.x/IGenArt721CoreContractV3_Base.sol";
5
+ import "../../interfaces/0.8.x/IMinterFilterV0.sol";
6
+ import "../../interfaces/0.8.x/IFilteredMinterDAExpSettlementV1.sol";
7
+ import "./MinterBase_v0_1_1.sol";
8
+
9
+ import "@openzeppelin-4.7/contracts/security/ReentrancyGuard.sol";
10
+ import "@openzeppelin-4.7/contracts/utils/math/SafeCast.sol";
11
+
12
+ pragma solidity 0.8.17;
13
+
14
+ /**
15
+ * @title Filtered Minter contract that allows tokens to be minted with ETH.
16
+ * Pricing is achieved using an automated Dutch-auction mechanism, with a
17
+ * settlement mechanism for tokens purchased before the auction ends.
18
+ * This is designed to be used with GenArt721CoreContractV3 flagship or
19
+ * engine contracts.
20
+ * @author Art Blocks Inc.
21
+ * @notice Privileged Roles and Ownership:
22
+ * This contract is designed to be managed, with limited powers.
23
+ * Privileged roles and abilities are controlled by the core contract's Admin
24
+ * ACL contract and a project's artist. Both of these roles hold extensive
25
+ * power and can modify minter details.
26
+ * Care must be taken to ensure that the admin ACL contract and artist
27
+ * addresses are secure behind a multi-sig or other access control mechanism.
28
+ * Additionally, the purchaser of a token has some trust assumptions regarding
29
+ * settlement, beyond typical minter Art Blocks trust assumptions. In general,
30
+ * Artists and Admin are trusted to not abuse their powers in a way that
31
+ * would artifically inflate the sellout price of a project. They are
32
+ * incentivized to not do so, as it would diminish their reputation and
33
+ * ability to sell future projects. Agreements between Admin and Artist
34
+ * may or may not be in place to further dissuade artificial inflation of an
35
+ * auction's sellout price.
36
+ * ----------------------------------------------------------------------------
37
+ * The following functions are restricted to the core contract's Admin ACL
38
+ * contract:
39
+ * - setAllowablePriceDecayHalfLifeRangeSeconds (note: this range is only
40
+ * enforced when creating new auctions)
41
+ * - resetAuctionDetails (note: this will prevent minting until a new auction
42
+ * is created)
43
+ * - adminEmergencyReduceSelloutPrice
44
+ * ----------------------------------------------------------------------------
45
+ * The following functions are restricted to a project's artist or the core
46
+ * contract's Admin ACL contract:
47
+ * - withdrawArtistAndAdminRevenues (note: this may only be called after an
48
+ * auction has sold out or has reached base price)
49
+ * ----------------------------------------------------------------------------
50
+ * The following functions are restricted to a project's artist:
51
+ * - setAuctionDetails (note: this may only be called when there is no active
52
+ * auction, and must start at a price less than or equal to any previously
53
+ * made purchases)
54
+ * - setProjectMaxInvocations
55
+ * - manuallyLimitProjectMaxInvocations
56
+ * ----------------------------------------------------------------------------
57
+ * Additional admin and artist privileged roles may be described on other
58
+ * contracts that this minter integrates with.
59
+ *
60
+ * @dev Note that while this minter makes use of `block.timestamp` and it is
61
+ * technically possible that this value is manipulated by block producers via
62
+ * denial of service (in PoS), such manipulation will not have material impact
63
+ * on the price values of this minter given the business practices for how
64
+ * pricing is congfigured for this minter and that variations on the order of
65
+ * less than a minute should not meaningfully impact price given the minimum
66
+ * allowable price decay rate that this minter intends to support.
67
+ */
68
+ contract MinterDAExpSettlementV2 is
69
+ ReentrancyGuard,
70
+ MinterBase,
71
+ IFilteredMinterDAExpSettlementV1
72
+ {
73
+ using SafeCast for uint256;
74
+
75
+ /// Core contract address this minter interacts with
76
+ address public immutable genArt721CoreAddress;
77
+ /// The core contract integrates with V3 contracts
78
+ IGenArt721CoreContractV3_Base private immutable genArtCoreContract_Base;
79
+
80
+ /// Minter filter address this minter interacts with
81
+ address public immutable minterFilterAddress;
82
+
83
+ /// Minter filter this minter may interact with.
84
+ IMinterFilterV0 private immutable minterFilter;
85
+
86
+ /// minterType for this minter
87
+ string public constant minterType = "MinterDAExpSettlementV2";
88
+
89
+ uint256 constant ONE_MILLION = 1_000_000;
90
+
91
+ struct ProjectConfig {
92
+ // maxHasBeenInvoked is only true if invocations are equal to the
93
+ // locally limited max invocations value, `maxInvocations`. It may be
94
+ // stale (e.g. a different minter reachd max invocations), may return a
95
+ // false negative if stale, and must be accounted for in this minter's
96
+ // logic.
97
+ bool maxHasBeenInvoked;
98
+ // maxInvocations is the maximum number of tokens that may be minted
99
+ // for this project. The value here is cached on the minter, and may
100
+ // be out of sync with the core contract's value. It is guaranteed to
101
+ // be either manually populated or synced to the core contract value if
102
+ // an auction has been populated (i.e. no stale initial values). This
103
+ // behavior must be appropriately accounted for in this minter's logic.
104
+ uint24 maxInvocations;
105
+ // set to true only after artist + admin revenues have been collected
106
+ bool auctionRevenuesCollected;
107
+ // number of tokens minted that have potential of future settlement.
108
+ // max uint24 > 16.7 million tokens > 1 million tokens/project max
109
+ uint24 numSettleableInvocations;
110
+ // max uint64 ~= 1.8e19 sec ~= 570 billion years
111
+ uint64 timestampStart;
112
+ uint64 priceDecayHalfLifeSeconds;
113
+ // Prices are packed internally as uint128, resulting in a maximum
114
+ // allowed price of ~3.4e20 ETH. This is many orders of magnitude
115
+ // greater than current ETH supply.
116
+ uint128 startPrice;
117
+ // base price is non-zero for all configured auctions on this minter
118
+ uint128 basePrice;
119
+ // This value is only zero if no purchases have been made on this
120
+ // minter.
121
+ // When non-zero, this value is used as a reference when an auction is
122
+ // reset by admin, and then a new auction is configured by an artist.
123
+ // In that case, the new auction will be required to have a starting
124
+ // price less than or equal to this value, if one or more purchases
125
+ // have been made on this minter.
126
+ uint256 latestPurchasePrice;
127
+ }
128
+
129
+ mapping(uint256 => ProjectConfig) public projectConfig;
130
+
131
+ /// Minimum price decay half life: price must decay with a half life of at
132
+ /// least this amount (must cut in half at least every N seconds).
133
+ uint256 public minimumPriceDecayHalfLifeSeconds = 300; // 5 minutes
134
+ /// Maximum price decay half life: price may decay with a half life of no
135
+ /// more than this amount (may cut in half at no more than every N seconds).
136
+ uint256 public maximumPriceDecayHalfLifeSeconds = 3600; // 60 minutes
137
+
138
+ struct Receipt {
139
+ // max uint232 allows for > 1e51 ETH (much more than max supply)
140
+ uint232 netPosted;
141
+ // max uint24 still allows for > max project supply of 1 million tokens
142
+ uint24 numPurchased;
143
+ }
144
+ /// user address => project ID => receipt
145
+ mapping(address => mapping(uint256 => Receipt)) receipts;
146
+
147
+ // modifier to restrict access to only AdminACL or the artist
148
+ modifier onlyCoreAdminACLOrArtist(uint256 _projectId, bytes4 _selector) {
149
+ require(
150
+ (msg.sender ==
151
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)) ||
152
+ (
153
+ genArtCoreContract_Base.adminACLAllowed(
154
+ msg.sender,
155
+ address(this),
156
+ _selector
157
+ )
158
+ ),
159
+ "Only Artist or Admin ACL"
160
+ );
161
+ _;
162
+ }
163
+
164
+ // modifier to restrict access to only AdminACL allowed calls
165
+ // @dev defers which ACL contract is used to the core contract
166
+ modifier onlyCoreAdminACL(bytes4 _selector) {
167
+ require(
168
+ genArtCoreContract_Base.adminACLAllowed(
169
+ msg.sender,
170
+ address(this),
171
+ _selector
172
+ ),
173
+ "Only Core AdminACL allowed"
174
+ );
175
+ _;
176
+ }
177
+
178
+ modifier onlyArtist(uint256 _projectId) {
179
+ require(
180
+ (msg.sender ==
181
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)),
182
+ "Only Artist"
183
+ );
184
+ _;
185
+ }
186
+
187
+ /**
188
+ * @notice Initializes contract to be a Filtered Minter for
189
+ * `_minterFilter`, integrated with Art Blocks core contract
190
+ * at address `_genArt721Address`.
191
+ * @param _genArt721Address Art Blocks core contract address for
192
+ * which this contract will be a minter.
193
+ * @param _minterFilter Minter filter for which
194
+ * this will a filtered minter.
195
+ */
196
+ constructor(
197
+ address _genArt721Address,
198
+ address _minterFilter
199
+ ) ReentrancyGuard() MinterBase(_genArt721Address) {
200
+ genArt721CoreAddress = _genArt721Address;
201
+ // always populate immutable engine contracts, but only use appropriate
202
+ // interface based on isEngine in the rest of the contract
203
+ genArtCoreContract_Base = IGenArt721CoreContractV3_Base(
204
+ _genArt721Address
205
+ );
206
+ minterFilterAddress = _minterFilter;
207
+ minterFilter = IMinterFilterV0(_minterFilter);
208
+ require(
209
+ minterFilter.genArt721CoreAddress() == _genArt721Address,
210
+ "Illegal contract pairing"
211
+ );
212
+ }
213
+
214
+ /**
215
+ * @notice This function is intentionally not implemented for this version
216
+ * of the minter. Due to potential for unintended consequences, the
217
+ * function `manuallyLimitProjectMaxInvocations` should be used to manually
218
+ * and explicitly limit the maximum invocations for a project to a value
219
+ * other than the core contract's maximum invocations for a project.
220
+ * @param _projectId Project ID to set the maximum invocations for.
221
+ * @dev This function is included for interface conformance purposes only.
222
+ */
223
+ function setProjectMaxInvocations(
224
+ uint256 _projectId
225
+ ) external view onlyArtist(_projectId) {
226
+ revert("Not implemented");
227
+ }
228
+
229
+ /**
230
+ * @notice Manually sets the local maximum invocations of project `_projectId`
231
+ * with the provided `_maxInvocations`, checking that `_maxInvocations` is less
232
+ * than or equal to the value of project `_project_id`'s maximum invocations that is
233
+ * set on the core contract.
234
+ * @dev Note that a `_maxInvocations` of 0 can only be set if the current `invocations`
235
+ * value is also 0 and this would also set `maxHasBeenInvoked` to true, correctly short-circuiting
236
+ * this minter's purchase function, avoiding extra gas costs from the core contract's maxInvocations check.
237
+ * @param _projectId Project ID to set the maximum invocations for.
238
+ * @param _maxInvocations Maximum invocations to set for the project.
239
+ */
240
+ function manuallyLimitProjectMaxInvocations(
241
+ uint256 _projectId,
242
+ uint256 _maxInvocations
243
+ ) external onlyArtist(_projectId) {
244
+ // CHECKS
245
+ // require that new maxInvocations is greater than 0 to prevent
246
+ // accidental premature closure of a project when artist is
247
+ // configuring, forever preventing any purchases on this minter
248
+ require(_maxInvocations > 0, "Only max invocations gt 0");
249
+ // do not allow changing maxInvocations if maxHasBeenInvoked is true
250
+ // @dev this is a guardrail to prevent accidental re-opening of a
251
+ // completed project that is waiting for revenues to be withdrawn
252
+ // @dev intentionally do not refresh maxHasBeenInvoked here via
253
+ // `_refreshMaxInvocations` because in the edge case of a stale
254
+ // hasMaxBeenInvoked, it is too difficult to determine what the artist
255
+ // may or may not want to do
256
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
257
+ require(
258
+ !_projectConfig.maxHasBeenInvoked,
259
+ "Max invocations already reached"
260
+ );
261
+
262
+ // ensure that the manually set maxInvocations is not greater than what is set on the core contract
263
+ uint256 coreInvocations;
264
+ uint256 coreMaxInvocations;
265
+ (
266
+ coreInvocations,
267
+ coreMaxInvocations
268
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
269
+ require(
270
+ _maxInvocations <= coreMaxInvocations,
271
+ "Cannot increase project max invocations above core contract set project max invocations"
272
+ );
273
+ require(
274
+ _maxInvocations >= coreInvocations,
275
+ "Cannot set project max invocations to less than current invocations"
276
+ );
277
+ // EFFECTS
278
+ // update storage with results
279
+ _projectConfig.maxInvocations = uint24(_maxInvocations);
280
+ // We need to ensure maxHasBeenInvoked is correctly set after manually setting the
281
+ // local maxInvocations value.
282
+ _projectConfig.maxHasBeenInvoked = coreInvocations == _maxInvocations;
283
+
284
+ emit ProjectMaxInvocationsLimitUpdated(_projectId, _maxInvocations);
285
+ }
286
+
287
+ /**
288
+ * @notice Warning: Disabling purchaseTo is not supported on this minter.
289
+ * This method exists purely for interface-conformance purposes.
290
+ */
291
+ function togglePurchaseToDisabled(
292
+ uint256 _projectId
293
+ ) external view onlyArtist(_projectId) {
294
+ revert("Action not supported");
295
+ }
296
+
297
+ /**
298
+ * @notice projectId => has project reached its maximum number of
299
+ * invocations on this minter?
300
+ * Note that this returns a local cached value on the minter, and may be
301
+ * out of sync with the core core contract's state, in which case it may
302
+ * return a false negative.
303
+ */
304
+ function projectMaxHasBeenInvoked(
305
+ uint256 _projectId
306
+ ) external view returns (bool) {
307
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
308
+ return _projectConfig.maxHasBeenInvoked;
309
+ }
310
+
311
+ /**
312
+ * @notice projectId => project's maximum number of invocations.
313
+ * Note that this returns a local cached value, and may be manually
314
+ * limited to be different than the core contract's maxInvocations,
315
+ * or may be out of sync with the core contract's maxInvocations state.
316
+ */
317
+ function projectMaxInvocations(
318
+ uint256 _projectId
319
+ ) external view returns (uint256) {
320
+ return projectConfig[_projectId].maxInvocations;
321
+ }
322
+
323
+ /**
324
+ * @notice projectId => auction parameters
325
+ */
326
+ function projectAuctionParameters(
327
+ uint256 _projectId
328
+ )
329
+ external
330
+ view
331
+ returns (
332
+ uint256 timestampStart,
333
+ uint256 priceDecayHalfLifeSeconds,
334
+ uint256 startPrice,
335
+ uint256 basePrice
336
+ )
337
+ {
338
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
339
+ return (
340
+ _projectConfig.timestampStart,
341
+ _projectConfig.priceDecayHalfLifeSeconds,
342
+ _projectConfig.startPrice,
343
+ _projectConfig.basePrice
344
+ );
345
+ }
346
+
347
+ /**
348
+ * @notice Sets the minimum and maximum values that are settable for
349
+ * `_priceDecayHalfLifeSeconds` across all projects.
350
+ * @param _minimumPriceDecayHalfLifeSeconds Minimum price decay half life
351
+ * (in seconds).
352
+ * @param _maximumPriceDecayHalfLifeSeconds Maximum price decay half life
353
+ * (in seconds).
354
+ */
355
+ function setAllowablePriceDecayHalfLifeRangeSeconds(
356
+ uint256 _minimumPriceDecayHalfLifeSeconds,
357
+ uint256 _maximumPriceDecayHalfLifeSeconds
358
+ )
359
+ external
360
+ onlyCoreAdminACL(
361
+ this.setAllowablePriceDecayHalfLifeRangeSeconds.selector
362
+ )
363
+ {
364
+ require(
365
+ _maximumPriceDecayHalfLifeSeconds >
366
+ _minimumPriceDecayHalfLifeSeconds,
367
+ "Maximum half life must be greater than minimum"
368
+ );
369
+ require(
370
+ _minimumPriceDecayHalfLifeSeconds > 0,
371
+ "Half life of zero not allowed"
372
+ );
373
+ minimumPriceDecayHalfLifeSeconds = _minimumPriceDecayHalfLifeSeconds;
374
+ maximumPriceDecayHalfLifeSeconds = _maximumPriceDecayHalfLifeSeconds;
375
+ emit AuctionHalfLifeRangeSecondsUpdated(
376
+ _minimumPriceDecayHalfLifeSeconds,
377
+ _maximumPriceDecayHalfLifeSeconds
378
+ );
379
+ }
380
+
381
+ ////// Auction Functions
382
+ /**
383
+ * @notice Sets auction details for project `_projectId`.
384
+ * @param _projectId Project ID to set auction details for.
385
+ * @param _auctionTimestampStart Timestamp at which to start the auction.
386
+ * @param _priceDecayHalfLifeSeconds The half life with which to decay the
387
+ * price (in seconds).
388
+ * @param _startPrice Price at which to start the auction, in Wei.
389
+ * If a previous auction existed on this minter and at least one settleable
390
+ * purchase has been made, this value must be less than or equal to the
391
+ * price when the previous auction was paused. This enforces an overall
392
+ * monatonically decreasing auction. Must be greater than or equal to
393
+ * max(uint128) for internal storage packing purposes.
394
+ * @param _basePrice Resting price of the auction, in Wei. Must be greater
395
+ * than or equal to max(uint128) for internal storage packing purposes.
396
+ * @dev Note that setting the auction price explicitly to `0` is
397
+ * intentionally not allowed. This allows the minter to use the assumption
398
+ * that a price of `0` indicates that the auction is not configured.
399
+ * @dev Note that prices must be <= max(128) for internal storage packing
400
+ * efficiency purposes only. This function's interface remains unchanged
401
+ * for interface conformance purposes.
402
+ * @dev Note that this function also populates the local minter max
403
+ * invocation values for the project. This is done to ensure that the
404
+ * minter's local max invocation values are guarenteed to be at least
405
+ * populated when an auction is configured.
406
+ */
407
+ function setAuctionDetails(
408
+ uint256 _projectId,
409
+ uint256 _auctionTimestampStart,
410
+ uint256 _priceDecayHalfLifeSeconds,
411
+ uint256 _startPrice,
412
+ uint256 _basePrice
413
+ ) external onlyArtist(_projectId) {
414
+ // CHECKS
415
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
416
+ require(
417
+ _projectConfig.timestampStart == 0 ||
418
+ block.timestamp < _projectConfig.timestampStart,
419
+ "No modifications mid-auction"
420
+ );
421
+ require(
422
+ block.timestamp < _auctionTimestampStart,
423
+ "Only future auctions"
424
+ );
425
+ require(
426
+ _startPrice > _basePrice,
427
+ "Auction start price must be greater than auction end price"
428
+ );
429
+ // require _basePrice is non-zero to simplify logic of this minter
430
+ require(_basePrice > 0, "Base price must be non-zero");
431
+ // If previous purchases have been made, require monotonically
432
+ // decreasing purchase prices to preserve settlement and revenue
433
+ // claiming logic. Since base price is always non-zero, if
434
+ // latestPurchasePrice is zero, then no previous purchases have been
435
+ // made, and startPrice may be set to any value.
436
+ require(
437
+ _projectConfig.latestPurchasePrice == 0 || // never purchased
438
+ _startPrice <= _projectConfig.latestPurchasePrice,
439
+ "Auction start price must be <= latest purchase price"
440
+ );
441
+ require(
442
+ (_priceDecayHalfLifeSeconds >= minimumPriceDecayHalfLifeSeconds) &&
443
+ (_priceDecayHalfLifeSeconds <=
444
+ maximumPriceDecayHalfLifeSeconds),
445
+ "Price decay half life must fall between min and max allowable values"
446
+ );
447
+ // EFFECTS
448
+ _projectConfig.timestampStart = _auctionTimestampStart.toUint64();
449
+ _projectConfig.priceDecayHalfLifeSeconds = _priceDecayHalfLifeSeconds
450
+ .toUint64();
451
+ _projectConfig.startPrice = _startPrice.toUint128();
452
+ _projectConfig.basePrice = _basePrice.toUint128();
453
+
454
+ emit SetAuctionDetails(
455
+ _projectId,
456
+ _auctionTimestampStart,
457
+ _priceDecayHalfLifeSeconds,
458
+ _startPrice,
459
+ _basePrice
460
+ );
461
+
462
+ // refresh max invocations, ensuring the values are populated, and
463
+ // updating any local values that are illogical with respect to the
464
+ // current core contract state.
465
+ // @dev this refresh enables the guarantee that a project's max
466
+ // invocation state is always populated if an auction is configured.
467
+ _refreshMaxInvocations(_projectId);
468
+ }
469
+
470
+ /**
471
+ * @notice Resets auction details for project `_projectId`, zero-ing out all
472
+ * relevant auction fields. Not intended to be used in normal auction
473
+ * operation, but rather only in case of the need to reset an ongoing
474
+ * auction. An expected time this might occur would be when a frontend
475
+ * issue was occuring, and many typical users are actively being prevented
476
+ * from easily minting (even though minting would technically be possible
477
+ * directly from the contract).
478
+ * This function is only callable by the core admin during an active
479
+ * auction, before revenues have been collected.
480
+ * The price at the time of the reset will be the maximum starting price
481
+ * when re-configuring the next auction if one or more settleable purchases
482
+ * have been made.
483
+ * This is to ensure that purchases up through the block that this is
484
+ * called on will remain settleable, and that revenue claimed does not
485
+ * surpass (payments - excess_settlement_funds) for a given project.
486
+ * @param _projectId Project ID to set auction details for.
487
+ */
488
+ function resetAuctionDetails(
489
+ uint256 _projectId
490
+ ) external onlyCoreAdminACL(this.resetAuctionDetails.selector) {
491
+ // CHECKS
492
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
493
+ require(_projectConfig.startPrice != 0, "Auction must be configured");
494
+ // no reset after revenues collected, since that solidifies amount due
495
+ require(
496
+ !_projectConfig.auctionRevenuesCollected,
497
+ "Only before revenues collected"
498
+ );
499
+ // EFFECTS
500
+ // reset to initial values
501
+ _projectConfig.timestampStart = 0;
502
+ _projectConfig.priceDecayHalfLifeSeconds = 0;
503
+ _projectConfig.startPrice = 0;
504
+ _projectConfig.basePrice = 0;
505
+ // Since auction revenues have not been collected, we can safely assume
506
+ // that numSettleableInvocations is the number of purchases made on
507
+ // this minter. A dummy value of 0 is used for latest purchase price if
508
+ // no purchases have been made.
509
+ emit ResetAuctionDetails(
510
+ _projectId,
511
+ _projectConfig.numSettleableInvocations,
512
+ _projectConfig.latestPurchasePrice
513
+ );
514
+ }
515
+
516
+ /**
517
+ * @notice This represents an admin stepping in and reducing the sellout
518
+ * price of an auction. This is only callable by the core admin, only
519
+ * after the auction is complete, but before project revenues are
520
+ * withdrawn.
521
+ * This is only intended to be used in the case where for some reason, the
522
+ * sellout price was too high.
523
+ * @param _projectId Project ID to reduce auction sellout price for.
524
+ * @param _newSelloutPrice New sellout price to set for the auction. Must
525
+ * be less than the current sellout price.
526
+ */
527
+ function adminEmergencyReduceSelloutPrice(
528
+ uint256 _projectId,
529
+ uint256 _newSelloutPrice
530
+ )
531
+ external
532
+ onlyCoreAdminACL(this.adminEmergencyReduceSelloutPrice.selector)
533
+ {
534
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
535
+ require(
536
+ !_projectConfig.auctionRevenuesCollected,
537
+ "Only before revenues collected"
538
+ );
539
+
540
+ // refresh max invocations, updating any local values that are
541
+ // illogical with respect to the current core contract state, and
542
+ // ensuring that local hasMaxBeenInvoked is accurate.
543
+ _refreshMaxInvocations(_projectId);
544
+
545
+ // require max invocations has been reached
546
+ require(_projectConfig.maxHasBeenInvoked, "Auction must be complete");
547
+ // @dev no need to check that auction max invocations has been reached,
548
+ // because if it was, the sellout price will be zero, and the following
549
+ // check will fail.
550
+ require(
551
+ _newSelloutPrice < _projectConfig.latestPurchasePrice,
552
+ "May only reduce sellout price"
553
+ );
554
+ require(
555
+ _newSelloutPrice >= _projectConfig.basePrice,
556
+ "May only reduce sellout price to base price or greater"
557
+ );
558
+ // ensure _newSelloutPrice is non-zero
559
+ require(_newSelloutPrice > 0, "Only sellout prices > 0");
560
+ _projectConfig.latestPurchasePrice = _newSelloutPrice;
561
+ emit SelloutPriceUpdated(_projectId, _newSelloutPrice);
562
+ }
563
+
564
+ /**
565
+ * @notice This withdraws project revenues for the artist and admin.
566
+ * This function is only callable by the artist or admin, and only after
567
+ * one of the following is true:
568
+ * - the auction has sold out above base price
569
+ * - the auction has reached base price
570
+ * Note that revenues are not claimable if in a temporary state after
571
+ * an auction is reset.
572
+ * Revenues may only be collected a single time per project.
573
+ * After revenues are collected, auction parameters will never be allowed
574
+ * to be reset, and excess settlement funds will become immutable and fully
575
+ * deterministic.
576
+ */
577
+ function withdrawArtistAndAdminRevenues(
578
+ uint256 _projectId
579
+ )
580
+ external
581
+ nonReentrant
582
+ onlyCoreAdminACLOrArtist(
583
+ _projectId,
584
+ this.withdrawArtistAndAdminRevenues.selector
585
+ )
586
+ {
587
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
588
+ // CHECKS
589
+ // require revenues to not have already been collected
590
+ require(
591
+ !_projectConfig.auctionRevenuesCollected,
592
+ "Revenues already collected"
593
+ );
594
+
595
+ // refresh max invocations, updating any local values that are
596
+ // illogical with respect to the current core contract state, and
597
+ // ensuring that local hasMaxBeenInvoked is accurate.
598
+ _refreshMaxInvocations(_projectId);
599
+
600
+ // get the current net price of the auction - reverts if no auction
601
+ // is configured.
602
+ // @dev we use _getPriceUnsafe here, since we just safely synced the
603
+ // project's max invocations and maxHasBeenInvoked, which guarantees
604
+ // an accurate price calculation from _getPriceUnsafe, while being
605
+ // more gas efficient than _getPriceSafe.
606
+ // @dev price is guaranteed <= _projectConfig.latestPurchasePrice,
607
+ // since this minter enforces monotonically decreasing purchase prices.
608
+ uint256 _price = _getPriceUnsafe(_projectId);
609
+ // if the price is not base price, require that the auction have
610
+ // reached max invocations. This prevents premature withdrawl
611
+ // before final auction price is possible to know.
612
+ if (_price != _projectConfig.basePrice) {
613
+ // @dev we can trust maxHasBeenInvoked, since we just
614
+ // refreshed it above with _refreshMaxInvocations, preventing any
615
+ // false negatives
616
+ require(
617
+ _projectConfig.maxHasBeenInvoked,
618
+ "Active auction not yet sold out"
619
+ );
620
+ } else {
621
+ uint256 basePrice = _projectConfig.basePrice;
622
+ // base price of zero indicates no sales, since base price of zero
623
+ // is not allowed when configuring an auction.
624
+ require(basePrice > 0, "Only latestPurchasePrice > 0");
625
+ // update the latest purchase price to the base price, to ensure
626
+ // the base price is used for all future settlement calculations
627
+ _projectConfig.latestPurchasePrice = basePrice;
628
+ }
629
+ // EFFECTS
630
+ _projectConfig.auctionRevenuesCollected = true;
631
+ // if the price is base price, the auction is valid and may be claimed
632
+ // calculate the artist and admin revenues
633
+ uint256 netRevenues = _projectConfig.numSettleableInvocations * _price;
634
+ // INTERACTIONS
635
+ splitRevenuesETH(_projectId, netRevenues, genArt721CoreAddress);
636
+ emit ArtistAndAdminRevenuesWithdrawn(_projectId);
637
+ }
638
+
639
+ /**
640
+ * @notice Purchases a token from project `_projectId`.
641
+ * @param _projectId Project ID to mint a token on.
642
+ * @return tokenId Token ID of minted token
643
+ */
644
+ function purchase(
645
+ uint256 _projectId
646
+ ) external payable returns (uint256 tokenId) {
647
+ tokenId = purchaseTo_do6(msg.sender, _projectId);
648
+ return tokenId;
649
+ }
650
+
651
+ /**
652
+ * @notice gas-optimized version of purchase(uint256).
653
+ */
654
+ function purchase_H4M(
655
+ uint256 _projectId
656
+ ) external payable returns (uint256 tokenId) {
657
+ tokenId = purchaseTo_do6(msg.sender, _projectId);
658
+ return tokenId;
659
+ }
660
+
661
+ /**
662
+ * @notice Purchases a token from project `_projectId` and sets
663
+ * the token's owner to `_to`.
664
+ * @param _to Address to be the new token's owner.
665
+ * @param _projectId Project ID to mint a token on.
666
+ * @return tokenId Token ID of minted token
667
+ */
668
+ function purchaseTo(
669
+ address _to,
670
+ uint256 _projectId
671
+ ) external payable returns (uint256 tokenId) {
672
+ return purchaseTo_do6(_to, _projectId);
673
+ }
674
+
675
+ /**
676
+ * @notice gas-optimized version of purchaseTo(address, uint256).
677
+ */
678
+ function purchaseTo_do6(
679
+ address _to,
680
+ uint256 _projectId
681
+ ) public payable nonReentrant returns (uint256 tokenId) {
682
+ // CHECKS
683
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
684
+
685
+ // Enforce the local limit of maxHasBeenInvoked, which is guaranteed
686
+ // to be populated for all configured auctions.
687
+ // @dev maxHasBeenInvoked can be sale and return a false negative.
688
+ // protect against that case by checking minted token's invocation
689
+ // against this minter's local max invocations immediately after
690
+ // receiving the newly minted tokenID.
691
+ require(
692
+ !_projectConfig.maxHasBeenInvoked,
693
+ "Maximum number of invocations reached"
694
+ );
695
+
696
+ // _getPriceUnsafe reverts if auction has not yet started or auction is
697
+ // unconfigured, and auction has not sold out or revenues have not been
698
+ // withdrawn.
699
+ // @dev _getPriceUnsafe is guaranteed to be accurate unless the core
700
+ // contract is limiting invocations and we have stale local state
701
+ // returning a false negative that max invocations have been reached.
702
+ // This is acceptable, because that case will revert this
703
+ // call later on in this function, when the core contract's max
704
+ // invocation check fails.
705
+ uint256 currentPriceInWei = _getPriceUnsafe(_projectId);
706
+
707
+ // EFFECTS
708
+ // update the purchaser's receipt and require sufficient net payment
709
+ Receipt storage receipt = receipts[msg.sender][_projectId];
710
+
711
+ // in memory copy + update
712
+ uint256 netPosted = receipt.netPosted + msg.value;
713
+ uint256 numPurchased = receipt.numPurchased + 1;
714
+
715
+ // require sufficient payment on project
716
+ require(
717
+ netPosted >= numPurchased * currentPriceInWei,
718
+ "Must send minimum value to mint"
719
+ );
720
+
721
+ // update Receipt in storage
722
+ // @dev overflow checks are not required since the added values cannot
723
+ // be enough to overflow due to maximum invocations or supply of ETH
724
+ receipt.netPosted = uint232(netPosted);
725
+ receipt.numPurchased = uint24(numPurchased);
726
+
727
+ // emit event indicating new receipt state
728
+ emit ReceiptUpdated(msg.sender, _projectId, numPurchased, netPosted);
729
+
730
+ // update latest purchase price (on this minter) in storage
731
+ // @dev this is used to enforce monotonically decreasing purchase price
732
+ // across multiple auctions
733
+ _projectConfig.latestPurchasePrice = currentPriceInWei;
734
+
735
+ tokenId = minterFilter.mint(_to, _projectId, msg.sender);
736
+
737
+ // invocation is token number plus one, and will never overflow due to
738
+ // limit of 1e6 invocations per project. block scope for gas efficiency
739
+ // (i.e. avoid an unnecessary var initialization to 0).
740
+ unchecked {
741
+ uint256 tokenInvocation = (tokenId % ONE_MILLION) + 1;
742
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
743
+ // handle the case where the token invocation == minter local max
744
+ // invocations occurred on a different minter, and we have a stale
745
+ // local maxHasBeenInvoked value returning a false negative.
746
+ // @dev this is a CHECK after EFFECTS, so security was considered
747
+ // in detail here.
748
+ require(
749
+ tokenInvocation <= localMaxInvocations,
750
+ "Maximum number of invocations reached"
751
+ );
752
+ // in typical case, update the local maxHasBeenInvoked value
753
+ // to true if the token invocation == minter local max invocations
754
+ // (enables gas efficient reverts after sellout)
755
+ if (tokenInvocation == localMaxInvocations) {
756
+ _projectConfig.maxHasBeenInvoked = true;
757
+ }
758
+ }
759
+
760
+ // INTERACTIONS
761
+ if (_projectConfig.auctionRevenuesCollected) {
762
+ // if revenues have been collected, split funds immediately.
763
+ // @dev note that we are guaranteed to be at auction base price,
764
+ // since we know we didn't sellout prior to this tx.
765
+ // note that we don't refund msg.sender here, since a separate
766
+ // settlement mechanism is provided on this minter, unrelated to
767
+ // msg.value
768
+ splitRevenuesETH(
769
+ _projectId,
770
+ currentPriceInWei,
771
+ genArt721CoreAddress
772
+ );
773
+ } else {
774
+ // increment the number of settleable invocations that will be
775
+ // claimable by the artist and admin once auction is validated.
776
+ // do not split revenue here since will be claimed at a later time.
777
+ _projectConfig.numSettleableInvocations++;
778
+ }
779
+
780
+ return tokenId;
781
+ }
782
+
783
+ /**
784
+ * @notice Reclaims the sender's payment above current settled price for
785
+ * project `_projectId`. The current settled price is the the price paid
786
+ * for the most recently purchased token, or the base price if the artist
787
+ * has withdrawn revenues after the auction reached base price.
788
+ * This function is callable at any point, but is expected to typically be
789
+ * called after auction has sold out above base price or after the auction
790
+ * has been purchased at base price. This minimizes the amount of gas
791
+ * required to send all excess settlement funds to the sender.
792
+ * Sends excess settlement funds to msg.sender.
793
+ * @param _projectId Project ID to reclaim excess settlement funds on.
794
+ */
795
+ function reclaimProjectExcessSettlementFunds(uint256 _projectId) external {
796
+ reclaimProjectExcessSettlementFundsTo(payable(msg.sender), _projectId);
797
+ }
798
+
799
+ /**
800
+ * @notice Reclaims the sender's payment above current settled price for
801
+ * project `_projectId`. The current settled price is the the price paid
802
+ * for the most recently purchased token, or the base price if the artist
803
+ * has withdrawn revenues after the auction reached base price.
804
+ * This function is callable at any point, but is expected to typically be
805
+ * called after auction has sold out above base price or after the auction
806
+ * has been purchased at base price. This minimizes the amount of gas
807
+ * required to send all excess settlement funds.
808
+ * Sends excess settlement funds to address `_to`.
809
+ * @param _to Address to send excess settlement funds to.
810
+ * @param _projectId Project ID to reclaim excess settlement funds on.
811
+ */
812
+ function reclaimProjectExcessSettlementFundsTo(
813
+ address payable _to,
814
+ uint256 _projectId
815
+ ) public nonReentrant {
816
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
817
+ Receipt storage receipt = receipts[msg.sender][_projectId];
818
+ uint256 numPurchased = receipt.numPurchased;
819
+ // CHECKS
820
+ // input validation
821
+ require(_to != address(0), "No claiming to the zero address");
822
+ // require that a user has purchased at least one token on this project
823
+ require(numPurchased > 0, "No purchases made by this address");
824
+ // get the latestPurchasePrice, which returns the sellout price if the
825
+ // auction sold out before reaching base price, or returns the base
826
+ // price if auction has reached base price and artist has withdrawn
827
+ // revenues.
828
+ // @dev if user is eligible for a reclaiming, they have purchased a
829
+ // token, therefore we are guaranteed to have a populated
830
+ // latestPurchasePrice
831
+ uint256 currentSettledTokenPrice = _projectConfig.latestPurchasePrice;
832
+
833
+ // EFFECTS
834
+ // calculate the excess settlement funds amount
835
+ // implicit overflow/underflow checks in solidity ^0.8
836
+ uint256 requiredAmountPosted = numPurchased * currentSettledTokenPrice;
837
+ uint256 excessSettlementFunds = receipt.netPosted -
838
+ requiredAmountPosted;
839
+ // update Receipt in storage
840
+ receipt.netPosted = requiredAmountPosted.toUint232();
841
+ // emit event indicating new receipt state
842
+ emit ReceiptUpdated(
843
+ msg.sender,
844
+ _projectId,
845
+ numPurchased,
846
+ requiredAmountPosted
847
+ );
848
+
849
+ // INTERACTIONS
850
+ bool success_;
851
+ (success_, ) = _to.call{value: excessSettlementFunds}("");
852
+ require(success_, "Reclaiming failed");
853
+ }
854
+
855
+ /**
856
+ * @notice Reclaims the sender's payment above current settled price for
857
+ * projects in `_projectIds`. The current settled price is the the price
858
+ * paid for the most recently purchased token, or the base price if the
859
+ * artist has withdrawn revenues after the auction reached base price.
860
+ * This function is callable at any point, but is expected to typically be
861
+ * called after auction has sold out above base price or after the auction
862
+ * has been purchased at base price. This minimizes the amount of gas
863
+ * required to send all excess settlement funds to the sender.
864
+ * Sends total of all excess settlement funds to msg.sender in a single
865
+ * chunk. Entire transaction reverts if any excess settlement calculation
866
+ * fails.
867
+ * @param _projectIds Array of project IDs to reclaim excess settlement
868
+ * funds on.
869
+ */
870
+ function reclaimProjectsExcessSettlementFunds(
871
+ uint256[] calldata _projectIds
872
+ ) external {
873
+ reclaimProjectsExcessSettlementFundsTo(
874
+ payable(msg.sender),
875
+ _projectIds
876
+ );
877
+ }
878
+
879
+ /**
880
+ * @notice Reclaims the sender's payment above current settled price for
881
+ * projects in `_projectIds`. The current settled price is the the price
882
+ * paid for the most recently purchased token, or the base price if the
883
+ * artist has withdrawn revenues after the auction reached base price.
884
+ * This function is callable at any point, but is expected to typically be
885
+ * called after auction has sold out above base price or after the auction
886
+ * has been purchased at base price. This minimizes the amount of gas
887
+ * required to send all excess settlement funds to the sender.
888
+ * Sends total of all excess settlement funds to `_to` in a single
889
+ * chunk. Entire transaction reverts if any excess settlement calculation
890
+ * fails.
891
+ * @param _to Address to send excess settlement funds to.
892
+ * @param _projectIds Array of project IDs to reclaim excess settlement
893
+ * funds on.
894
+ */
895
+ function reclaimProjectsExcessSettlementFundsTo(
896
+ address payable _to,
897
+ uint256[] memory _projectIds
898
+ ) public nonReentrant {
899
+ // CHECKS
900
+ // input validation
901
+ require(_to != address(0), "No claiming to the zero address");
902
+ // EFFECTS
903
+ // for each project, tally up the excess settlement funds and update
904
+ // the receipt in storage
905
+ uint256 excessSettlementFunds;
906
+ uint256 projectIdsLength = _projectIds.length;
907
+ for (uint256 i; i < projectIdsLength; ) {
908
+ uint256 projectId = _projectIds[i];
909
+ ProjectConfig storage _projectConfig = projectConfig[projectId];
910
+ Receipt storage receipt = receipts[msg.sender][projectId];
911
+ uint256 numPurchased = receipt.numPurchased;
912
+ // input validation
913
+ // require that a user has purchased at least one token on this project
914
+ require(numPurchased > 0, "No purchases made by this address");
915
+ // get the latestPurchasePrice, which returns the sellout price if the
916
+ // auction sold out before reaching base price, or returns the base
917
+ // price if auction has reached base price and artist has withdrawn
918
+ // revenues.
919
+ // @dev if user is eligible for a claim, they have purchased a token,
920
+ // therefore we are guaranteed to have a populated
921
+ // latestPurchasePrice
922
+ uint256 currentSettledTokenPrice = _projectConfig
923
+ .latestPurchasePrice;
924
+ // calculate the excessSettlementFunds amount
925
+ // implicit overflow/underflow checks in solidity ^0.8
926
+ uint256 requiredAmountPosted = numPurchased *
927
+ currentSettledTokenPrice;
928
+ excessSettlementFunds += (receipt.netPosted - requiredAmountPosted);
929
+ // reduce the netPosted (in storage) to value after excess settlement
930
+ // funds deducted
931
+ receipt.netPosted = requiredAmountPosted.toUint232();
932
+ // emit event indicating new receipt state
933
+ emit ReceiptUpdated(
934
+ msg.sender,
935
+ projectId,
936
+ numPurchased,
937
+ requiredAmountPosted
938
+ );
939
+ // gas efficiently increment i
940
+ // won't overflow due to for loop, as well as gas limts
941
+ unchecked {
942
+ ++i;
943
+ }
944
+ }
945
+
946
+ // INTERACTIONS
947
+ // send excess settlement funds in a single chunk for all
948
+ // projects
949
+ bool success_;
950
+ (success_, ) = _to.call{value: excessSettlementFunds}("");
951
+ require(success_, "Reclaiming failed");
952
+ }
953
+
954
+ /**
955
+ * @notice Gets price of minting a token on project `_projectId` given
956
+ * the project's AuctionParameters and current block timestamp.
957
+ * Reverts if auction has not yet started or auction is unconfigured, and
958
+ * auction has not sold out or revenues have not been withdrawn.
959
+ * Price is guaranteed to be accurate, regardless of the current state of
960
+ * the locally cached minter max invocations.
961
+ * @dev This method is less gas efficient than `_getPriceUnsafe`, but is
962
+ * guaranteed to be accurate.
963
+ * @param _projectId Project ID to get price of token for.
964
+ * @return tokenPriceInWei current price of token in Wei
965
+ * @dev This method calculates price decay using a linear interpolation
966
+ * of exponential decay based on the artist-provided half-life for price
967
+ * decay, `_priceDecayHalfLifeSeconds`.
968
+ */
969
+ function _getPriceSafe(
970
+ uint256 _projectId
971
+ ) private view returns (uint256 tokenPriceInWei) {
972
+ // accurately check if project has sold out
973
+ if (_projectMaxHasBeenInvokedSafe(_projectId)) {
974
+ // max invocations have been reached, return the latest purchased
975
+ // price
976
+ tokenPriceInWei = projectConfig[_projectId].latestPurchasePrice;
977
+ } else {
978
+ // if not sold out, return the current price
979
+ tokenPriceInWei = _getPriceUnsafe(_projectId);
980
+ }
981
+ return tokenPriceInWei;
982
+ }
983
+
984
+ /**
985
+ * @notice Gets price of minting a token on project `_projectId` given
986
+ * the project's AuctionParameters and current block timestamp.
987
+ * Reverts if auction has not yet started or auction is unconfigured, and
988
+ * local hasMaxBeenInvoked is false and revenues have not been withdrawn.
989
+ * Price is guaranteed to be accurate unless the minter's local
990
+ * hasMaxBeenInvoked is stale and returning a false negative.
991
+ * @dev when an accurate price is required regardless of the current state
992
+ * state of the locally cached minter max invocations, use the less gas
993
+ * efficient function `_getPriceSafe`.
994
+ * @param _projectId Project ID to get price of token for.
995
+ * @return uint256 current price of token in Wei, accurate if minter max
996
+ * invocations are up to date
997
+ * @dev This method calculates price decay using a linear interpolation
998
+ * of exponential decay based on the artist-provided half-life for price
999
+ * decay, `_priceDecayHalfLifeSeconds`.
1000
+ */
1001
+ function _getPriceUnsafe(
1002
+ uint256 _projectId
1003
+ ) private view returns (uint256) {
1004
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1005
+ // return latest purchase price if:
1006
+ // - minter is aware of a sold-out auction (without updating max
1007
+ // invocation value)
1008
+ // - auction revenues have been collected, at which point the
1009
+ // latest purchase price will never change again
1010
+ if (
1011
+ _projectConfig.maxHasBeenInvoked ||
1012
+ _projectConfig.auctionRevenuesCollected
1013
+ ) {
1014
+ return _projectConfig.latestPurchasePrice;
1015
+ }
1016
+ // otherwise calculate price based on current block timestamp and
1017
+ // auction configuration (will revert if auction has not started)
1018
+ // move parameters to memory if used more than once
1019
+ uint256 _timestampStart = uint256(_projectConfig.timestampStart);
1020
+ uint256 _priceDecayHalfLifeSeconds = uint256(
1021
+ _projectConfig.priceDecayHalfLifeSeconds
1022
+ );
1023
+ uint256 _basePrice = _projectConfig.basePrice;
1024
+
1025
+ require(block.timestamp > _timestampStart, "Auction not yet started");
1026
+ require(_priceDecayHalfLifeSeconds > 0, "Only configured auctions");
1027
+ uint256 decayedPrice = _projectConfig.startPrice;
1028
+ uint256 elapsedTimeSeconds;
1029
+ unchecked {
1030
+ // already checked that block.timestamp > _timestampStart above
1031
+ elapsedTimeSeconds = block.timestamp - _timestampStart;
1032
+ }
1033
+ // Divide by two (via bit-shifting) for the number of entirely completed
1034
+ // half-lives that have elapsed since auction start time.
1035
+ unchecked {
1036
+ // already required _priceDecayHalfLifeSeconds > 0
1037
+ decayedPrice >>= elapsedTimeSeconds / _priceDecayHalfLifeSeconds;
1038
+ }
1039
+ // Perform a linear interpolation between partial half-life points, to
1040
+ // approximate the current place on a perfect exponential decay curve.
1041
+ unchecked {
1042
+ // value of expression is provably always less than decayedPrice,
1043
+ // so no underflow is possible when the subtraction assignment
1044
+ // operator is used on decayedPrice.
1045
+ decayedPrice -=
1046
+ (decayedPrice *
1047
+ (elapsedTimeSeconds % _priceDecayHalfLifeSeconds)) /
1048
+ _priceDecayHalfLifeSeconds /
1049
+ 2;
1050
+ }
1051
+ if (decayedPrice < _basePrice) {
1052
+ // Price may not decay below stay `basePrice`.
1053
+ return _basePrice;
1054
+ }
1055
+ return decayedPrice;
1056
+ }
1057
+
1058
+ /**
1059
+ * @notice Gets the current excess settlement funds on project `_projectId`
1060
+ * for address `_walletAddress`. The returned value is expected to change
1061
+ * throughtout an auction, since the latest purchase price is used when
1062
+ * determining excess settlement funds.
1063
+ * A user may claim excess settlement funds by calling the function
1064
+ * `reclaimProjectExcessSettlementFunds(_projectId)`.
1065
+ * @param _projectId Project ID to query.
1066
+ * @param _walletAddress Account address for which the excess posted funds
1067
+ * is being queried.
1068
+ * @return excessSettlementFundsInWei Amount of excess settlement funds, in
1069
+ * wei
1070
+ */
1071
+ function getProjectExcessSettlementFunds(
1072
+ uint256 _projectId,
1073
+ address _walletAddress
1074
+ ) external view returns (uint256 excessSettlementFundsInWei) {
1075
+ // input validation
1076
+ require(_walletAddress != address(0), "No zero address");
1077
+ // load struct from storage
1078
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1079
+ Receipt storage receipt = receipts[_walletAddress][_projectId];
1080
+ // require that a user has purchased at least one token on this project
1081
+ require(receipt.numPurchased > 0, "No purchases made by this address");
1082
+ // get the latestPurchasePrice, which returns the sellout price if the
1083
+ // auction sold out before reaching base price, or returns the base
1084
+ // price if auction has reached base price and artist has withdrawn
1085
+ // revenues.
1086
+ // @dev if user is eligible for a reclaiming, they have purchased a
1087
+ // token, therefore we are guaranteed to have a populated
1088
+ // latestPurchasePrice
1089
+ uint256 currentSettledTokenPrice = _projectConfig.latestPurchasePrice;
1090
+
1091
+ // EFFECTS
1092
+ // calculate the excess settlement funds amount and return
1093
+ // implicit overflow/underflow checks in solidity ^0.8
1094
+ uint256 requiredAmountPosted = receipt.numPurchased *
1095
+ currentSettledTokenPrice;
1096
+ excessSettlementFundsInWei = receipt.netPosted - requiredAmountPosted;
1097
+ return excessSettlementFundsInWei;
1098
+ }
1099
+
1100
+ /**
1101
+ * @notice Gets the latest purchase price for project `_projectId`, or 0 if
1102
+ * no purchases have been made.
1103
+ */
1104
+ function getProjectLatestPurchasePrice(
1105
+ uint256 _projectId
1106
+ ) external view returns (uint256 latestPurchasePrice) {
1107
+ return projectConfig[_projectId].latestPurchasePrice;
1108
+ }
1109
+
1110
+ /**
1111
+ * @notice Gets the number of settleable invocations for project `_projectId`.
1112
+ */
1113
+ function getNumSettleableInvocations(
1114
+ uint256 _projectId
1115
+ ) external view returns (uint256 numSettleableInvocations) {
1116
+ return projectConfig[_projectId].numSettleableInvocations;
1117
+ }
1118
+
1119
+ /**
1120
+ * @notice Gets if price of token is configured, price of minting a
1121
+ * token on project `_projectId`, and currency symbol and address to be
1122
+ * used as payment. Supersedes any core contract price information.
1123
+ * @param _projectId Project ID to get price information for.
1124
+ * @return isConfigured true only if project's auction parameters have been
1125
+ * configured on this minter
1126
+ * @return tokenPriceInWei current price of token on this minter - invalid
1127
+ * if auction has not yet been configured
1128
+ * @return currencySymbol currency symbol for purchases of project on this
1129
+ * minter. This minter always returns "ETH"
1130
+ * @return currencyAddress currency address for purchases of project on
1131
+ * this minter. This minter always returns null address, reserved for ether
1132
+ */
1133
+ function getPriceInfo(
1134
+ uint256 _projectId
1135
+ )
1136
+ external
1137
+ view
1138
+ returns (
1139
+ bool isConfigured,
1140
+ uint256 tokenPriceInWei,
1141
+ string memory currencySymbol,
1142
+ address currencyAddress
1143
+ )
1144
+ {
1145
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1146
+
1147
+ isConfigured = (_projectConfig.startPrice > 0);
1148
+ if (block.timestamp <= _projectConfig.timestampStart) {
1149
+ // Provide a reasonable value for `tokenPriceInWei` when it would
1150
+ // otherwise revert, using the starting price before auction starts.
1151
+ tokenPriceInWei = _projectConfig.startPrice;
1152
+ } else if (_projectConfig.startPrice == 0) {
1153
+ // In the case of unconfigured auction, return price of zero when
1154
+ // it would otherwise revert
1155
+ tokenPriceInWei = 0;
1156
+ } else {
1157
+ tokenPriceInWei = _getPriceSafe(_projectId);
1158
+ }
1159
+ currencySymbol = "ETH";
1160
+ currencyAddress = address(0);
1161
+ }
1162
+
1163
+ /**
1164
+ * @notice Sets the local max invocation values of a project equal to the
1165
+ * values on the core contract.
1166
+ * @param _projectId Project ID to set the maximum invocations for.
1167
+ */
1168
+ function _syncProjectMaxInvocationsCoreCached(uint256 _projectId) internal {
1169
+ uint256 coreMaxInvocations;
1170
+ uint256 coreInvocations;
1171
+ (
1172
+ coreInvocations,
1173
+ coreMaxInvocations
1174
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1175
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1176
+ // update storage with results, emit event after change
1177
+ _projectConfig.maxInvocations = uint24(coreMaxInvocations);
1178
+ _projectConfig.maxHasBeenInvoked =
1179
+ coreMaxInvocations == coreInvocations;
1180
+
1181
+ emit ProjectMaxInvocationsLimitUpdated(_projectId, coreMaxInvocations);
1182
+ }
1183
+
1184
+ /**
1185
+ * @notice Returns the current invocations and maximum invocations of
1186
+ * project `_projectId` from the core contract.
1187
+ * @param _projectId Project ID to get invocations and maximum invocations
1188
+ * for.
1189
+ * @return invocations current invocations of project.
1190
+ * @return maxInvocations maximum invocations of project.
1191
+ */
1192
+ function _getProjectCoreInvocationsAndMaxInvocations(
1193
+ uint256 _projectId
1194
+ ) internal view returns (uint256 invocations, uint256 maxInvocations) {
1195
+ (invocations, maxInvocations, , , , ) = genArtCoreContract_Base
1196
+ .projectStateData(_projectId);
1197
+ }
1198
+
1199
+ /**
1200
+ * @notice Verifies the cached values of a project's maxInvocation state
1201
+ * are logically consistent with the core contract's maxInvocation state,
1202
+ * or populates them to equal the core contract's maxInvocation state if
1203
+ * they have never been populated.
1204
+ */
1205
+ function _refreshMaxInvocations(uint256 _projectId) internal {
1206
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1207
+ // project's max invocations and has max been invoked can only be
1208
+ // initial values if never populated, because setting a maxInvocations
1209
+ // of zero means maxHasBeenInvoked would be set to true
1210
+ bool notPopulated = (_projectConfig.maxInvocations == 0 &&
1211
+ _projectConfig.maxHasBeenInvoked == false);
1212
+ if (notPopulated) {
1213
+ // sync the minter max invocation state to equal the values on the
1214
+ // core contract (least restrictive state)
1215
+ _syncProjectMaxInvocationsCoreCached(_projectId);
1216
+ } else {
1217
+ // if using local max invocations, validate the local state
1218
+ // (i.e. ensure local max invocations not greater than core max
1219
+ // invocations)
1220
+ _validateProjectMaxInvocations(_projectId);
1221
+ }
1222
+ }
1223
+
1224
+ /**
1225
+ * @notice Checks and updates local project max invocations to determine if
1226
+ * if they are in an illogical state relative to the core contract's max
1227
+ * invocations.
1228
+ * This updates the project's local max invocations if the value is greater
1229
+ * than the core contract's max invocations, which is an illogical state
1230
+ * since V3 core contracts cannot increase max invocations. In that case,
1231
+ * the project's local max invocations are set to the core contract's max
1232
+ * invocations, and the project's `maxHasBeenInvoked` state is refreshed.
1233
+ * This also updates the project's `maxHasBeenInvoked` state if the core
1234
+ * contract's invocations are greater than or equal to the minter's local
1235
+ * max invocations. This handles the case where a different minter has been
1236
+ * used to mint above the local max invocations, which would cause
1237
+ * `maxHasBeenInvoked` to return a false negative.
1238
+ * @param _projectId Project ID to set the maximum invocations for.
1239
+ */
1240
+ function _validateProjectMaxInvocations(uint256 _projectId) internal {
1241
+ uint256 coreMaxInvocations;
1242
+ uint256 coreInvocations;
1243
+ (
1244
+ coreInvocations,
1245
+ coreMaxInvocations
1246
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1247
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1248
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
1249
+ // check if local max invocations is illogical relative to core
1250
+ // contract's max invocations
1251
+ if (localMaxInvocations > coreMaxInvocations) {
1252
+ // set local max invocations to core contract's max invocations
1253
+ _projectConfig.maxInvocations = uint24(coreMaxInvocations);
1254
+ // update the project's `maxHasBeenInvoked` state
1255
+ // @dev core values are equivalent to local values, use for gas
1256
+ // efficiency
1257
+ _projectConfig.maxHasBeenInvoked = (coreMaxInvocations ==
1258
+ coreInvocations);
1259
+ emit ProjectMaxInvocationsLimitUpdated(
1260
+ _projectId,
1261
+ coreMaxInvocations
1262
+ );
1263
+ } else if (coreInvocations >= localMaxInvocations) {
1264
+ // ensure the local `maxHasBeenInvoked` state is accurate to
1265
+ // prevent any false negatives due to minting on other minters
1266
+ _projectConfig.maxHasBeenInvoked = true;
1267
+ // emit event to ensure any indexers are aware of the change
1268
+ // @dev this is not strictly necessary, but is included for
1269
+ // convenience
1270
+ emit ProjectMaxInvocationsLimitUpdated(
1271
+ _projectId,
1272
+ coreMaxInvocations
1273
+ );
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * @notice Returns true if the project `_projectId` is sold out, false
1279
+ * otherwise. This function returns an accurate value regardless of whether
1280
+ * the project's maximum invocations value cached locally on the minter is
1281
+ * up to date with the core contract's maximum invocations value.
1282
+ * @param _projectId Project ID to check if sold out.
1283
+ * @return bool true if the project is sold out, false otherwise.
1284
+ * @dev this is a view method, and will not update the minter's local
1285
+ * cached state.
1286
+ */
1287
+ function _projectMaxHasBeenInvokedSafe(
1288
+ uint256 _projectId
1289
+ ) internal view returns (bool) {
1290
+ // get max invocations from core contract
1291
+ uint256 coreInvocations;
1292
+ uint256 coreMaxInvocations;
1293
+ (
1294
+ coreInvocations,
1295
+ coreMaxInvocations
1296
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1297
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1298
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
1299
+ // value is locally defined, and could be out of date.
1300
+ // only possible illogical state is if local max invocations is
1301
+ // greater than core contract's max invocations, in which case
1302
+ // we should use the core contract's max invocations
1303
+ if (localMaxInvocations > coreMaxInvocations) {
1304
+ // local max invocations is stale and illogical, defer to core
1305
+ // contract's max invocations since it is the limiting factor
1306
+ return (coreMaxInvocations == coreInvocations);
1307
+ }
1308
+ // local max invocations is limiting, so check core invocations against
1309
+ // local max invocations
1310
+ return (coreInvocations >= localMaxInvocations);
1311
+ }
1312
+ }