@bananapus/721-hook-v6 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.gas-snapshot +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +253 -0
  4. package/SKILLS.md +140 -0
  5. package/docs/book.css +13 -0
  6. package/docs/book.toml +12 -0
  7. package/docs/solidity.min.js +74 -0
  8. package/docs/src/README.md +253 -0
  9. package/docs/src/SUMMARY.md +38 -0
  10. package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +645 -0
  11. package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +99 -0
  12. package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +288 -0
  13. package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +1096 -0
  14. package/docs/src/src/README.md +11 -0
  15. package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +430 -0
  16. package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +309 -0
  17. package/docs/src/src/abstract/README.md +5 -0
  18. package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +29 -0
  19. package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +203 -0
  20. package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +25 -0
  21. package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +64 -0
  22. package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +265 -0
  23. package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +12 -0
  24. package/docs/src/src/interfaces/README.md +9 -0
  25. package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +14 -0
  26. package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +68 -0
  27. package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +82 -0
  28. package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +61 -0
  29. package/docs/src/src/libraries/README.md +7 -0
  30. package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +27 -0
  31. package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +59 -0
  32. package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +60 -0
  33. package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +26 -0
  34. package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +16 -0
  35. package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +20 -0
  36. package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +16 -0
  37. package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +19 -0
  38. package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +34 -0
  39. package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +23 -0
  40. package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +22 -0
  41. package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +51 -0
  42. package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +66 -0
  43. package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +21 -0
  44. package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +42 -0
  45. package/docs/src/src/structs/README.md +18 -0
  46. package/foundry.lock +11 -0
  47. package/foundry.toml +22 -0
  48. package/package.json +31 -0
  49. package/remappings.txt +1 -0
  50. package/script/Deploy.s.sol +140 -0
  51. package/script/helpers/Hook721DeploymentLib.sol +81 -0
  52. package/slither-ci.config.json +10 -0
  53. package/sphinx.lock +476 -0
  54. package/src/JB721TiersHook.sol +765 -0
  55. package/src/JB721TiersHookDeployer.sol +114 -0
  56. package/src/JB721TiersHookProjectDeployer.sol +413 -0
  57. package/src/JB721TiersHookStore.sol +1195 -0
  58. package/src/abstract/ERC721.sol +484 -0
  59. package/src/abstract/JB721Hook.sol +279 -0
  60. package/src/interfaces/IJB721Hook.sol +21 -0
  61. package/src/interfaces/IJB721TiersHook.sol +135 -0
  62. package/src/interfaces/IJB721TiersHookDeployer.sol +22 -0
  63. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +76 -0
  64. package/src/interfaces/IJB721TiersHookStore.sol +220 -0
  65. package/src/interfaces/IJB721TokenUriResolver.sol +10 -0
  66. package/src/libraries/JB721Constants.sol +7 -0
  67. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +44 -0
  68. package/src/libraries/JBBitmap.sol +57 -0
  69. package/src/libraries/JBIpfsDecoder.sol +95 -0
  70. package/src/structs/JB721InitTiersConfig.sol +20 -0
  71. package/src/structs/JB721Tier.sol +39 -0
  72. package/src/structs/JB721TierConfig.sol +40 -0
  73. package/src/structs/JB721TiersHookFlags.sol +17 -0
  74. package/src/structs/JB721TiersMintReservesConfig.sol +9 -0
  75. package/src/structs/JB721TiersRulesetMetadata.sol +12 -0
  76. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +9 -0
  77. package/src/structs/JBBitmapWord.sol +11 -0
  78. package/src/structs/JBDeploy721TiersHookConfig.sol +25 -0
  79. package/src/structs/JBLaunchProjectConfig.sol +18 -0
  80. package/src/structs/JBLaunchRulesetsConfig.sol +17 -0
  81. package/src/structs/JBPayDataHookRulesetConfig.sol +44 -0
  82. package/src/structs/JBPayDataHookRulesetMetadata.sol +46 -0
  83. package/src/structs/JBQueueRulesetsConfig.sol +13 -0
  84. package/src/structs/JBStored721Tier.sol +24 -0
  85. package/test/721HookAttacks.t.sol +396 -0
  86. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +944 -0
  87. package/test/invariants/TierLifecycleInvariant.t.sol +187 -0
  88. package/test/invariants/TieredHookStoreInvariant.t.sol +81 -0
  89. package/test/invariants/handlers/TierLifecycleHandler.sol +262 -0
  90. package/test/invariants/handlers/TierStoreHandler.sol +155 -0
  91. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +141 -0
  92. package/test/unit/JBBitmap.t.sol +169 -0
  93. package/test/unit/JBIpfsDecoder.t.sol +131 -0
  94. package/test/unit/M6_TierSupplyCheck.t.sol +220 -0
  95. package/test/unit/adjustTier_Unit.t.sol +1740 -0
  96. package/test/unit/deployer_Unit.t.sol +103 -0
  97. package/test/unit/getters_constructor_Unit.t.sol +548 -0
  98. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +443 -0
  99. package/test/unit/pay_Unit.t.sol +1537 -0
  100. package/test/unit/redeem_Unit.t.sol +459 -0
