@artblocks/contracts 1.2.1 → 1.3.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 (71) hide show
  1. package/artifacts/contracts/BytecodeStorageReaders/BytecodeStorageReaderContractV2_Web3Call.sol/BytecodeStorageReaderContractV2_Web3Call.json +201 -0
  2. package/artifacts/contracts/BytecodeStorageReaders/UniversalBytecodeStorageReader.sol/UniversalBytecodeStorageReader.json +2 -2
  3. package/artifacts/contracts/DependencyRegistryV0.sol/DependencyRegistryV0.json +42 -53
  4. package/artifacts/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol/GenArt721CoreV3_Engine_Flex.json +2 -2
  5. package/artifacts/contracts/generator/GenArt721GeneratorV0.sol/GenArt721GeneratorV0.json +2 -2
  6. package/artifacts/contracts/interfaces/v0.8.x/IDelegateRegistry.sol/IDelegateRegistry.json +820 -0
  7. package/artifacts/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol/IDependencyRegistryV0.json +13 -0
  8. package/artifacts/contracts/interfaces/v0.8.x/IPMPAugmentHook.sol/IPMPAugmentHook.json +83 -0
  9. package/artifacts/contracts/interfaces/v0.8.x/IPMPConfigureHook.sol/IPMPConfigureHook.json +80 -0
  10. package/artifacts/contracts/interfaces/v0.8.x/IPMPV0.sol/IPMPV0.json +402 -0
  11. package/artifacts/contracts/interfaces/v0.8.x/IUniversalBytecodeStorageReader.sol/IUniversalBytecodeStorageReader.json +13 -0
  12. package/artifacts/contracts/interfaces/v0.8.x/IWeb3Call.sol/IWeb3Call.json +47 -0
  13. package/artifacts/contracts/libs/integration-refs/delegation-registry/DelegateRegistry.sol/DelegateRegistry.json +846 -0
  14. package/artifacts/contracts/libs/integration-refs/delegation-registry/libraries/RegistryHashes.sol/RegistryHashes.json +10 -0
  15. package/artifacts/contracts/libs/integration-refs/delegation-registry/libraries/RegistryOps.sol/RegistryOps.json +10 -0
  16. package/artifacts/contracts/libs/integration-refs/delegation-registry/libraries/RegistryStorage.sol/RegistryStorage.json +10 -0
  17. package/artifacts/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol/DependencyRegistryStorageLib.json +2 -2
  18. package/artifacts/contracts/libs/v0.8.x/ImmutableStringArray.sol/ImmutableStringArray.json +10 -0
  19. package/artifacts/contracts/libs/v0.8.x/V3FlexLib.sol/V3FlexLib.json +2 -2
  20. package/artifacts/contracts/mock/ImmutableStringArrayMock.sol/ImmutableStringArrayMock.json +69 -0
  21. package/artifacts/contracts/mock/MockPMPAugmentHook.sol/MockPMPAugmentHook.json +214 -0
  22. package/artifacts/contracts/mock/MockPMPConfigureHook.sol/MockPMPConfigureHook.json +236 -0
  23. package/artifacts/contracts/web3call/PMPV0.sol/PMPV0.json +629 -0
  24. package/artifacts/contracts/web3call/Web3Call.sol/Web3Call.json +66 -0
  25. package/artifacts/contracts/web3call/augment-hooks/AbstractPMPAugmentHook.sol/AbstractPMPAugmentHook.json +83 -0
  26. package/artifacts/contracts/web3call/augment-hooks/InjectBaseGasFee.sol/InjectBaseGasFee.json +83 -0
  27. package/artifacts/contracts/web3call/augment-hooks/InjectChromieSquiggleFloorPrice.sol/IUniswapV2Pair.json +60 -0
  28. package/artifacts/contracts/web3call/augment-hooks/InjectChromieSquiggleFloorPrice.sol/InjectChromieSquiggleFloorPrice.json +107 -0
  29. package/artifacts/contracts/web3call/augment-hooks/InjectIsNounsDaoMember.sol/InjectIsNounsDaoMember.json +96 -0
  30. package/artifacts/contracts/web3call/augment-hooks/InjectIsProjectFullyMinted.sol/InjectIsProjectFullyMinted.json +83 -0
  31. package/artifacts/contracts/web3call/augment-hooks/InjectPolyptychTokenHash.sol/InjectPolyptychTokenHash.json +141 -0
  32. package/artifacts/contracts/web3call/augment-hooks/InjectTokenHashSeed.sol/InjectTokenHashSeed.json +99 -0
  33. package/artifacts/contracts/web3call/augment-hooks/InjectTokenOwner.sol/InjectTokenOwner.json +99 -0
  34. package/artifacts/contracts/web3call/augment-hooks/InjectTokenOwnerEthBalance.sol/InjectTokenOwnerEthBalance.json +83 -0
  35. package/artifacts/contracts/web3call/augment-hooks/InjectUSDCTotalSupply.sol/InjectUSDCTotalSupply.json +107 -0
  36. package/artifacts/contracts/web3call/configure-hooks/abstractPMPConfigureHook.sol/AbstractPMPConfigureHook.json +80 -0
  37. package/contracts/BytecodeStorageReaders/BytecodeStorageReaderContractV2_Web3Call.sol +128 -0
  38. package/contracts/DependencyRegistryV0.sol +67 -28
  39. package/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol +7 -1
  40. package/contracts/generator/GenArt721GeneratorV0.sol +77 -8
  41. package/contracts/interfaces/v0.8.x/IDelegateRegistry.sol +318 -0
  42. package/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol +2 -0
  43. package/contracts/interfaces/v0.8.x/IPMPAugmentHook.sol +31 -0
  44. package/contracts/interfaces/v0.8.x/IPMPConfigureHook.sol +25 -0
  45. package/contracts/interfaces/v0.8.x/IPMPV0.sol +224 -0
  46. package/contracts/interfaces/v0.8.x/IUniversalBytecodeStorageReader.sol +5 -0
  47. package/contracts/interfaces/v0.8.x/IWeb3Call.sol +28 -0
  48. package/contracts/libs/integration-refs/delegation-registry/DelegateRegistry.sol +753 -0
  49. package/contracts/libs/integration-refs/delegation-registry/libraries/RegistryHashes.sol +359 -0
  50. package/contracts/libs/integration-refs/delegation-registry/libraries/RegistryOps.sol +31 -0
  51. package/contracts/libs/integration-refs/delegation-registry/libraries/RegistryStorage.sol +87 -0
  52. package/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol +3 -0
  53. package/contracts/libs/v0.8.x/ImmutableStringArray.sol +301 -0
  54. package/contracts/libs/v0.8.x/V3FlexLib.sol +7 -1
  55. package/contracts/mock/ImmutableStringArrayMock.sol +47 -0
  56. package/contracts/mock/MockPMPAugmentHook.sol +170 -0
  57. package/contracts/mock/MockPMPConfigureHook.sol +78 -0
  58. package/contracts/web3call/PMPV0.sol +1126 -0
  59. package/contracts/web3call/Web3Call.sol +25 -0
  60. package/contracts/web3call/augment-hooks/AbstractPMPAugmentHook.sol +46 -0
  61. package/contracts/web3call/augment-hooks/InjectBaseGasFee.sol +57 -0
  62. package/contracts/web3call/augment-hooks/InjectChromieSquiggleFloorPrice.sol +103 -0
  63. package/contracts/web3call/augment-hooks/InjectIsNounsDaoMember.sol +66 -0
  64. package/contracts/web3call/augment-hooks/InjectIsProjectFullyMinted.sol +73 -0
  65. package/contracts/web3call/augment-hooks/InjectPolyptychTokenHash.sol +88 -0
  66. package/contracts/web3call/augment-hooks/InjectTokenHashSeed.sol +62 -0
  67. package/contracts/web3call/augment-hooks/InjectTokenOwner.sol +60 -0
  68. package/contracts/web3call/augment-hooks/InjectTokenOwnerEthBalance.sol +61 -0
  69. package/contracts/web3call/augment-hooks/InjectUSDCTotalSupply.sol +69 -0
  70. package/contracts/web3call/configure-hooks/abstractPMPConfigureHook.sol +41 -0
  71. package/package.json +5 -5
