@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,1304 @@
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
+ // function to restrict access to only AdminACL or the artist
148
+ function _onlyCoreAdminACLOrArtist(
149
+ uint256 _projectId,
150
+ bytes4 _selector
151
+ ) internal {
152
+ require(
153
+ (msg.sender ==
154
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)) ||
155
+ (
156
+ genArtCoreContract_Base.adminACLAllowed(
157
+ msg.sender,
158
+ address(this),
159
+ _selector
160
+ )
161
+ ),
162
+ "Only Artist or Admin ACL"
163
+ );
164
+ }
165
+
166
+ // function to restrict access to only AdminACL allowed calls
167
+ // @dev defers which ACL contract is used to the core contract
168
+ function _onlyCoreAdminACL(bytes4 _selector) internal {
169
+ require(
170
+ genArtCoreContract_Base.adminACLAllowed(
171
+ msg.sender,
172
+ address(this),
173
+ _selector
174
+ ),
175
+ "Only Core AdminACL allowed"
176
+ );
177
+ }
178
+
179
+ function _onlyArtist(uint256 _projectId) internal view {
180
+ require(
181
+ (msg.sender ==
182
+ genArtCoreContract_Base.projectIdToArtistAddress(_projectId)),
183
+ "Only Artist"
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(uint256 _projectId) external view {
224
+ _onlyArtist(_projectId);
225
+ revert("Not implemented");
226
+ }
227
+
228
+ /**
229
+ * @notice Manually sets the local maximum invocations of project `_projectId`
230
+ * with the provided `_maxInvocations`, checking that `_maxInvocations` is less
231
+ * than or equal to the value of project `_project_id`'s maximum invocations that is
232
+ * set on the core contract.
233
+ * @dev Note that a `_maxInvocations` of 0 can only be set if the current `invocations`
234
+ * value is also 0 and this would also set `maxHasBeenInvoked` to true, correctly short-circuiting
235
+ * this minter's purchase function, avoiding extra gas costs from the core contract's maxInvocations check.
236
+ * @param _projectId Project ID to set the maximum invocations for.
237
+ * @param _maxInvocations Maximum invocations to set for the project.
238
+ */
239
+ function manuallyLimitProjectMaxInvocations(
240
+ uint256 _projectId,
241
+ uint256 _maxInvocations
242
+ ) external {
243
+ _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(uint256 _projectId) external view {
292
+ _onlyArtist(_projectId);
293
+ revert("Action not supported");
294
+ }
295
+
296
+ /**
297
+ * @notice projectId => has project reached its maximum number of
298
+ * invocations on this minter?
299
+ * Note that this returns a local cached value on the minter, and may be
300
+ * out of sync with the core core contract's state, in which case it may
301
+ * return a false negative.
302
+ */
303
+ function projectMaxHasBeenInvoked(
304
+ uint256 _projectId
305
+ ) external view returns (bool) {
306
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
307
+ return _projectConfig.maxHasBeenInvoked;
308
+ }
309
+
310
+ /**
311
+ * @notice projectId => project's maximum number of invocations.
312
+ * Note that this returns a local cached value, and may be manually
313
+ * limited to be different than the core contract's maxInvocations,
314
+ * or may be out of sync with the core contract's maxInvocations state.
315
+ */
316
+ function projectMaxInvocations(
317
+ uint256 _projectId
318
+ ) external view returns (uint256) {
319
+ return projectConfig[_projectId].maxInvocations;
320
+ }
321
+
322
+ /**
323
+ * @notice projectId => auction parameters
324
+ */
325
+ function projectAuctionParameters(
326
+ uint256 _projectId
327
+ )
328
+ external
329
+ view
330
+ returns (
331
+ uint256 timestampStart,
332
+ uint256 priceDecayHalfLifeSeconds,
333
+ uint256 startPrice,
334
+ uint256 basePrice
335
+ )
336
+ {
337
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
338
+ return (
339
+ _projectConfig.timestampStart,
340
+ _projectConfig.priceDecayHalfLifeSeconds,
341
+ _projectConfig.startPrice,
342
+ _projectConfig.basePrice
343
+ );
344
+ }
345
+
346
+ /**
347
+ * @notice Sets the minimum and maximum values that are settable for
348
+ * `_priceDecayHalfLifeSeconds` across all projects.
349
+ * @param _minimumPriceDecayHalfLifeSeconds Minimum price decay half life
350
+ * (in seconds).
351
+ * @param _maximumPriceDecayHalfLifeSeconds Maximum price decay half life
352
+ * (in seconds).
353
+ */
354
+ function setAllowablePriceDecayHalfLifeRangeSeconds(
355
+ uint256 _minimumPriceDecayHalfLifeSeconds,
356
+ uint256 _maximumPriceDecayHalfLifeSeconds
357
+ ) external {
358
+ _onlyCoreAdminACL(
359
+ this.setAllowablePriceDecayHalfLifeRangeSeconds.selector
360
+ );
361
+ require(
362
+ _maximumPriceDecayHalfLifeSeconds >
363
+ _minimumPriceDecayHalfLifeSeconds,
364
+ "Maximum half life must be greater than minimum"
365
+ );
366
+ require(
367
+ _minimumPriceDecayHalfLifeSeconds > 0,
368
+ "Half life of zero not allowed"
369
+ );
370
+ minimumPriceDecayHalfLifeSeconds = _minimumPriceDecayHalfLifeSeconds;
371
+ maximumPriceDecayHalfLifeSeconds = _maximumPriceDecayHalfLifeSeconds;
372
+ emit AuctionHalfLifeRangeSecondsUpdated(
373
+ _minimumPriceDecayHalfLifeSeconds,
374
+ _maximumPriceDecayHalfLifeSeconds
375
+ );
376
+ }
377
+
378
+ ////// Auction Functions
379
+ /**
380
+ * @notice Sets auction details for project `_projectId`.
381
+ * @param _projectId Project ID to set auction details for.
382
+ * @param _auctionTimestampStart Timestamp at which to start the auction.
383
+ * @param _priceDecayHalfLifeSeconds The half life with which to decay the
384
+ * price (in seconds).
385
+ * @param _startPrice Price at which to start the auction, in Wei.
386
+ * If a previous auction existed on this minter and at least one settleable
387
+ * purchase has been made, this value must be less than or equal to the
388
+ * price when the previous auction was paused. This enforces an overall
389
+ * monatonically decreasing auction. Must be greater than or equal to
390
+ * max(uint128) for internal storage packing purposes.
391
+ * @param _basePrice Resting price of the auction, in Wei. Must be greater
392
+ * than or equal to max(uint128) for internal storage packing purposes.
393
+ * @dev Note that setting the auction price explicitly to `0` is
394
+ * intentionally not allowed. This allows the minter to use the assumption
395
+ * that a price of `0` indicates that the auction is not configured.
396
+ * @dev Note that prices must be <= max(128) for internal storage packing
397
+ * efficiency purposes only. This function's interface remains unchanged
398
+ * for interface conformance purposes.
399
+ * @dev Note that this function also populates the local minter max
400
+ * invocation values for the project. This is done to ensure that the
401
+ * minter's local max invocation values are guarenteed to be at least
402
+ * populated when an auction is configured.
403
+ */
404
+ function setAuctionDetails(
405
+ uint256 _projectId,
406
+ uint256 _auctionTimestampStart,
407
+ uint256 _priceDecayHalfLifeSeconds,
408
+ uint256 _startPrice,
409
+ uint256 _basePrice
410
+ ) external {
411
+ _onlyArtist(_projectId);
412
+ // CHECKS
413
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
414
+ require(
415
+ _projectConfig.timestampStart == 0 ||
416
+ block.timestamp < _projectConfig.timestampStart,
417
+ "No modifications mid-auction"
418
+ );
419
+ require(
420
+ block.timestamp < _auctionTimestampStart,
421
+ "Only future auctions"
422
+ );
423
+ require(
424
+ _startPrice > _basePrice,
425
+ "Auction start price must be greater than auction end price"
426
+ );
427
+ // require _basePrice is non-zero to simplify logic of this minter
428
+ require(_basePrice > 0, "Base price must be non-zero");
429
+ // If previous purchases have been made, require monotonically
430
+ // decreasing purchase prices to preserve settlement and revenue
431
+ // claiming logic. Since base price is always non-zero, if
432
+ // latestPurchasePrice is zero, then no previous purchases have been
433
+ // made, and startPrice may be set to any value.
434
+ require(
435
+ _projectConfig.latestPurchasePrice == 0 || // never purchased
436
+ _startPrice <= _projectConfig.latestPurchasePrice,
437
+ "Auction start price must be <= latest purchase price"
438
+ );
439
+ require(
440
+ (_priceDecayHalfLifeSeconds >= minimumPriceDecayHalfLifeSeconds) &&
441
+ (_priceDecayHalfLifeSeconds <=
442
+ maximumPriceDecayHalfLifeSeconds),
443
+ "Price decay half life must fall between min and max allowable values"
444
+ );
445
+ // EFFECTS
446
+ _projectConfig.timestampStart = _auctionTimestampStart.toUint64();
447
+ _projectConfig.priceDecayHalfLifeSeconds = _priceDecayHalfLifeSeconds
448
+ .toUint64();
449
+ _projectConfig.startPrice = _startPrice.toUint128();
450
+ _projectConfig.basePrice = _basePrice.toUint128();
451
+
452
+ emit SetAuctionDetails(
453
+ _projectId,
454
+ _auctionTimestampStart,
455
+ _priceDecayHalfLifeSeconds,
456
+ _startPrice,
457
+ _basePrice
458
+ );
459
+
460
+ // refresh max invocations, ensuring the values are populated, and
461
+ // updating any local values that are illogical with respect to the
462
+ // current core contract state.
463
+ // @dev this refresh enables the guarantee that a project's max
464
+ // invocation state is always populated if an auction is configured.
465
+ _refreshMaxInvocations(_projectId);
466
+ }
467
+
468
+ /**
469
+ * @notice Resets auction details for project `_projectId`, zero-ing out all
470
+ * relevant auction fields. Not intended to be used in normal auction
471
+ * operation, but rather only in case of the need to reset an ongoing
472
+ * auction. An expected time this might occur would be when a frontend
473
+ * issue was occuring, and many typical users are actively being prevented
474
+ * from easily minting (even though minting would technically be possible
475
+ * directly from the contract).
476
+ * This function is only callable by the core admin during an active
477
+ * auction, before revenues have been collected.
478
+ * The price at the time of the reset will be the maximum starting price
479
+ * when re-configuring the next auction if one or more settleable purchases
480
+ * have been made.
481
+ * This is to ensure that purchases up through the block that this is
482
+ * called on will remain settleable, and that revenue claimed does not
483
+ * surpass (payments - excess_settlement_funds) for a given project.
484
+ * @param _projectId Project ID to set auction details for.
485
+ */
486
+ function resetAuctionDetails(uint256 _projectId) external {
487
+ _onlyCoreAdminACL(this.resetAuctionDetails.selector);
488
+ // CHECKS
489
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
490
+ require(_projectConfig.startPrice != 0, "Auction must be configured");
491
+ // no reset after revenues collected, since that solidifies amount due
492
+ require(
493
+ !_projectConfig.auctionRevenuesCollected,
494
+ "Only before revenues collected"
495
+ );
496
+ // EFFECTS
497
+ // reset to initial values
498
+ _projectConfig.timestampStart = 0;
499
+ _projectConfig.priceDecayHalfLifeSeconds = 0;
500
+ _projectConfig.startPrice = 0;
501
+ _projectConfig.basePrice = 0;
502
+ // Since auction revenues have not been collected, we can safely assume
503
+ // that numSettleableInvocations is the number of purchases made on
504
+ // this minter. A dummy value of 0 is used for latest purchase price if
505
+ // no purchases have been made.
506
+ emit ResetAuctionDetails(
507
+ _projectId,
508
+ _projectConfig.numSettleableInvocations,
509
+ _projectConfig.latestPurchasePrice
510
+ );
511
+ }
512
+
513
+ /**
514
+ * @notice This represents an admin stepping in and reducing the sellout
515
+ * price of an auction. This is only callable by the core admin, only
516
+ * after the auction is complete, but before project revenues are
517
+ * withdrawn.
518
+ * This is only intended to be used in the case where for some reason, the
519
+ * sellout price was too high.
520
+ * @param _projectId Project ID to reduce auction sellout price for.
521
+ * @param _newSelloutPrice New sellout price to set for the auction. Must
522
+ * be less than the current sellout price.
523
+ */
524
+ function adminEmergencyReduceSelloutPrice(
525
+ uint256 _projectId,
526
+ uint256 _newSelloutPrice
527
+ ) external {
528
+ _onlyCoreAdminACL(this.adminEmergencyReduceSelloutPrice.selector);
529
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
530
+ require(
531
+ !_projectConfig.auctionRevenuesCollected,
532
+ "Only before revenues collected"
533
+ );
534
+
535
+ // refresh max invocations, updating any local values that are
536
+ // illogical with respect to the current core contract state, and
537
+ // ensuring that local hasMaxBeenInvoked is accurate.
538
+ _refreshMaxInvocations(_projectId);
539
+
540
+ // require max invocations has been reached
541
+ require(_projectConfig.maxHasBeenInvoked, "Auction must be complete");
542
+ // @dev no need to check that auction max invocations has been reached,
543
+ // because if it was, the sellout price will be zero, and the following
544
+ // check will fail.
545
+ require(
546
+ _newSelloutPrice < _projectConfig.latestPurchasePrice,
547
+ "May only reduce sellout price"
548
+ );
549
+ require(
550
+ _newSelloutPrice >= _projectConfig.basePrice,
551
+ "May only reduce sellout price to base price or greater"
552
+ );
553
+ // ensure _newSelloutPrice is non-zero
554
+ require(_newSelloutPrice > 0, "Only sellout prices > 0");
555
+ _projectConfig.latestPurchasePrice = _newSelloutPrice;
556
+ emit SelloutPriceUpdated(_projectId, _newSelloutPrice);
557
+ }
558
+
559
+ /**
560
+ * @notice This withdraws project revenues for the artist and admin.
561
+ * This function is only callable by the artist or admin, and only after
562
+ * one of the following is true:
563
+ * - the auction has sold out above base price
564
+ * - the auction has reached base price
565
+ * Note that revenues are not claimable if in a temporary state after
566
+ * an auction is reset.
567
+ * Revenues may only be collected a single time per project.
568
+ * After revenues are collected, auction parameters will never be allowed
569
+ * to be reset, and excess settlement funds will become immutable and fully
570
+ * deterministic.
571
+ */
572
+ function withdrawArtistAndAdminRevenues(
573
+ uint256 _projectId
574
+ ) external nonReentrant {
575
+ _onlyCoreAdminACLOrArtist(
576
+ _projectId,
577
+ this.withdrawArtistAndAdminRevenues.selector
578
+ );
579
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
580
+ // CHECKS
581
+ // require revenues to not have already been collected
582
+ require(
583
+ !_projectConfig.auctionRevenuesCollected,
584
+ "Revenues already collected"
585
+ );
586
+
587
+ // refresh max invocations, updating any local values that are
588
+ // illogical with respect to the current core contract state, and
589
+ // ensuring that local hasMaxBeenInvoked is accurate.
590
+ _refreshMaxInvocations(_projectId);
591
+
592
+ // get the current net price of the auction - reverts if no auction
593
+ // is configured.
594
+ // @dev we use _getPriceUnsafe here, since we just safely synced the
595
+ // project's max invocations and maxHasBeenInvoked, which guarantees
596
+ // an accurate price calculation from _getPriceUnsafe, while being
597
+ // more gas efficient than _getPriceSafe.
598
+ // @dev price is guaranteed <= _projectConfig.latestPurchasePrice,
599
+ // since this minter enforces monotonically decreasing purchase prices.
600
+ uint256 _price = _getPriceUnsafe(_projectId);
601
+ // if the price is not base price, require that the auction have
602
+ // reached max invocations. This prevents premature withdrawl
603
+ // before final auction price is possible to know.
604
+ if (_price != _projectConfig.basePrice) {
605
+ // @dev we can trust maxHasBeenInvoked, since we just
606
+ // refreshed it above with _refreshMaxInvocations, preventing any
607
+ // false negatives
608
+ require(
609
+ _projectConfig.maxHasBeenInvoked,
610
+ "Active auction not yet sold out"
611
+ );
612
+ } else {
613
+ uint256 basePrice = _projectConfig.basePrice;
614
+ // base price of zero indicates no sales, since base price of zero
615
+ // is not allowed when configuring an auction.
616
+ require(basePrice > 0, "Only latestPurchasePrice > 0");
617
+ // update the latest purchase price to the base price, to ensure
618
+ // the base price is used for all future settlement calculations
619
+ _projectConfig.latestPurchasePrice = basePrice;
620
+ }
621
+ // EFFECTS
622
+ _projectConfig.auctionRevenuesCollected = true;
623
+ // if the price is base price, the auction is valid and may be claimed
624
+ // calculate the artist and admin revenues
625
+ uint256 netRevenues = _projectConfig.numSettleableInvocations * _price;
626
+ // INTERACTIONS
627
+ splitRevenuesETH(_projectId, netRevenues, genArt721CoreAddress);
628
+ emit ArtistAndAdminRevenuesWithdrawn(_projectId);
629
+ }
630
+
631
+ /**
632
+ * @notice Purchases a token from project `_projectId`.
633
+ * @param _projectId Project ID to mint a token on.
634
+ * @return tokenId Token ID of minted token
635
+ */
636
+ function purchase(
637
+ uint256 _projectId
638
+ ) external payable returns (uint256 tokenId) {
639
+ tokenId = purchaseTo_do6(msg.sender, _projectId);
640
+ return tokenId;
641
+ }
642
+
643
+ /**
644
+ * @notice gas-optimized version of purchase(uint256).
645
+ */
646
+ function purchase_H4M(
647
+ uint256 _projectId
648
+ ) external payable returns (uint256 tokenId) {
649
+ tokenId = purchaseTo_do6(msg.sender, _projectId);
650
+ return tokenId;
651
+ }
652
+
653
+ /**
654
+ * @notice Purchases a token from project `_projectId` and sets
655
+ * the token's owner to `_to`.
656
+ * @param _to Address to be the new token's owner.
657
+ * @param _projectId Project ID to mint a token on.
658
+ * @return tokenId Token ID of minted token
659
+ */
660
+ function purchaseTo(
661
+ address _to,
662
+ uint256 _projectId
663
+ ) external payable returns (uint256 tokenId) {
664
+ return purchaseTo_do6(_to, _projectId);
665
+ }
666
+
667
+ /**
668
+ * @notice gas-optimized version of purchaseTo(address, uint256).
669
+ */
670
+ function purchaseTo_do6(
671
+ address _to,
672
+ uint256 _projectId
673
+ ) public payable nonReentrant returns (uint256 tokenId) {
674
+ // CHECKS
675
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
676
+
677
+ // Enforce the local limit of maxHasBeenInvoked, which is guaranteed
678
+ // to be populated for all configured auctions.
679
+ // @dev maxHasBeenInvoked can be sale and return a false negative.
680
+ // protect against that case by checking minted token's invocation
681
+ // against this minter's local max invocations immediately after
682
+ // receiving the newly minted tokenID.
683
+ require(
684
+ !_projectConfig.maxHasBeenInvoked,
685
+ "Maximum number of invocations reached"
686
+ );
687
+
688
+ // _getPriceUnsafe reverts if auction has not yet started or auction is
689
+ // unconfigured, and auction has not sold out or revenues have not been
690
+ // withdrawn.
691
+ // @dev _getPriceUnsafe is guaranteed to be accurate unless the core
692
+ // contract is limiting invocations and we have stale local state
693
+ // returning a false negative that max invocations have been reached.
694
+ // This is acceptable, because that case will revert this
695
+ // call later on in this function, when the core contract's max
696
+ // invocation check fails.
697
+ uint256 currentPriceInWei = _getPriceUnsafe(_projectId);
698
+
699
+ // EFFECTS
700
+ // update the purchaser's receipt and require sufficient net payment
701
+ Receipt storage receipt = receipts[msg.sender][_projectId];
702
+
703
+ // in memory copy + update
704
+ uint256 netPosted = receipt.netPosted + msg.value;
705
+ uint256 numPurchased = receipt.numPurchased + 1;
706
+
707
+ // require sufficient payment on project
708
+ require(
709
+ netPosted >= numPurchased * currentPriceInWei,
710
+ "Must send minimum value to mint"
711
+ );
712
+
713
+ // update Receipt in storage
714
+ // @dev overflow checks are not required since the added values cannot
715
+ // be enough to overflow due to maximum invocations or supply of ETH
716
+ receipt.netPosted = uint232(netPosted);
717
+ receipt.numPurchased = uint24(numPurchased);
718
+
719
+ // emit event indicating new receipt state
720
+ emit ReceiptUpdated(msg.sender, _projectId, numPurchased, netPosted);
721
+
722
+ // update latest purchase price (on this minter) in storage
723
+ // @dev this is used to enforce monotonically decreasing purchase price
724
+ // across multiple auctions
725
+ _projectConfig.latestPurchasePrice = currentPriceInWei;
726
+
727
+ tokenId = minterFilter.mint(_to, _projectId, msg.sender);
728
+
729
+ // invocation is token number plus one, and will never overflow due to
730
+ // limit of 1e6 invocations per project. block scope for gas efficiency
731
+ // (i.e. avoid an unnecessary var initialization to 0).
732
+ unchecked {
733
+ uint256 tokenInvocation = (tokenId % ONE_MILLION) + 1;
734
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
735
+ // handle the case where the token invocation == minter local max
736
+ // invocations occurred on a different minter, and we have a stale
737
+ // local maxHasBeenInvoked value returning a false negative.
738
+ // @dev this is a CHECK after EFFECTS, so security was considered
739
+ // in detail here.
740
+ require(
741
+ tokenInvocation <= localMaxInvocations,
742
+ "Maximum number of invocations reached"
743
+ );
744
+ // in typical case, update the local maxHasBeenInvoked value
745
+ // to true if the token invocation == minter local max invocations
746
+ // (enables gas efficient reverts after sellout)
747
+ if (tokenInvocation == localMaxInvocations) {
748
+ _projectConfig.maxHasBeenInvoked = true;
749
+ }
750
+ }
751
+
752
+ // INTERACTIONS
753
+ if (_projectConfig.auctionRevenuesCollected) {
754
+ // if revenues have been collected, split funds immediately.
755
+ // @dev note that we are guaranteed to be at auction base price,
756
+ // since we know we didn't sellout prior to this tx.
757
+ // note that we don't refund msg.sender here, since a separate
758
+ // settlement mechanism is provided on this minter, unrelated to
759
+ // msg.value
760
+ splitRevenuesETH(
761
+ _projectId,
762
+ currentPriceInWei,
763
+ genArt721CoreAddress
764
+ );
765
+ } else {
766
+ // increment the number of settleable invocations that will be
767
+ // claimable by the artist and admin once auction is validated.
768
+ // do not split revenue here since will be claimed at a later time.
769
+ _projectConfig.numSettleableInvocations++;
770
+ }
771
+
772
+ return tokenId;
773
+ }
774
+
775
+ /**
776
+ * @notice Reclaims the sender's payment above current settled price for
777
+ * project `_projectId`. The current settled price is the the price paid
778
+ * for the most recently purchased token, or the base price if the artist
779
+ * has withdrawn revenues after the auction reached base price.
780
+ * This function is callable at any point, but is expected to typically be
781
+ * called after auction has sold out above base price or after the auction
782
+ * has been purchased at base price. This minimizes the amount of gas
783
+ * required to send all excess settlement funds to the sender.
784
+ * Sends excess settlement funds to msg.sender.
785
+ * @param _projectId Project ID to reclaim excess settlement funds on.
786
+ */
787
+ function reclaimProjectExcessSettlementFunds(uint256 _projectId) external {
788
+ reclaimProjectExcessSettlementFundsTo(payable(msg.sender), _projectId);
789
+ }
790
+
791
+ /**
792
+ * @notice Reclaims the sender's payment above current settled price for
793
+ * project `_projectId`. The current settled price is the the price paid
794
+ * for the most recently purchased token, or the base price if the artist
795
+ * has withdrawn revenues after the auction reached base price.
796
+ * This function is callable at any point, but is expected to typically be
797
+ * called after auction has sold out above base price or after the auction
798
+ * has been purchased at base price. This minimizes the amount of gas
799
+ * required to send all excess settlement funds.
800
+ * Sends excess settlement funds to address `_to`.
801
+ * @param _to Address to send excess settlement funds to.
802
+ * @param _projectId Project ID to reclaim excess settlement funds on.
803
+ */
804
+ function reclaimProjectExcessSettlementFundsTo(
805
+ address payable _to,
806
+ uint256 _projectId
807
+ ) public nonReentrant {
808
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
809
+ Receipt storage receipt = receipts[msg.sender][_projectId];
810
+ uint256 numPurchased = receipt.numPurchased;
811
+ // CHECKS
812
+ // input validation
813
+ require(_to != address(0), "No claiming to the zero address");
814
+ // require that a user has purchased at least one token on this project
815
+ require(numPurchased > 0, "No purchases made by this address");
816
+ // get the latestPurchasePrice, which returns the sellout price if the
817
+ // auction sold out before reaching base price, or returns the base
818
+ // price if auction has reached base price and artist has withdrawn
819
+ // revenues.
820
+ // @dev if user is eligible for a reclaiming, they have purchased a
821
+ // token, therefore we are guaranteed to have a populated
822
+ // latestPurchasePrice
823
+ uint256 currentSettledTokenPrice = _projectConfig.latestPurchasePrice;
824
+
825
+ // EFFECTS
826
+ // calculate the excess settlement funds amount
827
+ // implicit overflow/underflow checks in solidity ^0.8
828
+ uint256 requiredAmountPosted = numPurchased * currentSettledTokenPrice;
829
+ uint256 excessSettlementFunds = receipt.netPosted -
830
+ requiredAmountPosted;
831
+ // update Receipt in storage
832
+ receipt.netPosted = requiredAmountPosted.toUint232();
833
+ // emit event indicating new receipt state
834
+ emit ReceiptUpdated(
835
+ msg.sender,
836
+ _projectId,
837
+ numPurchased,
838
+ requiredAmountPosted
839
+ );
840
+
841
+ // INTERACTIONS
842
+ bool success_;
843
+ (success_, ) = _to.call{value: excessSettlementFunds}("");
844
+ require(success_, "Reclaiming failed");
845
+ }
846
+
847
+ /**
848
+ * @notice Reclaims the sender's payment above current settled price for
849
+ * projects in `_projectIds`. The current settled price is the the price
850
+ * paid for the most recently purchased token, or the base price if the
851
+ * artist has withdrawn revenues after the auction reached base price.
852
+ * This function is callable at any point, but is expected to typically be
853
+ * called after auction has sold out above base price or after the auction
854
+ * has been purchased at base price. This minimizes the amount of gas
855
+ * required to send all excess settlement funds to the sender.
856
+ * Sends total of all excess settlement funds to msg.sender in a single
857
+ * chunk. Entire transaction reverts if any excess settlement calculation
858
+ * fails.
859
+ * @param _projectIds Array of project IDs to reclaim excess settlement
860
+ * funds on.
861
+ */
862
+ function reclaimProjectsExcessSettlementFunds(
863
+ uint256[] calldata _projectIds
864
+ ) external {
865
+ reclaimProjectsExcessSettlementFundsTo(
866
+ payable(msg.sender),
867
+ _projectIds
868
+ );
869
+ }
870
+
871
+ /**
872
+ * @notice Reclaims the sender's payment above current settled price for
873
+ * projects in `_projectIds`. The current settled price is the the price
874
+ * paid for the most recently purchased token, or the base price if the
875
+ * artist has withdrawn revenues after the auction reached base price.
876
+ * This function is callable at any point, but is expected to typically be
877
+ * called after auction has sold out above base price or after the auction
878
+ * has been purchased at base price. This minimizes the amount of gas
879
+ * required to send all excess settlement funds to the sender.
880
+ * Sends total of all excess settlement funds to `_to` in a single
881
+ * chunk. Entire transaction reverts if any excess settlement calculation
882
+ * fails.
883
+ * @param _to Address to send excess settlement funds to.
884
+ * @param _projectIds Array of project IDs to reclaim excess settlement
885
+ * funds on.
886
+ */
887
+ function reclaimProjectsExcessSettlementFundsTo(
888
+ address payable _to,
889
+ uint256[] memory _projectIds
890
+ ) public nonReentrant {
891
+ // CHECKS
892
+ // input validation
893
+ require(_to != address(0), "No claiming to the zero address");
894
+ // EFFECTS
895
+ // for each project, tally up the excess settlement funds and update
896
+ // the receipt in storage
897
+ uint256 excessSettlementFunds;
898
+ uint256 projectIdsLength = _projectIds.length;
899
+ for (uint256 i; i < projectIdsLength; ) {
900
+ uint256 projectId = _projectIds[i];
901
+ ProjectConfig storage _projectConfig = projectConfig[projectId];
902
+ Receipt storage receipt = receipts[msg.sender][projectId];
903
+ uint256 numPurchased = receipt.numPurchased;
904
+ // input validation
905
+ // require that a user has purchased at least one token on this project
906
+ require(numPurchased > 0, "No purchases made by this address");
907
+ // get the latestPurchasePrice, which returns the sellout price if the
908
+ // auction sold out before reaching base price, or returns the base
909
+ // price if auction has reached base price and artist has withdrawn
910
+ // revenues.
911
+ // @dev if user is eligible for a claim, they have purchased a token,
912
+ // therefore we are guaranteed to have a populated
913
+ // latestPurchasePrice
914
+ uint256 currentSettledTokenPrice = _projectConfig
915
+ .latestPurchasePrice;
916
+ // calculate the excessSettlementFunds amount
917
+ // implicit overflow/underflow checks in solidity ^0.8
918
+ uint256 requiredAmountPosted = numPurchased *
919
+ currentSettledTokenPrice;
920
+ excessSettlementFunds += (receipt.netPosted - requiredAmountPosted);
921
+ // reduce the netPosted (in storage) to value after excess settlement
922
+ // funds deducted
923
+ receipt.netPosted = requiredAmountPosted.toUint232();
924
+ // emit event indicating new receipt state
925
+ emit ReceiptUpdated(
926
+ msg.sender,
927
+ projectId,
928
+ numPurchased,
929
+ requiredAmountPosted
930
+ );
931
+ // gas efficiently increment i
932
+ // won't overflow due to for loop, as well as gas limts
933
+ unchecked {
934
+ ++i;
935
+ }
936
+ }
937
+
938
+ // INTERACTIONS
939
+ // send excess settlement funds in a single chunk for all
940
+ // projects
941
+ bool success_;
942
+ (success_, ) = _to.call{value: excessSettlementFunds}("");
943
+ require(success_, "Reclaiming failed");
944
+ }
945
+
946
+ /**
947
+ * @notice Gets price of minting a token on project `_projectId` given
948
+ * the project's AuctionParameters and current block timestamp.
949
+ * Reverts if auction has not yet started or auction is unconfigured, and
950
+ * auction has not sold out or revenues have not been withdrawn.
951
+ * Price is guaranteed to be accurate, regardless of the current state of
952
+ * the locally cached minter max invocations.
953
+ * @dev This method is less gas efficient than `_getPriceUnsafe`, but is
954
+ * guaranteed to be accurate.
955
+ * @param _projectId Project ID to get price of token for.
956
+ * @return tokenPriceInWei current price of token in Wei
957
+ * @dev This method calculates price decay using a linear interpolation
958
+ * of exponential decay based on the artist-provided half-life for price
959
+ * decay, `_priceDecayHalfLifeSeconds`.
960
+ */
961
+ function _getPriceSafe(
962
+ uint256 _projectId
963
+ ) private view returns (uint256 tokenPriceInWei) {
964
+ // accurately check if project has sold out
965
+ if (_projectMaxHasBeenInvokedSafe(_projectId)) {
966
+ // max invocations have been reached, return the latest purchased
967
+ // price
968
+ tokenPriceInWei = projectConfig[_projectId].latestPurchasePrice;
969
+ } else {
970
+ // if not sold out, return the current price
971
+ tokenPriceInWei = _getPriceUnsafe(_projectId);
972
+ }
973
+ return tokenPriceInWei;
974
+ }
975
+
976
+ /**
977
+ * @notice Gets price of minting a token on project `_projectId` given
978
+ * the project's AuctionParameters and current block timestamp.
979
+ * Reverts if auction has not yet started or auction is unconfigured, and
980
+ * local hasMaxBeenInvoked is false and revenues have not been withdrawn.
981
+ * Price is guaranteed to be accurate unless the minter's local
982
+ * hasMaxBeenInvoked is stale and returning a false negative.
983
+ * @dev when an accurate price is required regardless of the current state
984
+ * state of the locally cached minter max invocations, use the less gas
985
+ * efficient function `_getPriceSafe`.
986
+ * @param _projectId Project ID to get price of token for.
987
+ * @return uint256 current price of token in Wei, accurate if minter max
988
+ * invocations are up to date
989
+ * @dev This method calculates price decay using a linear interpolation
990
+ * of exponential decay based on the artist-provided half-life for price
991
+ * decay, `_priceDecayHalfLifeSeconds`.
992
+ */
993
+ function _getPriceUnsafe(
994
+ uint256 _projectId
995
+ ) private view returns (uint256) {
996
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
997
+ // return latest purchase price if:
998
+ // - minter is aware of a sold-out auction (without updating max
999
+ // invocation value)
1000
+ // - auction revenues have been collected, at which point the
1001
+ // latest purchase price will never change again
1002
+ if (
1003
+ _projectConfig.maxHasBeenInvoked ||
1004
+ _projectConfig.auctionRevenuesCollected
1005
+ ) {
1006
+ return _projectConfig.latestPurchasePrice;
1007
+ }
1008
+ // otherwise calculate price based on current block timestamp and
1009
+ // auction configuration (will revert if auction has not started)
1010
+ // move parameters to memory if used more than once
1011
+ uint256 _timestampStart = uint256(_projectConfig.timestampStart);
1012
+ uint256 _priceDecayHalfLifeSeconds = uint256(
1013
+ _projectConfig.priceDecayHalfLifeSeconds
1014
+ );
1015
+ uint256 _basePrice = _projectConfig.basePrice;
1016
+
1017
+ require(block.timestamp > _timestampStart, "Auction not yet started");
1018
+ require(_priceDecayHalfLifeSeconds > 0, "Only configured auctions");
1019
+ uint256 decayedPrice = _projectConfig.startPrice;
1020
+ uint256 elapsedTimeSeconds;
1021
+ unchecked {
1022
+ // already checked that block.timestamp > _timestampStart above
1023
+ elapsedTimeSeconds = block.timestamp - _timestampStart;
1024
+ }
1025
+ // Divide by two (via bit-shifting) for the number of entirely completed
1026
+ // half-lives that have elapsed since auction start time.
1027
+ unchecked {
1028
+ // already required _priceDecayHalfLifeSeconds > 0
1029
+ decayedPrice >>= elapsedTimeSeconds / _priceDecayHalfLifeSeconds;
1030
+ }
1031
+ // Perform a linear interpolation between partial half-life points, to
1032
+ // approximate the current place on a perfect exponential decay curve.
1033
+ unchecked {
1034
+ // value of expression is provably always less than decayedPrice,
1035
+ // so no underflow is possible when the subtraction assignment
1036
+ // operator is used on decayedPrice.
1037
+ decayedPrice -=
1038
+ (decayedPrice *
1039
+ (elapsedTimeSeconds % _priceDecayHalfLifeSeconds)) /
1040
+ _priceDecayHalfLifeSeconds /
1041
+ 2;
1042
+ }
1043
+ if (decayedPrice < _basePrice) {
1044
+ // Price may not decay below stay `basePrice`.
1045
+ return _basePrice;
1046
+ }
1047
+ return decayedPrice;
1048
+ }
1049
+
1050
+ /**
1051
+ * @notice Gets the current excess settlement funds on project `_projectId`
1052
+ * for address `_walletAddress`. The returned value is expected to change
1053
+ * throughtout an auction, since the latest purchase price is used when
1054
+ * determining excess settlement funds.
1055
+ * A user may claim excess settlement funds by calling the function
1056
+ * `reclaimProjectExcessSettlementFunds(_projectId)`.
1057
+ * @param _projectId Project ID to query.
1058
+ * @param _walletAddress Account address for which the excess posted funds
1059
+ * is being queried.
1060
+ * @return excessSettlementFundsInWei Amount of excess settlement funds, in
1061
+ * wei
1062
+ */
1063
+ function getProjectExcessSettlementFunds(
1064
+ uint256 _projectId,
1065
+ address _walletAddress
1066
+ ) external view returns (uint256 excessSettlementFundsInWei) {
1067
+ // input validation
1068
+ require(_walletAddress != address(0), "No zero address");
1069
+ // load struct from storage
1070
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1071
+ Receipt storage receipt = receipts[_walletAddress][_projectId];
1072
+ // require that a user has purchased at least one token on this project
1073
+ require(receipt.numPurchased > 0, "No purchases made by this address");
1074
+ // get the latestPurchasePrice, which returns the sellout price if the
1075
+ // auction sold out before reaching base price, or returns the base
1076
+ // price if auction has reached base price and artist has withdrawn
1077
+ // revenues.
1078
+ // @dev if user is eligible for a reclaiming, they have purchased a
1079
+ // token, therefore we are guaranteed to have a populated
1080
+ // latestPurchasePrice
1081
+ uint256 currentSettledTokenPrice = _projectConfig.latestPurchasePrice;
1082
+
1083
+ // EFFECTS
1084
+ // calculate the excess settlement funds amount and return
1085
+ // implicit overflow/underflow checks in solidity ^0.8
1086
+ uint256 requiredAmountPosted = receipt.numPurchased *
1087
+ currentSettledTokenPrice;
1088
+ excessSettlementFundsInWei = receipt.netPosted - requiredAmountPosted;
1089
+ return excessSettlementFundsInWei;
1090
+ }
1091
+
1092
+ /**
1093
+ * @notice Gets the latest purchase price for project `_projectId`, or 0 if
1094
+ * no purchases have been made.
1095
+ */
1096
+ function getProjectLatestPurchasePrice(
1097
+ uint256 _projectId
1098
+ ) external view returns (uint256 latestPurchasePrice) {
1099
+ return projectConfig[_projectId].latestPurchasePrice;
1100
+ }
1101
+
1102
+ /**
1103
+ * @notice Gets the number of settleable invocations for project `_projectId`.
1104
+ */
1105
+ function getNumSettleableInvocations(
1106
+ uint256 _projectId
1107
+ ) external view returns (uint256 numSettleableInvocations) {
1108
+ return projectConfig[_projectId].numSettleableInvocations;
1109
+ }
1110
+
1111
+ /**
1112
+ * @notice Gets if price of token is configured, price of minting a
1113
+ * token on project `_projectId`, and currency symbol and address to be
1114
+ * used as payment. Supersedes any core contract price information.
1115
+ * @param _projectId Project ID to get price information for.
1116
+ * @return isConfigured true only if project's auction parameters have been
1117
+ * configured on this minter
1118
+ * @return tokenPriceInWei current price of token on this minter - invalid
1119
+ * if auction has not yet been configured
1120
+ * @return currencySymbol currency symbol for purchases of project on this
1121
+ * minter. This minter always returns "ETH"
1122
+ * @return currencyAddress currency address for purchases of project on
1123
+ * this minter. This minter always returns null address, reserved for ether
1124
+ */
1125
+ function getPriceInfo(
1126
+ uint256 _projectId
1127
+ )
1128
+ external
1129
+ view
1130
+ returns (
1131
+ bool isConfigured,
1132
+ uint256 tokenPriceInWei,
1133
+ string memory currencySymbol,
1134
+ address currencyAddress
1135
+ )
1136
+ {
1137
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1138
+
1139
+ isConfigured = (_projectConfig.startPrice > 0);
1140
+ if (block.timestamp <= _projectConfig.timestampStart) {
1141
+ // Provide a reasonable value for `tokenPriceInWei` when it would
1142
+ // otherwise revert, using the starting price before auction starts.
1143
+ tokenPriceInWei = _projectConfig.startPrice;
1144
+ } else if (_projectConfig.startPrice == 0) {
1145
+ // In the case of unconfigured auction, return price of zero when
1146
+ // it would otherwise revert
1147
+ tokenPriceInWei = 0;
1148
+ } else {
1149
+ tokenPriceInWei = _getPriceSafe(_projectId);
1150
+ }
1151
+ currencySymbol = "ETH";
1152
+ currencyAddress = address(0);
1153
+ }
1154
+
1155
+ /**
1156
+ * @notice Sets the local max invocation values of a project equal to the
1157
+ * values on the core contract.
1158
+ * @param _projectId Project ID to set the maximum invocations for.
1159
+ */
1160
+ function _syncProjectMaxInvocationsCoreCached(uint256 _projectId) internal {
1161
+ uint256 coreMaxInvocations;
1162
+ uint256 coreInvocations;
1163
+ (
1164
+ coreInvocations,
1165
+ coreMaxInvocations
1166
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1167
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1168
+ // update storage with results, emit event after change
1169
+ _projectConfig.maxInvocations = uint24(coreMaxInvocations);
1170
+ _projectConfig.maxHasBeenInvoked =
1171
+ coreMaxInvocations == coreInvocations;
1172
+
1173
+ emit ProjectMaxInvocationsLimitUpdated(_projectId, coreMaxInvocations);
1174
+ }
1175
+
1176
+ /**
1177
+ * @notice Returns the current invocations and maximum invocations of
1178
+ * project `_projectId` from the core contract.
1179
+ * @param _projectId Project ID to get invocations and maximum invocations
1180
+ * for.
1181
+ * @return invocations current invocations of project.
1182
+ * @return maxInvocations maximum invocations of project.
1183
+ */
1184
+ function _getProjectCoreInvocationsAndMaxInvocations(
1185
+ uint256 _projectId
1186
+ ) internal view returns (uint256 invocations, uint256 maxInvocations) {
1187
+ (invocations, maxInvocations, , , , ) = genArtCoreContract_Base
1188
+ .projectStateData(_projectId);
1189
+ }
1190
+
1191
+ /**
1192
+ * @notice Verifies the cached values of a project's maxInvocation state
1193
+ * are logically consistent with the core contract's maxInvocation state,
1194
+ * or populates them to equal the core contract's maxInvocation state if
1195
+ * they have never been populated.
1196
+ */
1197
+ function _refreshMaxInvocations(uint256 _projectId) internal {
1198
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1199
+ // project's max invocations and has max been invoked can only be
1200
+ // initial values if never populated, because setting a maxInvocations
1201
+ // of zero means maxHasBeenInvoked would be set to true
1202
+ bool notPopulated = (_projectConfig.maxInvocations == 0 &&
1203
+ _projectConfig.maxHasBeenInvoked == false);
1204
+ if (notPopulated) {
1205
+ // sync the minter max invocation state to equal the values on the
1206
+ // core contract (least restrictive state)
1207
+ _syncProjectMaxInvocationsCoreCached(_projectId);
1208
+ } else {
1209
+ // if using local max invocations, validate the local state
1210
+ // (i.e. ensure local max invocations not greater than core max
1211
+ // invocations)
1212
+ _validateProjectMaxInvocations(_projectId);
1213
+ }
1214
+ }
1215
+
1216
+ /**
1217
+ * @notice Checks and updates local project max invocations to determine if
1218
+ * if they are in an illogical state relative to the core contract's max
1219
+ * invocations.
1220
+ * This updates the project's local max invocations if the value is greater
1221
+ * than the core contract's max invocations, which is an illogical state
1222
+ * since V3 core contracts cannot increase max invocations. In that case,
1223
+ * the project's local max invocations are set to the core contract's max
1224
+ * invocations, and the project's `maxHasBeenInvoked` state is refreshed.
1225
+ * This also updates the project's `maxHasBeenInvoked` state if the core
1226
+ * contract's invocations are greater than or equal to the minter's local
1227
+ * max invocations. This handles the case where a different minter has been
1228
+ * used to mint above the local max invocations, which would cause
1229
+ * `maxHasBeenInvoked` to return a false negative.
1230
+ * @param _projectId Project ID to set the maximum invocations for.
1231
+ */
1232
+ function _validateProjectMaxInvocations(uint256 _projectId) internal {
1233
+ uint256 coreMaxInvocations;
1234
+ uint256 coreInvocations;
1235
+ (
1236
+ coreInvocations,
1237
+ coreMaxInvocations
1238
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1239
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1240
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
1241
+ // check if local max invocations is illogical relative to core
1242
+ // contract's max invocations
1243
+ if (localMaxInvocations > coreMaxInvocations) {
1244
+ // set local max invocations to core contract's max invocations
1245
+ _projectConfig.maxInvocations = uint24(coreMaxInvocations);
1246
+ // update the project's `maxHasBeenInvoked` state
1247
+ // @dev core values are equivalent to local values, use for gas
1248
+ // efficiency
1249
+ _projectConfig.maxHasBeenInvoked = (coreMaxInvocations ==
1250
+ coreInvocations);
1251
+ emit ProjectMaxInvocationsLimitUpdated(
1252
+ _projectId,
1253
+ coreMaxInvocations
1254
+ );
1255
+ } else if (coreInvocations >= localMaxInvocations) {
1256
+ // ensure the local `maxHasBeenInvoked` state is accurate to
1257
+ // prevent any false negatives due to minting on other minters
1258
+ _projectConfig.maxHasBeenInvoked = true;
1259
+ // emit event to ensure any indexers are aware of the change
1260
+ // @dev this is not strictly necessary, but is included for
1261
+ // convenience
1262
+ emit ProjectMaxInvocationsLimitUpdated(
1263
+ _projectId,
1264
+ coreMaxInvocations
1265
+ );
1266
+ }
1267
+ }
1268
+
1269
+ /**
1270
+ * @notice Returns true if the project `_projectId` is sold out, false
1271
+ * otherwise. This function returns an accurate value regardless of whether
1272
+ * the project's maximum invocations value cached locally on the minter is
1273
+ * up to date with the core contract's maximum invocations value.
1274
+ * @param _projectId Project ID to check if sold out.
1275
+ * @return bool true if the project is sold out, false otherwise.
1276
+ * @dev this is a view method, and will not update the minter's local
1277
+ * cached state.
1278
+ */
1279
+ function _projectMaxHasBeenInvokedSafe(
1280
+ uint256 _projectId
1281
+ ) internal view returns (bool) {
1282
+ // get max invocations from core contract
1283
+ uint256 coreInvocations;
1284
+ uint256 coreMaxInvocations;
1285
+ (
1286
+ coreInvocations,
1287
+ coreMaxInvocations
1288
+ ) = _getProjectCoreInvocationsAndMaxInvocations(_projectId);
1289
+ ProjectConfig storage _projectConfig = projectConfig[_projectId];
1290
+ uint256 localMaxInvocations = _projectConfig.maxInvocations;
1291
+ // value is locally defined, and could be out of date.
1292
+ // only possible illogical state is if local max invocations is
1293
+ // greater than core contract's max invocations, in which case
1294
+ // we should use the core contract's max invocations
1295
+ if (localMaxInvocations > coreMaxInvocations) {
1296
+ // local max invocations is stale and illogical, defer to core
1297
+ // contract's max invocations since it is the limiting factor
1298
+ return (coreMaxInvocations == coreInvocations);
1299
+ }
1300
+ // local max invocations is limiting, so check core invocations against
1301
+ // local max invocations
1302
+ return (coreInvocations >= localMaxInvocations);
1303
+ }
1304
+ }