@@ -0,0 +1,765 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
6
+ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
7
+ import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
8
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
9
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
10
+ import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
11
+ import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
12
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
13
+ import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
14
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
15
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
16
+ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
17
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
18
+ import {mulDiv} from "@prb/math/src/Common.sol";
19
+
20
+ import {JB721Hook} from "./abstract/JB721Hook.sol";
21
+ import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
22
+ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
23
+ import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
24
+ import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
25
+ import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol";
26
+ import {JB721Tier} from "./structs/JB721Tier.sol";
27
+ import {JB721TierConfig} from "./structs/JB721TierConfig.sol";
28
+ import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscountPercentConfig.sol";
29
+ import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
30
+ import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol";
31
+ import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConfig.sol";
32
+
33
+ /// @title JB721TiersHook
34
+ /// @notice A Juicebox project can use this hook to sell tiered ERC-721 NFTs with different prices and metadata. When
35
+ /// the project is paid, the hook may mint NFTs to the payer, depending on the hook's setup, the amount paid, and
36
+ /// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
37
+ /// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
38
+ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
39
+ //*********************************************************************//
40
+ // --------------------------- custom errors ------------------------- //
41
+ //*********************************************************************//
42
+
43
+ error JB721TiersHook_AlreadyInitialized(uint256 projectId);
44
+ error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
45
+ error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
46
+ error JB721TiersHook_MintReserveNftsPaused();
47
+ error JB721TiersHook_NoProjectId();
48
+ error JB721TiersHook_Overspending(uint256 leftoverAmount);
49
+ error JB721TiersHook_TierTransfersPaused();
50
+
51
+ //*********************************************************************//
52
+ // --------------- public immutable stored properties ---------------- //
53
+ //*********************************************************************//
54
+
55
+ /// @notice The contract storing and managing project rulesets.
56
+ IJBRulesets public immutable override RULESETS;
57
+
58
+ /// @notice The contract that stores and manages data for this contract's NFTs.
59
+ IJB721TiersHookStore public immutable override STORE;
60
+
61
+ //*********************************************************************//
62
+ // ---------------------- public stored properties ------------------- //
63
+ //*********************************************************************//
64
+ /// @notice The base URI for the NFT `tokenUris`.
65
+ string public override baseURI;
66
+
67
+ /// @notice This contract's metadata URI.
68
+ string public override contractURI;
69
+
70
+ /// @notice If an address pays more than the price of the NFT they received, the extra amount is stored as credits
71
+ /// which can be cashed out to mint NFTs.
72
+ /// @custom:param addr The address to get the NFT credits balance of.
73
+ /// @return The amount of credits the address has.
74
+ mapping(address addr => uint256) public override payCreditsOf;
75
+
76
+ //*********************************************************************//
77
+ // --------------------- internal stored properties ------------------ //
78
+ //*********************************************************************//
79
+
80
+ /// @notice The first owner of each token ID, stored on first transfer out.
81
+ /// @custom:param The token ID of the NFT to get the stored first owner of.
82
+ mapping(uint256 tokenId => address) internal _firstOwnerOf;
83
+
84
+ /// @notice Packed context for the pricing of this contract's tiers.
85
+ /// @dev Packed into a uint256:
86
+ /// - currency in bits 0-31 (32 bits),
87
+ /// - pricing decimals in bits 32-39 (8 bits), and
88
+ /// - prices contract in bits 40-199 (160 bits).
89
+ uint256 internal _packedPricingContext;
90
+
91
+ //*********************************************************************//
92
+ // -------------------------- constructor ---------------------------- //
93
+ //*********************************************************************//
94
+
95
+ /// @param directory A directory of terminals and controllers for projects.
96
+ /// @param permissions A contract storing permissions.
97
+ /// @param rulesets A contract storing and managing project rulesets.
98
+ /// @param store The contract which stores the NFT's data.
99
+ /// @param trustedForwarder The trusted forwarder for the ERC2771Context.
100
+ constructor(
101
+ IJBDirectory directory,
102
+ IJBPermissions permissions,
103
+ IJBRulesets rulesets,
104
+ IJB721TiersHookStore store,
105
+ address trustedForwarder
106
+ )
107
+ JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
108
+ JB721Hook(directory)
109
+ ERC2771Context(trustedForwarder)
110
+ {
111
+ RULESETS = rulesets;
112
+ STORE = store;
113
+ }
114
+
115
+ //*********************************************************************//
116
+ // ------------------------- external views -------------------------- //
117
+ //*********************************************************************//
118
+
119
+ /// @notice The first owner of an NFT.
120
+ /// @dev This is generally the address which paid for the NFT.
121
+ /// @param tokenId The token ID of the NFT to get the first owner of.
122
+ /// @return The address of the NFT's first owner.
123
+ function firstOwnerOf(uint256 tokenId) external view override returns (address) {
124
+ // Get a reference to the first owner.
125
+ address storedFirstOwner = _firstOwnerOf[tokenId];
126
+
127
+ // If the stored first owner is set, return it.
128
+ if (storedFirstOwner != address(0)) return storedFirstOwner;
129
+
130
+ // Otherwise, the first owner must be the current owner.
131
+ return _ownerOf(tokenId);
132
+ }
133
+
134
+ /// @notice Context for the pricing of this hook's tiers.
135
+ /// @dev If the `prices` contract is the zero address, this contract only accepts payments in the `currency` token.
136
+ /// @return currency The currency used for tier prices.
137
+ /// @return decimals The amount of decimals being used in tier prices.
138
+ /// @return prices The prices contract used to resolve the value of payments in currencies other than `currency`.
139
+ function pricingContext() external view override returns (uint256 currency, uint256 decimals, IJBPrices prices) {
140
+ // Get a reference to the packed pricing context.
141
+ uint256 packed = _packedPricingContext;
142
+ // currency in bits 0-31 (32 bits).
143
+ currency = uint256(uint32(packed));
144
+ // pricing decimals in bits 32-39 (8 bits).
145
+ decimals = uint256(uint8(packed >> 32));
146
+ // prices contract in bits 40-199 (160 bits).
147
+ prices = IJBPrices(address(uint160(packed >> 40)));
148
+ }
149
+
150
+ //*********************************************************************//
151
+ // -------------------------- public views --------------------------- //
152
+ //*********************************************************************//
153
+
154
+ /// @notice The total number of this hook's NFTs that an address holds (from all tiers).
155
+ /// @param owner The address to check the balance of.
156
+ /// @return balance The number of NFTs the address owns across this hook's tiers.
157
+ function balanceOf(address owner) public view override returns (uint256 balance) {
158
+ return STORE.balanceOf({hook: address(this), owner: owner});
159
+ }
160
+
161
+ /// @notice Initializes a cloned copy of the original `JB721Hook` contract.
162
+ /// @param projectId The ID of the project this this hook is associated with.
163
+ /// @param name The name of the NFT collection.
164
+ /// @param symbol The symbol representing the NFT collection.
165
+ /// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
166
+ /// @param tokenUriResolver An optional contract responsible for resolving the token URI for each NFT's token ID.
167
+ /// @param contractUri A URI where this contract's metadata can be found.
168
+ /// @param tiersConfig The NFT tiers and pricing context to initialize the hook with. The tiers must be sorted by
169
+ /// category (from least to greatest).
170
+ /// @param flags A set of additional options which dictate how the hook behaves.
171
+ function initialize(
172
+ uint256 projectId,
173
+ string memory name,
174
+ string memory symbol,
175
+ string memory baseUri,
176
+ IJB721TokenUriResolver tokenUriResolver,
177
+ string memory contractUri,
178
+ JB721InitTiersConfig memory tiersConfig,
179
+ JB721TiersHookFlags memory flags
180
+ )
181
+ public
182
+ override
183
+ {
184
+ // Stop re-initialization by ensuring a projectId is provided and doesn't already exist.
185
+ if (PROJECT_ID != 0) revert JB721TiersHook_AlreadyInitialized(PROJECT_ID);
186
+
187
+ // Make sure a projectId is provided.
188
+ if (projectId == 0) revert JB721TiersHook_NoProjectId();
189
+
190
+ // Initialize the superclass.
191
+ JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
192
+
193
+ // Validate pricing decimals are within a reasonable range.
194
+ if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
195
+
196
+ // Pack pricing context from the `tiersConfig`.
197
+ uint256 packed;
198
+ // pack the currency in bits 0-31 (32 bits).
199
+ packed |= uint256(tiersConfig.currency);
200
+ // pack the pricing decimals in bits 32-39 (8 bits).
201
+ packed |= uint256(tiersConfig.decimals) << 32;
202
+ // pack the prices contract in bits 40-199 (160 bits).
203
+ packed |= uint256(uint160(address(tiersConfig.prices))) << 40;
204
+ // Store the packed value.
205
+ // slither-disable-next-line events-maths
206
+ _packedPricingContext = packed;
207
+
208
+ // Store the base URI if provided.
209
+ if (bytes(baseUri).length != 0) baseURI = baseUri;
210
+
211
+ // Set the contract URI if provided.
212
+ if (bytes(contractUri).length != 0) contractURI = contractUri;
213
+
214
+ // Set the token URI resolver if provided.
215
+ if (tokenUriResolver != IJB721TokenUriResolver(address(0))) {
216
+ _recordSetTokenUriResolver(tokenUriResolver);
217
+ }
218
+
219
+ // Record the tiers in this hook's store.
220
+ // slither-disable-next-line unused-return
221
+ if (tiersConfig.tiers.length != 0) STORE.recordAddTiers(tiersConfig.tiers);
222
+
223
+ // Set the flags if needed.
224
+ if (
225
+ flags.noNewTiersWithReserves || flags.noNewTiersWithVotes || flags.noNewTiersWithOwnerMinting
226
+ || flags.preventOverspending
227
+ ) STORE.recordFlags(flags);
228
+
229
+ // Transfer ownership to the initializer.
230
+ _transferOwnership(_msgSender());
231
+ }
232
+
233
+ /// @notice The combined cash out weight of the NFTs with the specified token IDs.
234
+ /// @dev An NFT's cash out weight is its price.
235
+ /// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
236
+ /// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
237
+ /// @return weight The cash out weight of the tokenIds.
238
+ function cashOutWeightOf(
239
+ uint256[] memory tokenIds,
240
+ JBBeforeCashOutRecordedContext calldata
241
+ )
242
+ public
243
+ view
244
+ virtual
245
+ override
246
+ returns (uint256)
247
+ {
248
+ return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
249
+ }
250
+
251
+ /// @notice Indicates if this contract adheres to the specified interface.
252
+ /// @dev See {IERC165-supportsInterface}.
253
+ /// @param interfaceId The ID of the interface to check for adherence to.
254
+ function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
255
+ return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
256
+ }
257
+
258
+ /// @notice The metadata URI of the NFT with the specified token ID.
259
+ /// @dev Defers to the `tokenUriResolver` if it is set. Otherwise, use the `tokenUri` corresponding with the NFT's
260
+ /// tier.
261
+ /// @param tokenId The token ID of the NFT to get the metadata URI of.
262
+ /// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's
263
+ /// tier.
264
+ function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
265
+ // Get a reference to the `tokenUriResolver`.
266
+ IJB721TokenUriResolver resolver = STORE.tokenUriResolverOf(address(this));
267
+
268
+ // If a `tokenUriResolver` is set, use it to resolve the token URI.
269
+ if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: address(this), tokenId: tokenId});
270
+
271
+ // Otherwise, return the token URI corresponding with the NFT's tier.
272
+ return JBIpfsDecoder.decode({
273
+ baseUri: baseURI, hexString: STORE.encodedTierIPFSUriOf({hook: address(this), tokenId: tokenId})
274
+ });
275
+ }
276
+
277
+ /// @notice The combined cash out weight of all outstanding NFTs.
278
+ /// @dev An NFT's cash out weight is its price.
279
+ /// @return weight The total cash out weight.
280
+ function totalCashOutWeight(JBBeforeCashOutRecordedContext calldata)
281
+ public
282
+ view
283
+ virtual
284
+ override
285
+ returns (uint256)
286
+ {
287
+ return STORE.totalCashOutWeight(address(this));
288
+ }
289
+
290
+ //*********************************************************************//
291
+ // -------------------------- internal views ------------------------- //
292
+ //*********************************************************************//
293
+
294
+ /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
295
+ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
296
+ return super._contextSuffixLength();
297
+ }
298
+
299
+ /// @notice The project's current ruleset.
300
+ /// @param projectId The ID of the project to check.
301
+ /// @return The project's current ruleset.
302
+ function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
303
+ // slither-disable-next-line calls-loop
304
+ return RULESETS.currentOf(projectId);
305
+ }
306
+
307
+ /// @notice Returns the calldata, preferred to use over `msg.data`
308
+ /// @return calldata the `msg.data` of this call
309
+ function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
310
+ return ERC2771Context._msgData();
311
+ }
312
+
313
+ /// @notice Returns the sender, preferred to use over `msg.sender`
314
+ /// @return sender the sender address of this call.
315
+ function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
316
+ return ERC2771Context._msgSender();
317
+ }
318
+
319
+ //*********************************************************************//
320
+ // ---------------------- external transactions ---------------------- //
321
+ //*********************************************************************//
322
+
323
+ /// @notice Add or delete tiers.
324
+ /// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
325
+ /// tiers.
326
+ /// @dev Any added tiers must adhere to this hook's `JB721TiersHookFlags`.
327
+ /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs`.
328
+ /// @param tierIdsToRemove The tiers to remove, as an array of tier IDs.
329
+ function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
330
+ // Enforce permissions.
331
+ _requirePermissionFrom({
332
+ account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS
333
+ });
334
+
335
+ // Remove the tiers.
336
+ if (tierIdsToRemove.length != 0) {
337
+ // Emit events for each removed tier.
338
+ for (uint256 i; i < tierIdsToRemove.length; i++) {
339
+ emit RemoveTier({tierId: tierIdsToRemove[i], caller: _msgSender()});
340
+ }
341
+
342
+ // Record the removed tiers.
343
+ // slither-disable-next-line reentrancy-events
344
+ STORE.recordRemoveTierIds(tierIdsToRemove);
345
+ }
346
+
347
+ // Add the tiers.
348
+ if (tiersToAdd.length != 0) {
349
+ // Record the added tiers in the store.
350
+ uint256[] memory tierIdsAdded = STORE.recordAddTiers(tiersToAdd);
351
+
352
+ // Emit events for each added tier.
353
+ for (uint256 i; i < tiersToAdd.length; i++) {
354
+ emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: _msgSender()});
355
+ }
356
+ }
357
+ }
358
+
359
+ /// @notice Manually mint NFTs from the provided tiers .
360
+ /// @param tierIds The IDs of the tiers to mint from.
361
+ /// @param beneficiary The address to mint to.
362
+ /// @return tokenIds The IDs of the newly minted tokens.
363
+ function mintFor(
364
+ uint16[] calldata tierIds,
365
+ address beneficiary
366
+ )
367
+ external
368
+ override
369
+ returns (uint256[] memory tokenIds)
370
+ {
371
+ // Enforce permissions.
372
+ _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
373
+
374
+ // Record the mint. The token IDs returned correspond to the tiers passed in.
375
+ // slither-disable-next-line reentrancy-events,unused-return
376
+ (tokenIds,) = STORE.recordMint({
377
+ amount: type(uint256).max, // force the mint.
378
+ tierIds: tierIds,
379
+ isOwnerMint: true // manual mint.
380
+ });
381
+
382
+ for (uint256 i; i < tierIds.length; i++) {
383
+ // Set the token ID.
384
+ uint256 tokenId = tokenIds[i];
385
+
386
+ // Mint the NFT.
387
+ _mint(beneficiary, tokenId);
388
+
389
+ emit Mint({
390
+ tokenId: tokenId, tierId: tierIds[i], beneficiary: beneficiary, totalAmountPaid: 0, caller: _msgSender()
391
+ });
392
+ }
393
+ }
394
+
395
+ /// @notice Mint pending reserved NFTs based on the provided information.
396
+ /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
397
+ /// @param reserveMintConfigs Contains information about how many reserved tokens to mint for each tier.
398
+ function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
399
+ for (uint256 i; i < reserveMintConfigs.length; i++) {
400
+ // Get a reference to the params being iterated upon.
401
+ JB721TiersMintReservesConfig memory params = reserveMintConfigs[i];
402
+
403
+ // Mint pending reserved NFTs from the tier.
404
+ mintPendingReservesFor({tierId: params.tierId, count: params.count});
405
+ }
406
+ }
407
+
408
+ /// @notice Allows the collection's owner to set the discount for a tier, if the tier allows it.
409
+ /// @dev Only the contract's owner or an operator with the `SET_721_DISCOUNT_PERCENT` permission from the owner can
410
+ /// adjust the
411
+ /// tiers.
412
+ /// @param tierId The ID of the tier to set the discount of.
413
+ /// @param discountPercent The discount percent to set.
414
+ function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
415
+ // Enforce permissions.
416
+ _requirePermissionFrom({
417
+ account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
418
+ });
419
+ _setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
420
+ }
421
+
422
+ /// @notice Allows the collection's owner to set the discount percent for multiple tiers.
423
+ /// @param configs The configs to set the discount percent for.
424
+ function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
425
+ // Enforce permissions.
426
+ _requirePermissionFrom({
427
+ account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
428
+ });
429
+
430
+ for (uint256 i; i < configs.length; i++) {
431
+ // Set the config being iterated on.
432
+ JB721TiersSetDiscountPercentConfig memory config = configs[i];
433
+
434
+ _setDiscountPercentOf({tierId: config.tierId, discountPercent: config.discountPercent});
435
+ }
436
+ }
437
+
438
+ /// @notice Update this hook's URI metadata properties.
439
+ /// @dev Only this contract's owner can set the metadata.
440
+ /// @param baseUri The new base URI.
441
+ /// @param contractUri The new contract URI.
442
+ /// @param tokenUriResolver The new URI resolver.
443
+ /// @param encodedIPFSUriTierId The ID of the tier to set the encoded IPFS URI of.
444
+ /// @param encodedIPFSUri The encoded IPFS URI to set.
445
+ function setMetadata(
446
+ string calldata baseUri,
447
+ string calldata contractUri,
448
+ IJB721TokenUriResolver tokenUriResolver,
449
+ uint256 encodedIPFSUriTierId,
450
+ bytes32 encodedIPFSUri
451
+ )
452
+ external
453
+ override
454
+ {
455
+ // Enforce permissions.
456
+ _requirePermissionFrom({
457
+ account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_METADATA
458
+ });
459
+
460
+ if (bytes(baseUri).length != 0) {
461
+ // Store the new base URI.
462
+ baseURI = baseUri;
463
+ emit SetBaseUri({baseUri: baseUri, caller: _msgSender()});
464
+ }
465
+ if (bytes(contractUri).length != 0) {
466
+ // Store the new contract URI.
467
+ contractURI = contractUri;
468
+ emit SetContractUri({uri: contractUri, caller: _msgSender()});
469
+ }
470
+
471
+ if (tokenUriResolver != IJB721TokenUriResolver(address(this))) {
472
+ // Store the new URI resolver.
473
+ // slither-disable-next-line reentrancy-events
474
+ _recordSetTokenUriResolver(tokenUriResolver);
475
+ }
476
+ if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
477
+ emit SetEncodedIPFSUri({tierId: encodedIPFSUriTierId, encodedUri: encodedIPFSUri, caller: _msgSender()});
478
+
479
+ // Store the new encoded IPFS URI.
480
+ STORE.recordSetEncodedIPFSUriOf({tierId: encodedIPFSUriTierId, encodedIPFSUri: encodedIPFSUri});
481
+ }
482
+ }
483
+
484
+ //*********************************************************************//
485
+ // ----------------------- public transactions ----------------------- //
486
+ //*********************************************************************//
487
+
488
+ /// @notice Mint reserved pending reserved NFTs within the provided tier.
489
+ /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
490
+ /// @param tierId The ID of the tier to mint reserved NFTs from.
491
+ /// @param count The number of reserved NFTs to mint.
492
+ function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
493
+ // Get a reference to the project's current ruleset.
494
+ JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
495
+
496
+ // Pending reserve mints must not be paused.
497
+ if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
498
+ {
499
+ revert JB721TiersHook_MintReserveNftsPaused();
500
+ }
501
+
502
+ // Record the reserved mint for the tier.
503
+ // slither-disable-next-line reentrancy-events,calls-loop
504
+ uint256[] memory tokenIds = STORE.recordMintReservesFor({tierId: tierId, count: count});
505
+
506
+ // Keep a reference to the beneficiary.
507
+ // slither-disable-next-line calls-loop
508
+ address reserveBeneficiary = STORE.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
509
+
510
+ for (uint256 i; i < count; i++) {
511
+ // Set the token ID.
512
+ uint256 tokenId = tokenIds[i];
513
+
514
+ emit MintReservedNft({
515
+ tokenId: tokenId, tierId: tierId, beneficiary: reserveBeneficiary, caller: _msgSender()
516
+ });
517
+
518
+ // Mint the NFT.
519
+ // slither-disable-next-line reentrency-events
520
+ _mint(reserveBeneficiary, tokenId);
521
+ }
522
+ }
523
+
524
+ //*********************************************************************//
525
+ // ------------------------ internal functions ----------------------- //
526
+ //*********************************************************************//
527
+
528
+ /// @notice A function which gets called after NFTs have been cashed out and recorded by the terminal.
529
+ /// @param tokenIds The token IDs of the NFTs that were burned.
530
+ function _didBurn(uint256[] memory tokenIds) internal virtual override {
531
+ // Add to burned counter.
532
+ STORE.recordBurn(tokenIds);
533
+ }
534
+
535
+ /// @notice Mints one NFT from each of the specified tiers for the beneficiary.
536
+ /// @dev The same tier can be specified more than once.
537
+ /// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than
538
+ /// this amount.
539
+ /// @param mintTierIds An array of NFT tier IDs to be minted.
540
+ /// @param beneficiary The address receiving the newly minted NFTs.
541
+ /// @return leftoverAmount The `amount` leftover after minting.
542
+ function _mintAll(
543
+ uint256 amount,
544
+ uint16[] memory mintTierIds,
545
+ address beneficiary
546
+ )
547
+ internal
548
+ returns (uint256 leftoverAmount)
549
+ {
550
+ // Keep a reference to the NFT token IDs.
551
+ uint256[] memory tokenIds;
552
+
553
+ // Record the NFT mints. The token IDs returned correspond to the tier IDs passed in.
554
+ (tokenIds, leftoverAmount) = STORE.recordMint({
555
+ amount: amount,
556
+ tierIds: mintTierIds,
557
+ isOwnerMint: false // Not a manual mint
558
+ });
559
+
560
+ // Loop through each token ID and mint the corresponding NFT.
561
+ for (uint256 i; i < tokenIds.length; i++) {
562
+ // Get a reference to the token ID being iterated on.
563
+ uint256 tokenId = tokenIds[i];
564
+
565
+ emit Mint({
566
+ tokenId: tokenId,
567
+ tierId: mintTierIds[i],
568
+ beneficiary: beneficiary,
569
+ totalAmountPaid: amount,
570
+ caller: _msgSender()
571
+ });
572
+
573
+ // Mint the NFT.
574
+ // slither-disable-next-line reentrancy-events
575
+ _mint(beneficiary, tokenId);
576
+ }
577
+ }
578
+
579
+ /// @notice Process a payment, minting NFTs and updating credits as necessary.
580
+ /// @dev Pay credits are tracked per beneficiary, not per payer. When the payer differs from the beneficiary,
581
+ /// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
582
+ /// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
583
+ /// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
584
+ function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override {
585
+ // Normalize the payment value based on the pricing context.
586
+ uint256 value;
587
+
588
+ {
589
+ uint256 packed = _packedPricingContext;
590
+ // pricing currency in bits 0-31 (32 bits).
591
+ uint256 pricingCurrency = uint256(uint32(packed));
592
+ if (context.amount.currency == pricingCurrency) {
593
+ value = context.amount.value;
594
+ } else {
595
+ // prices in bits 40-199 (160 bits).
596
+ IJBPrices prices = IJBPrices(address(uint160(packed >> 40)));
597
+ if (prices != IJBPrices(address(0))) {
598
+ // pricing decimals in bits 32-39 (8 bits).
599
+ uint256 pricingDecimals = uint256(uint8(packed >> 32));
600
+ value = mulDiv(
601
+ context.amount.value,
602
+ 10 ** pricingDecimals,
603
+ prices.pricePerUnitOf({
604
+ projectId: PROJECT_ID,
605
+ pricingCurrency: context.amount.currency,
606
+ unitCurrency: pricingCurrency,
607
+ decimals: context.amount.decimals
608
+ })
609
+ );
610
+ } else {
611
+ revert JB721TiersHook_CurrencyMismatch(context.amount.currency, pricingCurrency);
612
+ }
613
+ }
614
+ }
615
+
616
+ // Keep a reference to the number of NFT credits the beneficiary already has.
617
+ uint256 payCredits = payCreditsOf[context.beneficiary];
618
+
619
+ // Set the leftover amount as the initial value.
620
+ uint256 leftoverAmount = value;
621
+
622
+ // If the payer is the beneficiary, combine their NFT credits with the amount paid.
623
+ uint256 unusedPayCredits;
624
+ if (context.payer == context.beneficiary) {
625
+ unchecked {
626
+ leftoverAmount += payCredits;
627
+ }
628
+ } else {
629
+ // Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
630
+ unusedPayCredits = payCredits;
631
+ }
632
+
633
+ // Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted is
634
+ // allowed. Defaults to the collection's flag.
635
+ bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
636
+
637
+ // Resolve the metadata.
638
+ (bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
639
+ id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: context.payerMetadata
640
+ });
641
+
642
+ if (found) {
643
+ // Keep a reference to the IDs of the tier be to minted.
644
+ uint16[] memory tierIdsToMint;
645
+
646
+ // Keep a reference to the payer's flag indicating whether overspending is allowed.
647
+ bool payerAllowsOverspending;
648
+
649
+ // Decode the metadata.
650
+ (payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
651
+
652
+ // Make sure overspending is allowed if requested.
653
+ if (allowOverspending && !payerAllowsOverspending) {
654
+ allowOverspending = false;
655
+ }
656
+
657
+ // Mint NFTs from the tiers as specified.
658
+ if (tierIdsToMint.length != 0) {
659
+ // slither-disable-next-line reentrancy-events,reentrancy-no-eth
660
+ leftoverAmount =
661
+ _mintAll({amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary});
662
+ }
663
+ }
664
+
665
+ // If overspending is allowed and there are leftover funds, add those funds to the beneficiary's NFT credits.
666
+ if (leftoverAmount != 0) {
667
+ // If overspending isn't allowed, revert.
668
+ if (!allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
669
+
670
+ // Store the leftover amount as NFT credits.
671
+ unchecked {
672
+ // Keep a reference to the amount of new NFT credits.
673
+ uint256 newPayCredits = leftoverAmount + unusedPayCredits;
674
+
675
+ // Emit the change in NFT credits.
676
+ if (newPayCredits > payCredits) {
677
+ emit AddPayCredits({
678
+ amount: newPayCredits - payCredits,
679
+ newTotalCredits: newPayCredits,
680
+ account: context.beneficiary,
681
+ caller: _msgSender()
682
+ });
683
+ } else if (payCredits > newPayCredits) {
684
+ emit UsePayCredits({
685
+ amount: payCredits - newPayCredits,
686
+ newTotalCredits: newPayCredits,
687
+ account: context.beneficiary,
688
+ caller: _msgSender()
689
+ });
690
+ }
691
+
692
+ // Store the new NFT credits for the beneficiary.
693
+ payCreditsOf[context.beneficiary] = newPayCredits;
694
+ }
695
+ // Otherwise, reset their NFT credits.
696
+ } else if (payCredits != unusedPayCredits) {
697
+ // Emit the change in NFT credits.
698
+ emit UsePayCredits({
699
+ amount: payCredits - unusedPayCredits,
700
+ newTotalCredits: unusedPayCredits,
701
+ account: context.beneficiary,
702
+ caller: _msgSender()
703
+ });
704
+
705
+ // Store the new NFT credits.
706
+ payCreditsOf[context.beneficiary] = unusedPayCredits;
707
+ }
708
+ }
709
+
710
+ /// @notice Record the setting of a new token URI resolver.
711
+ /// @param tokenUriResolver The new token URI resolver.
712
+ function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
713
+ emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
714
+
715
+ STORE.recordSetTokenUriResolver(tokenUriResolver);
716
+ }
717
+
718
+ /// @notice Internal function to set the discount percent for a tier.
719
+ /// @param tierId The ID of the tier to set the discount percent for.
720
+ /// @param discountPercent The discount percent to set for the tier.
721
+ function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
722
+ emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: _msgSender()});
723
+
724
+ // Record the discount percent for the tier.
725
+ // slither-disable-next-line calls-loop
726
+ STORE.recordSetDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
727
+ }
728
+
729
+ /// @notice Before transferring an NFT, register its first owner (if necessary).
730
+ /// @param to The address the NFT is being transferred to.
731
+ /// @param tokenId The token ID of the NFT being transferred.
732
+ function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
733
+ // Get a reference to the tier.
734
+ // slither-disable-next-line calls-loop
735
+ JB721Tier memory tier = STORE.tierOfTokenId({hook: address(this), tokenId: tokenId, includeResolvedUri: false});
736
+
737
+ // Record the transfers and keep a reference to where the token is coming from.
738
+ from = super._update(to, tokenId, auth);
739
+
740
+ // Transfers must not be paused (when not minting or burning).
741
+ if (from != address(0)) {
742
+ // If transfers are pausable, check if they're paused.
743
+ if (tier.transfersPausable) {
744
+ // Get a reference to the project's current ruleset.
745
+ JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
746
+
747
+ // If transfers are paused and the NFT isn't being transferred to the zero address, revert.
748
+ if (
749
+ to != address(0)
750
+ && JB721TiersRulesetMetadataResolver.transfersPaused(
751
+ (JBRulesetMetadataResolver.metadata(ruleset))
752
+ )
753
+ ) revert JB721TiersHook_TierTransfersPaused();
754
+ }
755
+
756
+ // If the token isn't already associated with a first owner, store the sender as the first owner.
757
+ // slither-disable-next-line calls-loop
758
+ if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
759
+ }
760
+
761
+ // Record the transfer.
762
+ // slither-disable-next-line reentrency-events,calls-loop
763
+ STORE.recordTransferForTier({tierId: tier.id, from: from, to: to});
764
+ }
765
+ }