@@ -0,0 +1,1126 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-only
2
+ // Created By: Art Blocks Inc.
3
+
4
+ pragma solidity 0.8.22;
5
+
6
+ import {IWeb3Call} from "../interfaces/v0.8.x/IWeb3Call.sol";
7
+ import {IPMPV0} from "../interfaces/v0.8.x/IPMPV0.sol";
8
+ import {IPMPConfigureHook} from "../interfaces/v0.8.x/IPMPConfigureHook.sol";
9
+ import {IPMPAugmentHook} from "../interfaces/v0.8.x/IPMPAugmentHook.sol";
10
+ import {IGenArt721CoreContractV3_Base} from "../interfaces/v0.8.x/IGenArt721CoreContractV3_Base.sol";
11
+ import {IERC721} from "@openzeppelin-5.0/contracts/token/ERC721/IERC721.sol";
12
+ import {IDelegateRegistry} from "../interfaces/v0.8.x/IDelegateRegistry.sol";
13
+
14
+ import {ERC165Checker} from "@openzeppelin-5.0/contracts/utils/introspection/ERC165Checker.sol";
15
+ import {Strings} from "@openzeppelin-5.0/contracts/utils/Strings.sol";
16
+ import {ReentrancyGuard} from "@openzeppelin-5.0/contracts/utils/ReentrancyGuard.sol";
17
+
18
+ import {Web3Call} from "./Web3Call.sol";
19
+ import {ImmutableStringArray} from "../libs/v0.8.x/ImmutableStringArray.sol";
20
+ import {ABHelpers} from "../libs/v0.8.x/ABHelpers.sol";
21
+
22
+ /**
23
+ * @title Project Metadata Parameters (PMP) contract, V0
24
+ * @author Art Blocks Inc.
25
+ * @notice This contract enables Artists to define and configure project parameters that token
26
+ * owners can set within constraints. This provides a standardized way for projects to expose
27
+ * configurable parameters that can be used by renderers and other contracts.
28
+ * WARNING: This contract implements an open protocol for parameter configuration, and does not
29
+ * restrict usage to Art Blocks or its affiliates. Use with caution. The contract does assume
30
+ * that the core contract conforms to the IGenArt721CoreContractV3_Base interface for authentication,
31
+ * and that the core contract is using the ERC721 standard for token management. These assumptions
32
+ * may not hold for all core contracts, especially those outside of the Art Blocks ecosystem.
33
+ * @notice Artists may configure arbitrary external hooks for post-token-configuration and
34
+ * read-augmentation. These hooks are executed at the end of the token configuration process
35
+ * and when reading token PMPs, respectively. These hooks are validated for ERC165 interface
36
+ * compatibility, but have the ability to execute arbitrary code at the discretion of the artist.
37
+ * The contract implements reentrancy guards to protect against reentrancy attacks during hook calls,
38
+ * resulting in the subcall having a similar level as a minting contract sending funds to an artist's
39
+ * additional payee address during a token sale. Use appropriate discretion.
40
+ * WARNING: Hook calls are not validated, and may cause unexpected behavior. These include:
41
+ * - Hooks may revert the transaction, resulting in denial of service
42
+ * - Hooks may augment the token's returned parameters with unintended data, resulting in
43
+ * unexpected behavior
44
+ * The artist is solely responsible for configuring hooks and validating their behavior.
45
+ * @dev This contract implements the IWeb3Call and IPMPV0 interfaces, providing functionality
46
+ * for parameter configuration and retrieval. It includes support for various parameter types,
47
+ * authorization options, and hooks for extending functionality.
48
+ */
49
+ contract PMPV0 is IPMPV0, Web3Call, ReentrancyGuard {
50
+ using Strings for string;
51
+ using Strings for uint256;
52
+ using Strings for int256;
53
+ using ImmutableStringArray for ImmutableStringArray.StringArray;
54
+
55
+ IDelegateRegistry public immutable delegateRegistry;
56
+ bytes32 public constant DELEGATION_REGISTRY_TOKEN_OWNER_RIGHTS =
57
+ bytes32("postmintparameters");
58
+
59
+ bytes32 private constant _TYPE = "PMPV0";
60
+
61
+ bytes16 private constant _HEX_DIGITS = "0123456789abcdef";
62
+ uint256 private constant _DECIMAL_PRECISION_DIGITS = 10;
63
+ uint256 private constant _DECIMAL_PRECISION =
64
+ 10 ** _DECIMAL_PRECISION_DIGITS;
65
+
66
+ // @dev min hex color assumed to be 0x000000
67
+ // @dev intentionally not including alpha channel, can be separate PMP if desired
68
+ uint256 private constant _HEX_COLOR_MAX = 0xFFFFFF;
69
+
70
+ uint256 private constant _TIMESTAMP_MIN = 0; // @dev unix timestamp, 0 = 1970-01-01
71
+ uint256 private constant _TIMESTAMP_MAX = type(uint64).max; // max guardrail, ~10 billion years
72
+
73
+ /**
74
+ * @notice Storage structure for parameter configuration.
75
+ * @dev Includes additional field highestConfigNonce compared to PMPConfig.
76
+ */
77
+ struct PMPConfigStorage {
78
+ // @dev highest config nonce for which this PMPConfig is valid (relative to projectConfig.configNonce)
79
+ uint8 highestConfigNonce; // slot 0: 1 byte
80
+ AuthOption authOption; // slot 0: 1 byte
81
+ ParamType paramType; // slot 0: 1 byte
82
+ uint48 pmpLockedAfterTimestamp; // slot 0: 6 bytes // @dev uint48 is sufficient to store ~2^48 seconds, ~8,900 years
83
+ address authAddress; // slot 0: 20 bytes
84
+ // @dev store array length as uint8 in slot 0 for SLOAD efficiency during token configuration
85
+ uint8 selectOptionsLength; // slot 0: 1 byte
86
+ // @dev use immutable string array for storage efficiency during project configuration
87
+ ImmutableStringArray.StringArray selectOptions; // slot 1: 32 bytes
88
+ bytes32 minRange; // slot 2: 32 bytes
89
+ bytes32 maxRange; // slot 3: 32 bytes
90
+ }
91
+
92
+ // @dev core contract address and projectId are implicit based on mapping pointing to ProjectConfig struct
93
+ struct ProjectConfig {
94
+ // @dev array of pmpKeys for efficient enumeration, uses efficient SSTORE2 storage
95
+ ImmutableStringArray.StringArray pmpKeys; // slot 0: 32 bytes
96
+ // @dev mapping of pmpKeys to PMPConfigStorage for O(1) access, and cheap updates when no changes
97
+ mapping(bytes32 pmpKeyHash => PMPConfigStorage pmpConfigStorage) pmpConfigsStorage; // slot 1: 32 bytes
98
+ // config nonce that is incremented during each configureProject call
99
+ uint8 configNonce; // slot 2: 1 byte
100
+ // post-configuration hook to be called after a token's PMP is configured
101
+ IPMPConfigureHook tokenPMPPostConfigHook; // slot 2: 20 bytes
102
+ // token pmp read augmentation hook to be called when reading a token's PMPs
103
+ IPMPAugmentHook tokenPMPReadAugmentationHook; // slot 3: 20 bytes
104
+ }
105
+
106
+ // mapping of ProjectConfig structs for each project
107
+ mapping(address coreContract => mapping(uint256 projectId => ProjectConfig projectConfig)) projectConfigs;
108
+
109
+ // mapping of PMP structs for each token
110
+ mapping(address coreContract => mapping(uint256 tokenId => mapping(bytes32 pmpKeyHash => PMPStorage pmp))) tokenPMPs;
111
+
112
+ /**
113
+ * @notice Constructor for PMPV0 contract.
114
+ * @param delegateRegistry_ The address of the delegate registry contract. Intended to be
115
+ * the delegate.xyz v2 contract.
116
+ */
117
+ constructor(IDelegateRegistry delegateRegistry_) {
118
+ delegateRegistry = delegateRegistry_;
119
+ emit DelegationRegistryUpdated(address(delegateRegistry_));
120
+ }
121
+
122
+ /**
123
+ * @notice Configure project hooks for post-configuration and read augmentation.
124
+ * WARNING: Hook calls may revert the transaction, and may cause unexpected behavior.
125
+ * The artist is solely responsible for configuring hooks and validating their behavior.
126
+ * Improper configuration or hooks with unexpected behavior may result in denial of service,
127
+ * unexpected behavior, or other issues.
128
+ * @param coreContract The address of the core contract.
129
+ * @param projectId The project ID to configure hooks for.
130
+ * @param tokenPMPPostConfigHook The hook to call after a token's PMP is configured.
131
+ * @param tokenPMPReadAugmentationHook The hook to call when reading a token's PMPs.
132
+ * @dev Only the project artist can configure project hooks.
133
+ * @dev Both hooks are validated for ERC165 interface compatibility.
134
+ * @dev Uses nonReentrant modifier to prevent reentrancy attacks during hook calls or auth checks.
135
+ */
136
+ function configureProjectHooks(
137
+ address coreContract,
138
+ uint256 projectId,
139
+ IPMPConfigureHook tokenPMPPostConfigHook,
140
+ IPMPAugmentHook tokenPMPReadAugmentationHook
141
+ ) external nonReentrant {
142
+ // only artists may configure project hooks
143
+ _onlyArtist({
144
+ coreContract: coreContract,
145
+ projectId: projectId,
146
+ sender: msg.sender
147
+ });
148
+ // validation - check for ERC165 implementation for non-null hooks
149
+ if (address(tokenPMPPostConfigHook) != address(0)) {
150
+ // use ERC165 checker to validate implementation
151
+ require(
152
+ ERC165Checker.supportsInterface({
153
+ account: address(tokenPMPPostConfigHook),
154
+ interfaceId: type(IPMPConfigureHook).interfaceId
155
+ }),
156
+ "PMP: tokenPMPPostConfigHook does not implement IPMPConfigureHook"
157
+ );
158
+ }
159
+ if (address(tokenPMPReadAugmentationHook) != address(0)) {
160
+ require(
161
+ ERC165Checker.supportsInterface({
162
+ account: address(tokenPMPReadAugmentationHook),
163
+ interfaceId: type(IPMPAugmentHook).interfaceId
164
+ }),
165
+ "PMP: tokenPMPReadAugmentationHook does not implement IPMPAugmentHook"
166
+ );
167
+ }
168
+ // update projectConfig
169
+ ProjectConfig storage projectConfig = projectConfigs[coreContract][
170
+ projectId
171
+ ];
172
+ projectConfig.tokenPMPPostConfigHook = tokenPMPPostConfigHook;
173
+ projectConfig
174
+ .tokenPMPReadAugmentationHook = tokenPMPReadAugmentationHook;
175
+
176
+ // emit event
177
+ emit ProjectHooksConfigured({
178
+ coreContract: coreContract,
179
+ projectId: projectId,
180
+ tokenPMPPostConfigHook: tokenPMPPostConfigHook,
181
+ tokenPMPReadAugmentationHook: tokenPMPReadAugmentationHook
182
+ });
183
+ }
184
+
185
+ /**
186
+ * @notice Configure the available parameters for a project and their constraints.
187
+ * @param coreContract The address of the core contract.
188
+ * @param projectId The project ID to configure parameters for.
189
+ * @param pmpInputConfigs Array of parameter configurations defining the available parameters.
190
+ * @dev Only the project artist can configure project parameters.
191
+ * @dev Each configuration is validated for proper parameter type and constraints.
192
+ * @dev The project's configuration nonce is incremented with each call.
193
+ * @dev Only <= 256 configs are supported.
194
+ * @dev Only <= 255 bytes are supported for pmpKeys.
195
+ * @dev nonReentrant due to auth check that requires interaction.
196
+ */
197
+ function configureProject(
198
+ address coreContract,
199
+ uint256 projectId,
200
+ PMPInputConfig[] calldata pmpInputConfigs
201
+ ) external nonReentrant {
202
+ // only artists may configure projects
203
+ _onlyArtist({
204
+ coreContract: coreContract,
205
+ projectId: projectId,
206
+ sender: msg.sender
207
+ });
208
+ // validate pmpInputConfigs
209
+ uint256 pmpInputConfigsLength = pmpInputConfigs.length;
210
+ // @dev no coverage on else branch due to test complexity
211
+ require(pmpInputConfigsLength <= 256, "PMP: Only <= 256 configs");
212
+ for (uint256 i = 0; i < pmpInputConfigsLength; i++) {
213
+ uint256 keyLengthBytes = bytes(pmpInputConfigs[i].key).length;
214
+ // @dev max key length constraint is a reasonable gas guardrail
215
+ require(
216
+ keyLengthBytes > 0 && keyLengthBytes < 256,
217
+ "PMP: pmpKey cannot be empty or exceed 255 bytes"
218
+ );
219
+ _validatePMPConfig(pmpInputConfigs[i].pmpConfig);
220
+ }
221
+ // store pmpInputConfigs data in ProjectConfig struct
222
+ // @dev load projectConfig storage pointer
223
+ ProjectConfig storage projectConfig = projectConfigs[coreContract][
224
+ projectId
225
+ ];
226
+ // increment config nonce
227
+ // @dev solidity ^0.8 reverts on overflow (greater than 255 edits not supported)
228
+ uint8 newConfigNonce = projectConfig.configNonce + 1;
229
+ projectConfig.configNonce = newConfigNonce;
230
+
231
+ // efficiently sync pmp keys to ProjectConfig
232
+ // copy input pmp keys to memory array for efficient passing to _syncPMPKeys
233
+ string[] memory pmpKeys = new string[](pmpInputConfigsLength);
234
+ for (uint256 i = 0; i < pmpInputConfigsLength; i++) {
235
+ pmpKeys[i] = pmpInputConfigs[i].key;
236
+ }
237
+ _syncPMPKeys({inputKeys: pmpKeys, projectConfig: projectConfig});
238
+ // store pmp configs in ProjectConfig struct's mapping
239
+ for (uint256 i = 0; i < pmpInputConfigsLength; i++) {
240
+ // store pmpConfigStorage in ProjectConfig struct's mapping
241
+ PMPConfigStorage storage pmpConfigStorage = projectConfig
242
+ .pmpConfigsStorage[_getStringHash(pmpKeys[i])];
243
+ {
244
+ // validate that any current pmp at this key is not locked
245
+ uint256 currentPPMLockedAfterTimestamp = pmpConfigStorage
246
+ .pmpLockedAfterTimestamp;
247
+ require(
248
+ currentPPMLockedAfterTimestamp == 0 ||
249
+ currentPPMLockedAfterTimestamp > block.timestamp,
250
+ "PMP: pmp is locked and cannot be updated"
251
+ );
252
+ }
253
+ // update highestConfigNonce
254
+ pmpConfigStorage.highestConfigNonce = newConfigNonce;
255
+ // copy function input pmpConfig data to pmpConfigStorage
256
+ PMPConfig memory inputPMPConfig = pmpInputConfigs[i].pmpConfig;
257
+ pmpConfigStorage.authOption = inputPMPConfig.authOption;
258
+ pmpConfigStorage.paramType = inputPMPConfig.paramType;
259
+ pmpConfigStorage.pmpLockedAfterTimestamp = inputPMPConfig
260
+ .pmpLockedAfterTimestamp;
261
+ pmpConfigStorage.authAddress = inputPMPConfig.authAddress;
262
+ // @dev length already validated <= 255 in _validatePMPConfig, safe to cast unchecked
263
+ pmpConfigStorage.selectOptionsLength = uint8(
264
+ inputPMPConfig.selectOptions.length
265
+ );
266
+ // @dev ImmutableStringArray is optimized for the case where the array is empty.
267
+ ImmutableStringArray.store(
268
+ pmpConfigStorage.selectOptions,
269
+ inputPMPConfig.selectOptions
270
+ );
271
+ pmpConfigStorage.minRange = inputPMPConfig.minRange;
272
+ pmpConfigStorage.maxRange = inputPMPConfig.maxRange;
273
+ }
274
+
275
+ // emit event
276
+ emit ProjectConfigured({
277
+ coreContract: coreContract,
278
+ projectId: projectId,
279
+ pmpInputConfigs: pmpInputConfigs,
280
+ projectConfigNonce: newConfigNonce
281
+ });
282
+ }
283
+
284
+ /**
285
+ * @notice Configure the parameters for a specific token according to project constraints.
286
+ * WARNING: This contract represents an open protocol for parameter configuration, and does not
287
+ * restrict usage to Art Blocks or its affiliates. Use with caution. The contract does assume
288
+ * that the core contract conforms to the IGenArt721CoreContractV3_Base interface for authentication,
289
+ * and that the core contract is using the ERC721 standard for token management. These assumptions
290
+ * may not hold for all core contracts, especially those outside of the Art Blocks ecosystem.
291
+ * WARNING: Hook calls may revert the transaction, and may cause unexpected behavior.
292
+ * The artist is solely responsible for configuring hooks and validating their behavior.
293
+ * Improper configuration or hooks with unexpected behavior may result in denial of service,
294
+ * unexpected behavior, or other issues.
295
+ * ERC-721 Token-level wallet delegation for the TokenOwner role is supported via delegate.xyz v2.
296
+ * For opt-in granular control of rights specific to these operations, vault owners may define subdelegations
297
+ * with bytes32 rights "postmintparameters". Per the delegate.xyz v2 specification, delegations made with the
298
+ * empty string "" will be interpreted as a full delegation of all rights.
299
+ * @param coreContract The address of the core contract.
300
+ * @param tokenId The tokenId of the token to configure.
301
+ * @param pmpInputs The parameter inputs to configure for the token.
302
+ * @dev Validates each parameter input against the project's configuration.
303
+ * @dev Stores the configured parameters for the token.
304
+ * @dev Calls the post-configuration hook if one is configured for the project.
305
+ * @dev Uses nonReentrant modifier to prevent reentrancy attacks during hook calls or auth checks.
306
+ */
307
+ function configureTokenParams(
308
+ address coreContract,
309
+ uint256 tokenId,
310
+ PMPInput[] calldata pmpInputs
311
+ ) external nonReentrant {
312
+ ProjectConfig storage projectConfig = projectConfigs[coreContract][
313
+ ABHelpers.tokenIdToProjectId(tokenId)
314
+ ];
315
+ // assign each pmpInput to the token
316
+ // @dev pmpInputs processed sequentially in order of input
317
+ for (uint256 i = 0; i < pmpInputs.length; i++) {
318
+ bytes32 pmpKeyHash = _getStringHash(pmpInputs[i].key);
319
+ PMPInput memory pmpInput = pmpInputs[i];
320
+ PMPStorage storage tokenPMP = tokenPMPs[coreContract][tokenId][
321
+ pmpKeyHash
322
+ ];
323
+ PMPConfigStorage storage pmpConfigStorage = projectConfig
324
+ .pmpConfigsStorage[pmpKeyHash];
325
+ // validate pmpInput
326
+ _validatePMPInputAndAuth({
327
+ tokenId: tokenId,
328
+ coreContract: coreContract,
329
+ pmpInput: pmpInput,
330
+ pmpConfigStorage: pmpConfigStorage,
331
+ projectConfigNonce: projectConfig.configNonce
332
+ });
333
+ // store pmpInput data in PMPStorage struct
334
+ tokenPMP.configuredParamType = pmpConfigStorage.paramType;
335
+ // only assign the value that affects param type (gas savings)
336
+ if (pmpConfigStorage.paramType == ParamType.String) {
337
+ if (pmpInput.configuringArtistString) {
338
+ tokenPMP.artistConfiguredValueString = pmpInput
339
+ .configuredValueString;
340
+ } else {
341
+ tokenPMP.nonArtistConfiguredValueString = pmpInput
342
+ .configuredValueString;
343
+ }
344
+ } else {
345
+ tokenPMP.configuredValue = pmpInput.configuredValue;
346
+ }
347
+ // call post-config hook if configured for the project
348
+ // @dev this function is nonreentrant for additional reentrancy protection
349
+ // @dev intentionally revert entire transaction if hook reverts to prevent latent failure
350
+ if (address(projectConfig.tokenPMPPostConfigHook) != address(0)) {
351
+ projectConfig.tokenPMPPostConfigHook.onTokenPMPConfigure({
352
+ coreContract: coreContract,
353
+ tokenId: tokenId,
354
+ pmpInput: pmpInput
355
+ });
356
+ }
357
+ }
358
+
359
+ // emit event
360
+ emit TokenParamsConfigured({
361
+ coreContract: coreContract,
362
+ tokenId: tokenId,
363
+ pmpInputs: pmpInputs
364
+ });
365
+ }
366
+
367
+ /**
368
+ * @notice Get the token parameters for a given token.
369
+ * If none are configured, the tokenParams should be empty.
370
+ * WARNING: Hook calls may revert the transaction, and may cause unexpected behavior.
371
+ * The artist is solely responsible for configuring hooks and validating their behavior.
372
+ * Improper configuration or hooks with unexpected behavior may result in denial of service,
373
+ * unexpected behavior, or other issues.
374
+ * @param coreContract The address of the core contract to call.
375
+ * @param tokenId The tokenId of the token to get data for.
376
+ * @return tokenParams An array of token parameters for the queried token.
377
+ */
378
+ function getTokenParams(
379
+ address coreContract,
380
+ uint256 tokenId
381
+ )
382
+ external
383
+ view
384
+ override
385
+ returns (IWeb3Call.TokenParam[] memory tokenParams)
386
+ {
387
+ uint256 projectId = ABHelpers.tokenIdToProjectId(tokenId);
388
+ ProjectConfig storage projectConfig = projectConfigs[coreContract][
389
+ projectId
390
+ ];
391
+ string[] memory pmpKeys = projectConfig.pmpKeys.getAll();
392
+ uint256 pmpKeysLength = pmpKeys.length;
393
+ // @dev initialize tokenParams array with maximum possible length
394
+ tokenParams = new IWeb3Call.TokenParam[](pmpKeysLength);
395
+ uint256 populatedParamsIndex = 0; // @dev index of next populated tokenParam
396
+ for (uint256 i = 0; i < pmpKeysLength; i++) {
397
+ // load pmp value
398
+ bytes32 pmpKeyHash = _getStringHash(pmpKeys[i]);
399
+ (bool isConfigured, string memory value) = _getPMPValue({
400
+ pmpConfigStorage: projectConfig.pmpConfigsStorage[pmpKeyHash],
401
+ pmp: tokenPMPs[coreContract][tokenId][pmpKeyHash]
402
+ });
403
+ // continue when param is unconfigured
404
+ if (!isConfigured) {
405
+ // continue without incrementing populatedParamsIndex
406
+ continue;
407
+ }
408
+ // append configured to tokenParams array
409
+ tokenParams[populatedParamsIndex] = IWeb3Call.TokenParam({
410
+ key: pmpKeys[i],
411
+ value: value
412
+ });
413
+ populatedParamsIndex++;
414
+ }
415
+ // truncate tokenParams array to populatedParamsIndex via assembly
416
+ assembly {
417
+ // directly modify the memory array length in memory
418
+ mstore(tokenParams, populatedParamsIndex)
419
+ }
420
+
421
+ // call augmentation hook if configured for the project
422
+ if (address(projectConfig.tokenPMPReadAugmentationHook) != address(0)) {
423
+ // assign return value to the augmented tokenParams
424
+ // @dev executed in read-only context, artist-configured hook contract (not entirely arbitrary)
425
+ tokenParams = projectConfig
426
+ .tokenPMPReadAugmentationHook
427
+ .onTokenPMPReadAugmentation({
428
+ coreContract: coreContract,
429
+ tokenId: tokenId,
430
+ tokenParams: tokenParams
431
+ });
432
+ }
433
+
434
+ // @dev implicitly returns tokenParams
435
+ }
436
+
437
+ // ---- introspection view functions ----
438
+
439
+ /**
440
+ * @notice Get the project config for a given project.
441
+ * @param coreContract The address of the core contract to call.
442
+ * @param projectId The projectId of the project to get data for.
443
+ * @return pmpKeys The configured pmpKeys for the project.
444
+ * @return configNonce The config nonce for the project.
445
+ * @return tokenPMPPostConfigHook The tokenPMPPostConfigHook for the project.
446
+ * @return tokenPMPReadAugmentationHook The tokenPMPReadAugmentationHook for the project.
447
+ */
448
+ function getProjectConfig(
449
+ address coreContract,
450
+ uint256 projectId
451
+ )
452
+ external
453
+ view
454
+ returns (
455
+ string[] memory pmpKeys,
456
+ uint8 configNonce,
457
+ IPMPConfigureHook tokenPMPPostConfigHook,
458
+ IPMPAugmentHook tokenPMPReadAugmentationHook
459
+ )
460
+ {
461
+ ProjectConfig storage projectConfig = projectConfigs[coreContract][
462
+ projectId
463
+ ];
464
+ // @dev edge case - uninitialized pmpKeys - empty array returned by getAll()
465
+ pmpKeys = projectConfig.pmpKeys.getAll();
466
+ configNonce = projectConfig.configNonce;
467
+ tokenPMPPostConfigHook = projectConfig.tokenPMPPostConfigHook;
468
+ tokenPMPReadAugmentationHook = projectConfig
469
+ .tokenPMPReadAugmentationHook;
470
+ }
471
+
472
+ /**
473
+ * @notice Checks if the given wallet has the owner role for the given token.
474
+ * It returns true if the wallet is the owner of the token or if the wallet
475
+ * is a delegate of the token owner; otherwise it returns false.
476
+ * Reverts if an invalid coreContract or tokenId is provided.
477
+ * Provided for convenience, as the same check is performed in the
478
+ * configureTokenParams function.
479
+ * @param wallet The wallet address to check.
480
+ * @param coreContract The address of the core contract to call.
481
+ * @param tokenId The tokenId of the token to check.
482
+ * @return isTokenOwnerOrDelegate True if the wallet is the owner or a delegate of the token,
483
+ * false otherwise.
484
+ */
485
+ function isTokenOwnerOrDelegate(
486
+ address wallet,
487
+ address coreContract,
488
+ uint256 tokenId
489
+ ) external view returns (bool) {
490
+ return
491
+ _isTokenOwnerOrDelegate({
492
+ tokenId: tokenId,
493
+ coreContract: coreContract,
494
+ sender: wallet
495
+ });
496
+ }
497
+
498
+ /**
499
+ * @notice Get the PMP config from storage for a given project and pmpKey.
500
+ * @dev Returns the storage values, even if unconfigured or not part of the
501
+ * active project config. Check latestConfigNonce to verify if the pmpKey
502
+ * is part of the active project config.
503
+ * @dev any populated select options are loaded and returned as a string array
504
+ * @param coreContract The address of the core contract to call.
505
+ * @param projectId The projectId of the project to get data for.
506
+ * @param pmpKey The pmpKey of the pmp to get data for.
507
+ * @return pmpConfigView The PMP config for the given project and pmpKey.
508
+ */
509
+ function getProjectPMPConfig(
510
+ address coreContract,
511
+ uint256 projectId,
512
+ string memory pmpKey
513
+ ) external view returns (PMPConfigView memory pmpConfigView) {
514
+ PMPConfigStorage storage pmpConfigStorage = projectConfigs[
515
+ coreContract
516
+ ][projectId].pmpConfigsStorage[_getStringHash(pmpKey)];
517
+ // load values from storage
518
+ pmpConfigView.highestConfigNonce = pmpConfigStorage.highestConfigNonce;
519
+ pmpConfigView.authOption = pmpConfigStorage.authOption;
520
+ pmpConfigView.paramType = pmpConfigStorage.paramType;
521
+ pmpConfigView.pmpLockedAfterTimestamp = pmpConfigStorage
522
+ .pmpLockedAfterTimestamp;
523
+ pmpConfigView.authAddress = pmpConfigStorage.authAddress;
524
+ pmpConfigView.selectOptionsLength = pmpConfigStorage
525
+ .selectOptionsLength;
526
+ pmpConfigView.selectOptions = pmpConfigStorage.selectOptions.getAll();
527
+ pmpConfigView.minRange = pmpConfigStorage.minRange;
528
+ pmpConfigView.maxRange = pmpConfigStorage.maxRange;
529
+ }
530
+
531
+ /**
532
+ * @notice Get the PMP storage for a given token and pmpKey.
533
+ * Returns the PMP storage struct for the queried token and pmpKey. The storage
534
+ * may be stale if the token has been reconfigured, or empty if never configured.
535
+ * @param coreContract The address of the core contract to call.
536
+ * @param tokenId The tokenId of the token to get data for.
537
+ * @param pmpKey The pmpKey of the pmp to get data for.
538
+ * @return pmp The PMP storage for the given token and pmpKey.
539
+ */
540
+ function getTokenPMPStorage(
541
+ address coreContract,
542
+ uint256 tokenId,
543
+ string memory pmpKey
544
+ ) external view returns (PMPStorage memory pmp) {
545
+ pmp = tokenPMPs[coreContract][tokenId][_getStringHash(pmpKey)];
546
+ }
547
+
548
+ /**
549
+ * @notice Synchronizes the project's parameter keys with the provided input keys.
550
+ * @dev Only updates the storage if the keys have changed to optimize gas usage.
551
+ * @param inputKeys The new parameter keys to store.
552
+ * @param projectConfig The project configuration storage reference.
553
+ */
554
+ function _syncPMPKeys(
555
+ string[] memory inputKeys,
556
+ ProjectConfig storage projectConfig
557
+ ) internal {
558
+ string[] memory currentKeys = projectConfig.pmpKeys.getAll();
559
+ // determine if inputKeys are different from currentKeys to avoid expensive operation
560
+ // @dev in general, we don't expect many changes to pmpKeys after initial configuration
561
+ bool keysChanged = false;
562
+ if (inputKeys.length != currentKeys.length) {
563
+ keysChanged = true;
564
+ } else {
565
+ for (uint256 i = 0; i < inputKeys.length; i++) {
566
+ if (!inputKeys[i].equal(currentKeys[i])) {
567
+ keysChanged = true;
568
+ break;
569
+ }
570
+ }
571
+ }
572
+ // if keys changed, update projectConfig's pmpKeys (expensive operation)
573
+ if (keysChanged) {
574
+ ImmutableStringArray.store(projectConfig.pmpKeys, inputKeys);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * @notice Validates a PMP configuration. Ensures arbitrary input is valid and intentional.
580
+ * @dev Verifies parameter types, authorization options, and constraints.
581
+ * @param pmpConfig The PMP configuration to validate.
582
+ */
583
+ function _validatePMPConfig(PMPConfig calldata pmpConfig) internal view {
584
+ // memoize paramType and authOption
585
+ ParamType paramType = pmpConfig.paramType;
586
+ AuthOption authOption = pmpConfig.authOption;
587
+ // require type is not unconfigured
588
+ require(
589
+ paramType != ParamType.Unconfigured,
590
+ "PMP: paramType is unconfigured"
591
+ );
592
+ // validate locked after timestamp is in the future, or is zero (unlimited)
593
+ require(
594
+ pmpConfig.pmpLockedAfterTimestamp == 0 ||
595
+ pmpConfig.pmpLockedAfterTimestamp > block.timestamp,
596
+ "PMP: pmpLockedAfterTimestamp is in the past and not unlimited (zero)"
597
+ );
598
+ // validate enums are within bounds
599
+ // @dev no coverage else branch, this is redundant as used due to solidity compiler checks on function enum inputs
600
+ require(
601
+ authOption <= AuthOption.ArtistAndTokenOwnerAndAddress,
602
+ "PMP: Invalid authOption"
603
+ );
604
+ // @dev no coverage else branch, this is redundant as used due to solidity compiler checks on function enum inputs
605
+ require(paramType <= ParamType.String, "PMP: Invalid paramType");
606
+ // only artist+ authentication types are supported for string params
607
+ if (paramType == ParamType.String) {
608
+ require(
609
+ authOption == AuthOption.Artist ||
610
+ authOption == AuthOption.ArtistAndTokenOwner ||
611
+ authOption == AuthOption.ArtistAndTokenOwnerAndAddress ||
612
+ authOption == AuthOption.ArtistAndAddress,
613
+ "PMP: String params must have artist+ authentication"
614
+ );
615
+ }
616
+ // validate auth with address has non-zero auth address
617
+ if (
618
+ authOption == AuthOption.Address ||
619
+ authOption == AuthOption.TokenOwnerAndAddress ||
620
+ authOption == AuthOption.ArtistAndAddress ||
621
+ authOption == AuthOption.ArtistAndTokenOwnerAndAddress
622
+ ) {
623
+ require(
624
+ pmpConfig.authAddress != address(0),
625
+ "PMP: authAddress is zero"
626
+ );
627
+ } else {
628
+ // auth address must be zero for any non-address auth option
629
+ require(
630
+ pmpConfig.authAddress == address(0),
631
+ "PMP: authAddress is not zero"
632
+ );
633
+ }
634
+ // validate appropriate fields are empty
635
+ if (
636
+ paramType == ParamType.Bool ||
637
+ paramType == ParamType.String ||
638
+ paramType == ParamType.HexColor
639
+ ) {
640
+ // @dev should have all fields empty
641
+ require(
642
+ pmpConfig.selectOptions.length == 0,
643
+ "PMP: selectOptions is not empty"
644
+ );
645
+ // @dev min/max range for hex color checked during assignment, should be empty in config
646
+ require(pmpConfig.minRange == 0, "PMP: minRange is not empty");
647
+ require(pmpConfig.maxRange == 0, "PMP: maxRange is not empty");
648
+ } else if (paramType == ParamType.Select) {
649
+ // @dev select should have selectOptions and empty min/max range values
650
+ require(
651
+ pmpConfig.selectOptions.length > 0,
652
+ "PMP: selectOptions is empty"
653
+ );
654
+ require(
655
+ pmpConfig.selectOptions.length < 256,
656
+ "PMP: selectOptions length > 255"
657
+ );
658
+ // @dev do not check if options are unique on-chain
659
+ // require min/max range values are empty
660
+ require(pmpConfig.minRange == 0, "PMP: minRange is not empty");
661
+ require(pmpConfig.maxRange == 0, "PMP: maxRange is not empty");
662
+ } else if (
663
+ paramType == ParamType.Uint256Range ||
664
+ paramType == ParamType.Int256Range ||
665
+ paramType == ParamType.DecimalRange ||
666
+ paramType == ParamType.Timestamp
667
+ ) {
668
+ // @dev range params should have empty selectOptions
669
+ require(
670
+ pmpConfig.selectOptions.length == 0,
671
+ "PMP: selectOptions is not empty"
672
+ );
673
+ // require minRange is less than maxRange
674
+ if (paramType == ParamType.Int256Range) {
675
+ // cast minRange and maxRange to int256
676
+ int256 minRange = int256(uint256(pmpConfig.minRange));
677
+ int256 maxRange = int256(uint256(pmpConfig.maxRange));
678
+ require(minRange < maxRange, "PMP: minRange >= maxRange");
679
+ } else {
680
+ // cast minRange and maxRange to uint256
681
+ // @dev lt works for uint256, decimal, or timestamp types cast as uint256
682
+ uint256 minRange = uint256(pmpConfig.minRange);
683
+ uint256 maxRange = uint256(pmpConfig.maxRange);
684
+ require(minRange < maxRange, "PMP: minRange >= maxRange");
685
+ // additional guardrails on timestamp range
686
+ if (paramType == ParamType.Timestamp) {
687
+ // require maxRange is not gt _TIMESTAMP_MAX
688
+ require(
689
+ maxRange <= _TIMESTAMP_MAX,
690
+ "PMP: maxRange > _TIMESTAMP_MAX"
691
+ );
692
+ }
693
+ }
694
+ } else {
695
+ // @dev should never reach, no coverage
696
+ revert("PMP: Invalid paramType");
697
+ }
698
+ }
699
+
700
+ /**
701
+ * @notice Validates a PMP input against the project's configuration.
702
+ * Includes auth checks based on the pmpKey's current authOption.
703
+ * @dev Checks authorization, parameter type consistency, and value constraints.
704
+ * @dev Checks that pmp param is included in most recently configured PMP config for token's project.
705
+ * @param tokenId The token ID for which the parameter is being configured.
706
+ * @param coreContract The address of the core contract.
707
+ * @param pmpInput The parameter input to validate.
708
+ * @param pmpConfigStorage The project's configuration storage for this parameter.
709
+ * @param projectConfigNonce The project's current configuration nonce.
710
+ */
711
+ function _validatePMPInputAndAuth(
712
+ uint256 tokenId,
713
+ address coreContract,
714
+ PMPInput memory pmpInput,
715
+ PMPConfigStorage storage pmpConfigStorage,
716
+ uint8 projectConfigNonce
717
+ ) internal view {
718
+ // check that the param is part of the project's most recently configured PMP params
719
+ // @dev use config nonce to check if param is part of most recently configured PMP params
720
+ require(
721
+ pmpConfigStorage.highestConfigNonce == projectConfigNonce,
722
+ "PMP: param not part of most recently configured PMP params"
723
+ );
724
+ // check that the param type matches and is not unconfigured
725
+ require(
726
+ pmpInput.configuredParamType == pmpConfigStorage.paramType,
727
+ "PMP: paramType mismatch"
728
+ );
729
+ // @dev checking value in memory for lower gas cost
730
+ require(
731
+ pmpInput.configuredParamType != ParamType.Unconfigured,
732
+ "PMP: input paramType is unconfigured"
733
+ );
734
+
735
+ // ensure caller has appropriate auth
736
+ {
737
+ // @dev block scope to reduce stack depth
738
+ AuthOption authOption = pmpConfigStorage.authOption;
739
+ if (authOption == AuthOption.Artist) {
740
+ require(
741
+ _isArtist({
742
+ tokenId: tokenId,
743
+ coreContract: coreContract,
744
+ sender: msg.sender
745
+ }),
746
+ "PMP: artist auth required"
747
+ );
748
+ } else if (authOption == AuthOption.TokenOwner) {
749
+ require(
750
+ _isTokenOwnerOrDelegate({
751
+ tokenId: tokenId,
752
+ coreContract: coreContract,
753
+ sender: msg.sender
754
+ }),
755
+ "PMP: token owner auth required"
756
+ );
757
+ } else if (authOption == AuthOption.ArtistAndTokenOwner) {
758
+ // @dev check token owner or delegate last to enable pre-mint configuration by non-token-owner
759
+ require(
760
+ _isArtist({
761
+ tokenId: tokenId,
762
+ coreContract: coreContract,
763
+ sender: msg.sender
764
+ }) ||
765
+ _isTokenOwnerOrDelegate({
766
+ tokenId: tokenId,
767
+ coreContract: coreContract,
768
+ sender: msg.sender
769
+ }),
770
+ "PMP: artist and token owner auth required"
771
+ );
772
+ } else if (authOption == AuthOption.Address) {
773
+ require(
774
+ msg.sender == pmpConfigStorage.authAddress,
775
+ "PMP: address auth required"
776
+ );
777
+ } else if (authOption == AuthOption.ArtistAndTokenOwnerAndAddress) {
778
+ // @dev check token owner or delegate last to enable pre-mint configuration by non-token-owner
779
+ require(
780
+ _isArtist({
781
+ tokenId: tokenId,
782
+ coreContract: coreContract,
783
+ sender: msg.sender
784
+ }) ||
785
+ msg.sender == pmpConfigStorage.authAddress ||
786
+ _isTokenOwnerOrDelegate({
787
+ tokenId: tokenId,
788
+ coreContract: coreContract,
789
+ sender: msg.sender
790
+ }),
791
+ "PMP: artist and token owner and address auth required"
792
+ );
793
+ } else if (authOption == AuthOption.ArtistAndAddress) {
794
+ require(
795
+ _isArtist({
796
+ tokenId: tokenId,
797
+ coreContract: coreContract,
798
+ sender: msg.sender
799
+ }) || msg.sender == pmpConfigStorage.authAddress,
800
+ "PMP: artist and address auth required"
801
+ );
802
+ } else if (authOption == AuthOption.TokenOwnerAndAddress) {
803
+ // @dev check token owner or delegate last to enable pre-mint configuration by non-token-owner
804
+ require(
805
+ msg.sender == pmpConfigStorage.authAddress ||
806
+ _isTokenOwnerOrDelegate({
807
+ tokenId: tokenId,
808
+ coreContract: coreContract,
809
+ sender: msg.sender
810
+ }),
811
+ "PMP: token owner and address auth required"
812
+ );
813
+ } else {
814
+ // @dev no coverage, this should never be reached
815
+ revert("PMP: invalid authOption");
816
+ }
817
+ }
818
+
819
+ // ensure properly configured value
820
+ ParamType paramType = pmpConfigStorage.paramType;
821
+ // range checks for non-string params
822
+ if (paramType != ParamType.String) {
823
+ if (paramType == ParamType.Select) {
824
+ require(
825
+ uint256(pmpInput.configuredValue) <
826
+ pmpConfigStorage.selectOptionsLength,
827
+ "PMP: selectOptions index out of bounds"
828
+ );
829
+ } else if (paramType == ParamType.Bool) {
830
+ require(
831
+ pmpInput.configuredValue == bytes32(0) ||
832
+ uint256(pmpInput.configuredValue) == 1,
833
+ "PMP: bool param value must be 0 or 1"
834
+ );
835
+ } else if (
836
+ paramType == ParamType.Uint256Range ||
837
+ paramType == ParamType.DecimalRange
838
+ ) {
839
+ require(
840
+ pmpInput.configuredValue >= pmpConfigStorage.minRange &&
841
+ pmpInput.configuredValue <= pmpConfigStorage.maxRange,
842
+ "PMP: param value out of bounds"
843
+ );
844
+ } else if (paramType == ParamType.Int256Range) {
845
+ require(
846
+ int256(uint256(pmpInput.configuredValue)) >= // @dev ensure this converts as expected
847
+ int256(uint256(pmpConfigStorage.minRange)) &&
848
+ int256(uint256(pmpInput.configuredValue)) <=
849
+ int256(uint256(pmpConfigStorage.maxRange)),
850
+ "PMP: param value out of bounds"
851
+ );
852
+ } else if (paramType == ParamType.Timestamp) {
853
+ require(
854
+ uint256(pmpInput.configuredValue) <= _TIMESTAMP_MAX &&
855
+ uint256(pmpInput.configuredValue) >= _TIMESTAMP_MIN, // @dev no coverage, this is redundant due to being assigned zero
856
+ "PMP: param value out of bounds"
857
+ );
858
+ } else if (paramType == ParamType.HexColor) {
859
+ require(
860
+ uint256(pmpInput.configuredValue) <= _HEX_COLOR_MAX, // @dev minimum hex color of zero implicitly passed by using uint256
861
+ "PMP: invalid hex color"
862
+ );
863
+ } else {
864
+ // @dev no coverage, this should never be reached
865
+ revert("PMP: invalid paramType");
866
+ }
867
+ }
868
+
869
+ // string and non-string checks
870
+ if (pmpConfigStorage.paramType == ParamType.String) {
871
+ require(
872
+ pmpInput.configuredValue == bytes32(0),
873
+ "PMP: value must be empty for string params"
874
+ );
875
+ // require artist is caller if configuring artist string
876
+ if (pmpInput.configuringArtistString) {
877
+ // require artist is caller
878
+ require(
879
+ _isArtist({
880
+ tokenId: tokenId,
881
+ coreContract: coreContract,
882
+ sender: msg.sender
883
+ }),
884
+ "PMP: artist auth required to configure artist string"
885
+ );
886
+ }
887
+ } else {
888
+ // non-string - ensure configured string is empty
889
+ require(
890
+ bytes(pmpInput.configuredValueString).length == 0,
891
+ "PMP: non-string param must have empty string value"
892
+ );
893
+ // non-string - ensure configuring artist string is false, because it is not relevant for non-string params
894
+ require(
895
+ !pmpInput.configuringArtistString,
896
+ "PMP: artist string cannot be configured for non-string params"
897
+ );
898
+ }
899
+ }
900
+
901
+ /**
902
+ * @notice Gets the value of a PMP as a formatted string.
903
+ * @dev Used internally to retrieve a PMP value in string format for external consumption.
904
+ * @param pmpConfigStorage The config storage for the PMP.
905
+ * @param pmp The PMP storage instance.
906
+ * @return isConfigured Whether the PMP has been configured for the token.
907
+ * @return value The string representation of the PMP value, or empty if not configured.
908
+ */
909
+ function _getPMPValue(
910
+ PMPConfigStorage storage pmpConfigStorage,
911
+ PMPStorage storage pmp
912
+ ) internal view returns (bool isConfigured, string memory value) {
913
+ ParamType configuredParamType = pmp.configuredParamType;
914
+ // unconfigured param for token
915
+ if (
916
+ configuredParamType == ParamType.Unconfigured || // unconfigured for token
917
+ configuredParamType != pmpConfigStorage.paramType // stale - token configured param type is different from project config
918
+ ) {
919
+ return (false, "");
920
+ }
921
+ // if string, return value
922
+ if (pmpConfigStorage.paramType == ParamType.String) {
923
+ // return artist configured value if present
924
+ if (bytes(pmp.artistConfiguredValueString).length > 0) {
925
+ return (true, pmp.artistConfiguredValueString);
926
+ }
927
+ // return non-artist configured value if present
928
+ if (bytes(pmp.nonArtistConfiguredValueString).length > 0) {
929
+ return (true, pmp.nonArtistConfiguredValueString);
930
+ }
931
+ // empty string is considered not configured
932
+ return (false, "");
933
+ }
934
+ if (configuredParamType == ParamType.Select) {
935
+ // return unconfigured if index is out of bounds (obviously stale)
936
+ if (
937
+ uint256(pmp.configuredValue) >=
938
+ pmpConfigStorage.selectOptionsLength
939
+ ) {
940
+ return (false, "");
941
+ }
942
+ return (
943
+ true,
944
+ pmpConfigStorage.selectOptions.get(uint256(pmp.configuredValue))
945
+ );
946
+ }
947
+ if (configuredParamType == ParamType.Bool) {
948
+ return (true, pmp.configuredValue == bytes32(0) ? "false" : "true");
949
+ }
950
+ if (
951
+ configuredParamType == ParamType.Uint256Range ||
952
+ configuredParamType == ParamType.HexColor ||
953
+ configuredParamType == ParamType.Timestamp ||
954
+ configuredParamType == ParamType.DecimalRange
955
+ ) {
956
+ // verify configured value is within bounds (obviously stale if not)
957
+ uint256 configuredValue = uint256(pmp.configuredValue);
958
+ uint256 maxRange = configuredParamType == ParamType.HexColor
959
+ ? _HEX_COLOR_MAX
960
+ : uint256(pmpConfigStorage.maxRange);
961
+ uint256 minRange = configuredParamType == ParamType.HexColor
962
+ ? 0
963
+ : uint256(pmpConfigStorage.minRange);
964
+ if (
965
+ configuredValue < uint256(minRange) ||
966
+ configuredValue > uint256(maxRange)
967
+ ) {
968
+ return (false, "");
969
+ }
970
+ // handle decimal case
971
+ if (configuredParamType == ParamType.DecimalRange) {
972
+ return (true, _uintToDecimalString(configuredValue));
973
+ }
974
+ // handle hex color case
975
+ if (configuredParamType == ParamType.HexColor) {
976
+ return (true, _uintToHexColorString(configuredValue));
977
+ }
978
+ // handle other cases - uint256 and timestamp
979
+ return (true, configuredValue.toString());
980
+ }
981
+ if (configuredParamType == ParamType.Int256Range) {
982
+ // verify configured value is within bounds (obviously stale if not)
983
+ int256 configuredValue = int256(uint256(pmp.configuredValue));
984
+ if (
985
+ configuredValue < int256(uint256(pmpConfigStorage.minRange)) ||
986
+ configuredValue > int256(uint256(pmpConfigStorage.maxRange))
987
+ ) {
988
+ return (false, "");
989
+ }
990
+ return (true, configuredValue.toStringSigned());
991
+ }
992
+ // @dev should never reach
993
+ // @dev no coverage, unreachable
994
+ revert("PMP: Unhandled ParamType");
995
+ }
996
+
997
+ /**
998
+ * @notice Enforces that the caller is the artist of the specified project.
999
+ * @dev Reverts if the caller is not the artist.
1000
+ * @dev Assumes Art Blocks V3 Core Contract interface.
1001
+ * @param coreContract The address of the core contract.
1002
+ * @param projectId The project ID to check artist permissions for.
1003
+ * @param sender The address to check if it's the artist.
1004
+ */
1005
+ function _onlyArtist(
1006
+ address coreContract,
1007
+ uint256 projectId,
1008
+ address sender
1009
+ ) internal view {
1010
+ require(
1011
+ IGenArt721CoreContractV3_Base(coreContract)
1012
+ .projectIdToArtistAddress(projectId) == sender,
1013
+ "PMP: only artist"
1014
+ );
1015
+ }
1016
+
1017
+ /**
1018
+ * @notice Checks if an address is the artist of the project associated with a token.
1019
+ * @dev Assumes Art Blocks V3 Core Contract interface.
1020
+ * @param tokenId The token ID to get the project ID from.
1021
+ * @param coreContract The address of the core contract.
1022
+ * @param sender The address to check if it's the artist.
1023
+ * @return Returns true if the sender is the artist, false otherwise.
1024
+ */
1025
+ function _isArtist(
1026
+ uint256 tokenId,
1027
+ address coreContract,
1028
+ address sender
1029
+ ) internal view returns (bool) {
1030
+ uint256 projectId = ABHelpers.tokenIdToProjectId(tokenId);
1031
+ return
1032
+ IGenArt721CoreContractV3_Base(coreContract)
1033
+ .projectIdToArtistAddress(projectId) == sender;
1034
+ }
1035
+
1036
+ /**
1037
+ * @notice Checks if an address is the owner of a token.
1038
+ * Supports ERC-721 Token-level wallet delegation for the TokenOwner role via delegate.xyz v2,
1039
+ * using the delegate.xyz v2 "postmintparameters" subdelegation rights.
1040
+ * @dev Assumes Art Blocks V3 Core Contract interface.
1041
+ * @param tokenId The token ID to check ownership for.
1042
+ * @param coreContract The address of the core contract.
1043
+ * @param sender The address to check if it's the token owner.
1044
+ * @return Returns true if the sender is the token owner, false otherwise.
1045
+ * @dev Always execute within a nonReentrant context.
1046
+ */
1047
+ function _isTokenOwnerOrDelegate(
1048
+ uint256 tokenId,
1049
+ address coreContract,
1050
+ address sender
1051
+ ) internal view returns (bool) {
1052
+ // @dev leading interaction - only execute within a nonReentrant context
1053
+ address tokenOwner = IERC721(coreContract).ownerOf(tokenId);
1054
+ if (tokenOwner == sender) {
1055
+ return true;
1056
+ }
1057
+ // @dev delegate.xyz v2 support
1058
+ return
1059
+ delegateRegistry.checkDelegateForERC721({
1060
+ to: sender, // hot wallet
1061
+ from: tokenOwner, // vault
1062
+ contract_: coreContract, // ERC-721 contract
1063
+ tokenId: tokenId, // tokenId
1064
+ rights: bytes32(DELEGATION_REGISTRY_TOKEN_OWNER_RIGHTS) // opt-in granular control of rights
1065
+ });
1066
+ }
1067
+
1068
+ /**
1069
+ * @notice Computes the keccak256 hash of a string.
1070
+ * @dev Used to create mapping keys from string values.
1071
+ * @param str The string to hash.
1072
+ * @return The bytes32 hash of the input string.
1073
+ */
1074
+ function _getStringHash(string memory str) internal pure returns (bytes32) {
1075
+ return keccak256(abi.encode(str));
1076
+ }
1077
+
1078
+ /**
1079
+ * @notice Converts a uint256 to a decimal string representation.
1080
+ * @dev Converts integer and fractional parts separately and combines them.
1081
+ * @param number The uint256 value to convert to decimal string.
1082
+ * @return A string representation of the decimal number.
1083
+ */
1084
+ function _uintToDecimalString(
1085
+ uint256 number
1086
+ ) private pure returns (string memory) {
1087
+ uint256 integerPart = number / _DECIMAL_PRECISION; // Integer part
1088
+ uint256 fractionalPart = number % _DECIMAL_PRECISION; // Fractional part
1089
+
1090
+ // Convert integer and fractional parts to strings
1091
+ string memory intStr = integerPart.toString();
1092
+ string memory fracStr = fractionalPart.toString();
1093
+
1094
+ // Pad fractional part with zeros if necessary
1095
+ while (bytes(fracStr).length < _DECIMAL_PRECISION_DIGITS) {
1096
+ fracStr = string(abi.encodePacked("0", fracStr));
1097
+ }
1098
+
1099
+ // Combine integer and fractional parts with a decimal point
1100
+ return string(abi.encodePacked(intStr, ".", fracStr));
1101
+ }
1102
+
1103
+ /**
1104
+ * @notice Converts a uint256 to a hex color string.
1105
+ * @dev forked from OpenZeppelin's Strings library to use # prefix
1106
+ * @dev Assumes the value is a valid hex color.
1107
+ * @param value The uint256 value to convert to hex color string.
1108
+ * @return A string representation of the hex color.
1109
+ */
1110
+ function _uintToHexColorString(
1111
+ uint256 value
1112
+ ) private pure returns (string memory) {
1113
+ uint256 localValue = value;
1114
+ bytes memory buffer = new bytes(2 * 3 + 1);
1115
+ buffer[0] = "#";
1116
+ for (uint256 i = 2 * 3; i > 0; --i) {
1117
+ buffer[i] = _HEX_DIGITS[localValue & 0xf];
1118
+ localValue >>= 4;
1119
+ }
1120
+ if (localValue != 0) {
1121
+ // @dev no coverage, this is redundant due to use of max hex color value
1122
+ revert("PMP: invalid hex color");
1123
+ }
1124
+ return string(buffer);
1125
+ }
1126
+ }