@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,443 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "../utils/UnitTestSetup.sol";
5
+
6
+ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup {
7
+ using stdStorage for StdStorage;
8
+
9
+ function test_mintPendingReservesFor_mintsCorrectly() public {
10
+ uint256 initialSupply = 200; // The number of NFTs available for each tier.
11
+ uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
12
+ uint256 reservedMinted = 1; // The number of reserve NFTs already minted (out of `totalMinted`).
13
+ uint256 reserveFrequency = 4000; // The frequency at which NFTs are reserved (4000/10000 = 40%).
14
+ uint256 numberOfTiers = 3; // The number of tiers to set up.
15
+
16
+ // With 120 total NFTs minted and 1 being a reserve mint, 119 are non-reserved.
17
+ // With a 40% reserve frequency, 47 should be reserved.
18
+ // Accounting for the 1 already minted, there should be 46 pending reserve mints.
19
+
20
+ ForTest_JB721TiersHook hook = _initializeForTestHook(numberOfTiers);
21
+
22
+ // Initialize `numberOfTiers` tiers.
23
+ for (uint256 i; i < numberOfTiers; i++) {
24
+ hook.test_store()
25
+ .ForTest_setTier(
26
+ address(hook),
27
+ i + 1,
28
+ JBStored721Tier({
29
+ price: uint104((i + 1) * 10),
30
+ remainingSupply: uint32(initialSupply - totalMinted),
31
+ initialSupply: uint32(initialSupply),
32
+ votingUnits: uint16(0),
33
+ reserveFrequency: uint16(reserveFrequency),
34
+ category: uint24(100),
35
+ discountPercent: uint8(0),
36
+ packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false)
37
+ })
38
+ );
39
+ hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
40
+ }
41
+
42
+ // Iterate through the tiers, minting the pending reserves,
43
+ // and ensuring that the correct number of NFTs have been minted.
44
+ for (uint256 tier = 1; tier <= numberOfTiers; tier++) {
45
+ uint256 mintable = hook.test_store().numberOfPendingReservesFor(address(hook), tier);
46
+
47
+ // Mint the reserve NFTs for the tier.
48
+ for (uint256 token = 1; token <= mintable; token++) {
49
+ vm.expectEmit(true, true, true, true, address(hook));
50
+ emit MintReservedNft(_generateTokenId(tier, totalMinted + token), tier, reserveBeneficiary, owner);
51
+ }
52
+
53
+ vm.prank(owner);
54
+ hook.mintPendingReservesFor(tier, mintable);
55
+
56
+ // Check: does the reserve beneficiary have the correct number of NFTs?
57
+ assertEq(hook.balanceOf(reserveBeneficiary), mintable * tier);
58
+ }
59
+ }
60
+
61
+ // Todo case: initial 10, rr 3, minted 6, what happens? mint the reserves till there's no remaining. then if all
62
+ // users burn, what happens?
63
+ function test_mintPendingReservesFor_mintOddReservedTokens() public {
64
+ uint256 initialSupply = 10; // The number of NFTs available for each tier.
65
+ uint256 totalMinted = 6; // The number of NFTs already minted for each tier (out of `initialSupply`).
66
+ uint256 reserveFrequency = 3; // The frequency at which NFTs are reserved (3/10 = 30%).
67
+
68
+ ForTest_JB721TiersHook hook = _initializeForTestHook(1);
69
+
70
+ // Initialize `numberOfTiers` tiers.
71
+ hook.test_store()
72
+ .ForTest_setTier(
73
+ address(hook),
74
+ 1,
75
+ JBStored721Tier({
76
+ price: uint104(10),
77
+ remainingSupply: uint32(initialSupply),
78
+ initialSupply: uint32(initialSupply),
79
+ votingUnits: uint16(0),
80
+ reserveFrequency: uint16(reserveFrequency),
81
+ category: uint24(100),
82
+ discountPercent: uint8(0),
83
+ packedBools: hook.test_store().ForTest_packBools(true, false, true, false, false)
84
+ })
85
+ );
86
+
87
+ // Mint the initial tiers.
88
+ uint16[] memory tiersToMint = new uint16[](totalMinted);
89
+ for (uint256 i; i < totalMinted; i++) {
90
+ tiersToMint[i] = 1;
91
+ }
92
+ vm.prank(owner);
93
+ hook.mintFor(tiersToMint, beneficiary);
94
+
95
+ // Iterate through the tiers, calculating how many reserve NFTs should be mintable.
96
+ uint256 mintable = hook.test_store().numberOfPendingReservesFor(address(hook), 1);
97
+ assertEq(mintable, 2, "Tier 1 should have 2 reserve NFTs mintable.");
98
+
99
+ // Mint the next tier
100
+ tiersToMint = new uint16[](1);
101
+ tiersToMint[0] = 1;
102
+ vm.prank(owner);
103
+ hook.mintFor(tiersToMint, beneficiary);
104
+
105
+ // Should have one more reserved.
106
+ mintable = hook.test_store().numberOfPendingReservesFor(address(hook), 1);
107
+ assertEq(mintable, 3, "Tier 1 should have 3 reserve NFTs mintable.");
108
+
109
+ // Revert when minting the next.
110
+ vm.expectRevert(
111
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
112
+ );
113
+ vm.prank(owner);
114
+ hook.mintFor(tiersToMint, beneficiary);
115
+
116
+ // Package reserves to mint.
117
+ JB721TiersMintReservesConfig[] memory reservesToMint = new JB721TiersMintReservesConfig[](1);
118
+ reservesToMint[0] = JB721TiersMintReservesConfig({tierId: uint32(1), count: uint16(mintable)});
119
+
120
+ // Mint the pending reserve NFTs.
121
+ vm.prank(owner);
122
+ hook.mintPendingReservesFor(reservesToMint);
123
+
124
+ // Check: does the reserve beneficiary and beneficiary have the correct number of NFTs?
125
+ assertEq(hook.balanceOf(reserveBeneficiary), 3);
126
+ assertEq(hook.balanceOf(beneficiary), 7);
127
+
128
+ mintable = hook.test_store().numberOfPendingReservesFor(address(hook), 1);
129
+ assertEq(mintable, 0, "Tier 1 should have 0 reserve NFTs mintable.");
130
+
131
+ // Burn them all.
132
+ uint256[] memory tokenIdsToBurn = new uint256[](initialSupply);
133
+ for (uint256 i; i < initialSupply; i++) {
134
+ tokenIdsToBurn[i] = 1_000_000_000 + 1 + i;
135
+ }
136
+ vm.prank(address(hook));
137
+ hook.burn(tokenIdsToBurn);
138
+
139
+ // Check: does the reserve beneficiary and beneficiary have the correct number of NFTs?
140
+ assertEq(hook.balanceOf(reserveBeneficiary), 0);
141
+ assertEq(hook.balanceOf(beneficiary), 0);
142
+
143
+ // No pending reserves still.
144
+ mintable = hook.test_store().numberOfPendingReservesFor(address(hook), 1);
145
+ assertEq(mintable, 0, "Tier 1 should have 0 reserve NFTs mintable.");
146
+
147
+ // No remaining supply still.
148
+ JB721Tier memory tier = hook.STORE().tierOf(address(hook), 1, false);
149
+ assertEq(tier.remainingSupply, 0, "Tier 1 should have 0 remaining supply.");
150
+
151
+ // Revert when minting the next.
152
+ vm.expectRevert(
153
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
154
+ );
155
+ vm.prank(owner);
156
+ hook.mintFor(tiersToMint, beneficiary);
157
+ }
158
+
159
+ function test_mintPendingReservesFor_mintMultipleReservedTokens() public {
160
+ uint256 initialSupply = 200; // The number of NFTs available for each tier.
161
+ uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
162
+ uint256 reservedMinted = 1; // The number of reserve NFTs already minted (out of `totalMinted`).
163
+ uint256 reserveFrequency = 4000; // The frequency at which NFTs are reserved (4000/10000 = 40%).
164
+ uint256 numberOfTiers = 3; // The number of tiers to set up.
165
+
166
+ // With 120 total NFTs minted and 1 being a reserve mint, 119 are non-reserved.
167
+ // With a 40% reserve frequency, 47 should be reserved.
168
+ // Accounting for the 1 already minted, there should be 46 pending reserve mints.
169
+
170
+ ForTest_JB721TiersHook hook = _initializeForTestHook(numberOfTiers);
171
+
172
+ // Initialize `numberOfTiers` tiers.
173
+ for (uint256 i; i < numberOfTiers; i++) {
174
+ hook.test_store()
175
+ .ForTest_setTier(
176
+ address(hook),
177
+ i + 1,
178
+ JBStored721Tier({
179
+ price: uint104((i + 1) * 10),
180
+ remainingSupply: uint32(initialSupply - totalMinted),
181
+ initialSupply: uint32(initialSupply),
182
+ votingUnits: uint16(0),
183
+ reserveFrequency: uint16(reserveFrequency),
184
+ category: uint24(100),
185
+ discountPercent: uint8(0),
186
+ packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false)
187
+ })
188
+ );
189
+
190
+ // Set the number of reserve NFTs already minted for the tier.
191
+ hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
192
+ }
193
+
194
+ uint256 totalMintable; // Keep a running counter of how many reserve NFTs should be mintable.
195
+
196
+ JB721TiersMintReservesConfig[] memory reservesToMint = new JB721TiersMintReservesConfig[](numberOfTiers);
197
+
198
+ // Iterate through the tiers, calculating how many reserve NFTs should be mintable.
199
+ for (uint256 tier = 1; tier <= numberOfTiers; tier++) {
200
+ uint256 mintable = hook.test_store().numberOfPendingReservesFor(address(hook), tier);
201
+ reservesToMint[tier - 1] = JB721TiersMintReservesConfig({tierId: uint32(tier), count: uint16(mintable)});
202
+ totalMintable += mintable;
203
+ for (uint256 token = 1; token <= mintable; token++) {
204
+ uint256 tokenNonce = totalMinted + token; // Avoid stack too deep
205
+ vm.expectEmit(true, true, true, true, address(hook));
206
+ emit MintReservedNft(_generateTokenId(tier, tokenNonce), tier, reserveBeneficiary, owner);
207
+ }
208
+ }
209
+
210
+ // Mint the pending reserve NFTs.
211
+ vm.prank(owner);
212
+ hook.mintPendingReservesFor(reservesToMint);
213
+
214
+ // Check: does the reserve beneficiary has the correct number of NFTs?
215
+ assertEq(hook.balanceOf(reserveBeneficiary), totalMintable);
216
+ }
217
+
218
+ function test_mintPendingReservesFor_revertIfReservedMintingIsPausedInRuleset() public {
219
+ uint256 initialSupply = 200; // The number of NFTs available for each tier.
220
+ uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
221
+ uint256 reservedMinted = 1; // The number of reserve NFTs already minted (out of `totalMinted`).
222
+ uint256 reserveFrequency = 4000; // The frequency at which NFTs are reserved (4000/10000 = 40%).
223
+ uint256 numberOfTiers = 3; // The number of tiers to set up.
224
+
225
+ // Set up the ruleset to pause reserved minting.
226
+ // This is done with the `JBRulesetMetadata.metadata` field.
227
+ // The second bit in `JBRulesetMetadata.metadata` is the `mintPendingReservesPaused` bit.
228
+ // See `JB721TiersRulesetMetadataResolver`.
229
+ mockAndExpect(
230
+ mockJBRulesets,
231
+ abi.encodeCall(IJBRulesets.currentOf, projectId),
232
+ abi.encode(
233
+ JBRuleset({
234
+ cycleNumber: 1,
235
+ id: uint48(block.timestamp),
236
+ basedOnId: 0,
237
+ start: uint48(block.timestamp),
238
+ duration: 600,
239
+ weight: 10e18,
240
+ weightCutPercent: 0,
241
+ approvalHook: IJBRulesetApprovalHook(address(0)),
242
+ metadata: JBRulesetMetadataResolver.packRulesetMetadata(
243
+ JBRulesetMetadata({
244
+ reservedPercent: 5000, //50%
245
+ cashOutTaxRate: 5000, //50%
246
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
247
+ pausePay: false,
248
+ pauseCreditTransfers: false,
249
+ allowOwnerMinting: true,
250
+ allowSetCustomToken: false,
251
+ allowTerminalMigration: false,
252
+ allowSetTerminals: false,
253
+ allowSetController: false,
254
+ allowAddAccountingContext: false,
255
+ allowAddPriceFeed: false,
256
+ ownerMustSendPayouts: false,
257
+ holdFees: false,
258
+ useTotalSurplusForCashOuts: false,
259
+ useDataHookForPay: true,
260
+ useDataHookForCashOut: true,
261
+ dataHook: address(0),
262
+ metadata: 2
263
+ })
264
+ )
265
+ })
266
+ )
267
+ );
268
+
269
+ ForTest_JB721TiersHook hook = _initializeForTestHook(numberOfTiers);
270
+
271
+ for (uint256 i; i < numberOfTiers; i++) {
272
+ hook.test_store()
273
+ .ForTest_setTier(
274
+ address(hook),
275
+ i + 1,
276
+ JBStored721Tier({
277
+ price: uint104((i + 1) * 10),
278
+ remainingSupply: uint32(initialSupply - totalMinted),
279
+ initialSupply: uint32(initialSupply),
280
+ votingUnits: uint16(0),
281
+ reserveFrequency: uint16(reserveFrequency),
282
+ category: uint24(100),
283
+ discountPercent: uint8(0),
284
+ packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false)
285
+ })
286
+ );
287
+ hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
288
+ }
289
+
290
+ // Iterate through the tiers, attempting to mint the pending reserves.
291
+ // Check: is the correct error thrown?
292
+ for (uint256 tier = 1; tier <= numberOfTiers; tier++) {
293
+ uint256 mintable = hook.test_store().numberOfPendingReservesFor(address(hook), tier);
294
+ vm.prank(owner);
295
+ vm.expectRevert(JB721TiersHook.JB721TiersHook_MintReserveNftsPaused.selector);
296
+ hook.mintPendingReservesFor(tier, mintable);
297
+ }
298
+ }
299
+
300
+ function test_mintPendingReservesFor_revertIfNotEnoughPendingReserves() public {
301
+ uint256 initialSupply = 200; // The number of NFTs available for each tier.
302
+ uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
303
+ uint256 reservedMinted = 1; // The number of reserve NFTs already minted (out of `totalMinted`).
304
+ uint256 reserveFrequency = 4000; // The frequency at which NFTs are reserved (4000/10000 = 40%).
305
+
306
+ ForTest_JB721TiersHook hook = _initializeForTestHook(10);
307
+
308
+ // Initialize `numberOfTiers` tiers.
309
+ for (uint256 i; i < 10; i++) {
310
+ hook.test_store()
311
+ .ForTest_setTier(
312
+ address(hook),
313
+ i + 1,
314
+ JBStored721Tier({
315
+ price: uint104((i + 1) * 10),
316
+ remainingSupply: uint32(initialSupply - totalMinted),
317
+ initialSupply: uint32(initialSupply),
318
+ votingUnits: uint16(0),
319
+ reserveFrequency: uint16(reserveFrequency),
320
+ category: uint24(100),
321
+ discountPercent: uint8(0),
322
+ packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false)
323
+ })
324
+ );
325
+ hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
326
+ }
327
+
328
+ // Iterate through the tiers, attempting to mint more pending reserves than what is available.
329
+ for (uint256 i = 1; i <= 10; i++) {
330
+ // Get the number that we could mint successfully.
331
+ uint256 amount = hook.test_store().numberOfPendingReservesFor(address(hook), i);
332
+ // Increase it by 1 to cause an error, then attempt to mint.
333
+ amount++;
334
+ // Check: is the correct error thrown?
335
+ vm.expectRevert(
336
+ abi.encodeWithSelector(
337
+ JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector, amount, amount - 1
338
+ )
339
+ );
340
+ vm.prank(owner);
341
+ hook.mintPendingReservesFor(i, amount);
342
+ }
343
+ }
344
+
345
+ function test_numberOfPendingReservesFor_noReservesIfNoBeneficiarySet() public {
346
+ uint256 initialSupply = 200; // The number of NFTs available for each tier.
347
+ uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
348
+ uint256 reservedMinted = 10; // The number of reserve NFTs already minted (out of `totalMinted`).
349
+ uint256 reserveFrequency = 9; // The frequency at which NFTs are reserved.
350
+ // (For every 9 NFTs minted, 1 is reserved).
351
+
352
+ reserveBeneficiary = address(0);
353
+ ForTest_JB721TiersHook hook = _initializeForTestHook(10);
354
+
355
+ // Initialize `numberOfTiers` tiers, and set the number of reserve NFTs already minted for each tier.
356
+ // Although the `reserveFrequency` is set, it should be ignored since there is no reserve beneficiary.
357
+ for (uint256 i; i < 10; i++) {
358
+ hook.test_store()
359
+ .ForTest_setTier(
360
+ address(hook),
361
+ i + 1,
362
+ JBStored721Tier({
363
+ price: uint104((i + 1) * 10),
364
+ remainingSupply: uint32(initialSupply - totalMinted),
365
+ initialSupply: uint32(initialSupply),
366
+ votingUnits: uint16(0),
367
+ reserveFrequency: uint16(reserveFrequency),
368
+ category: uint24(100),
369
+ discountPercent: uint8(0),
370
+ packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false)
371
+ })
372
+ );
373
+ hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
374
+ }
375
+
376
+ // Fetch the stored tiers.
377
+ JB721Tier[] memory storedTiers = hook.test_store().tiersOf(address(hook), new uint256[](0), false, 0, 10);
378
+
379
+ // Check: did the reserve frequency default to 0 for all tiers?
380
+ for (uint256 i; i < 10; i++) {
381
+ assertEq(storedTiers[i].reserveFrequency, 0, "Reserve frequency should be zero (no beneficiary set).");
382
+ }
383
+ // Check: are we sure there are no pending reserves for all tiers?
384
+ for (uint256 i; i < 10; i++) {
385
+ assertEq(
386
+ hook.test_store().numberOfPendingReservesFor(address(hook), i + 1),
387
+ 0,
388
+ "There should not be any pending reserves (no beneficiary set)."
389
+ );
390
+ }
391
+ }
392
+
393
+ function test_mintFor_mintArrayOfTiers() public {
394
+ uint256 numberOfTiers = 3;
395
+
396
+ defaultTierConfig.allowOwnerMint = true;
397
+ defaultTierConfig.reserveFrequency = 0;
398
+ ForTest_JB721TiersHook hook = _initializeForTestHook(numberOfTiers);
399
+
400
+ // Mint 6 NFTs, 2 from each tier.
401
+ uint16[] memory tiersToMint = new uint16[](numberOfTiers * 2);
402
+ for (uint256 i; i < numberOfTiers; i++) {
403
+ tiersToMint[i] = uint16(i) + 1;
404
+ tiersToMint[tiersToMint.length - 1 - i] = uint16(i) + 1;
405
+ }
406
+
407
+ vm.prank(owner);
408
+ hook.mintFor(tiersToMint, beneficiary);
409
+
410
+ // Check: does the beneficiary have the correct number of NFTs?
411
+ assertEq(hook.balanceOf(beneficiary), 6);
412
+
413
+ // Check: does the beneficiary own the correct NFTs?
414
+ assertEq(hook.ownerOf(_generateTokenId(1, 1)), beneficiary);
415
+ assertEq(hook.ownerOf(_generateTokenId(1, 2)), beneficiary);
416
+ assertEq(hook.ownerOf(_generateTokenId(2, 1)), beneficiary);
417
+ assertEq(hook.ownerOf(_generateTokenId(2, 2)), beneficiary);
418
+ assertEq(hook.ownerOf(_generateTokenId(3, 1)), beneficiary);
419
+ assertEq(hook.ownerOf(_generateTokenId(3, 2)), beneficiary);
420
+ }
421
+
422
+ function test_mintFor_revertIfManualMintNotAllowed() public {
423
+ uint256 numberOfTiers = 10;
424
+
425
+ uint16[] memory tiersToMint = new uint16[](numberOfTiers * 2);
426
+ for (uint256 i; i < numberOfTiers; i++) {
427
+ tiersToMint[i] = uint16(i) + 1;
428
+ tiersToMint[tiersToMint.length - 1 - i] = uint16(i) + 1;
429
+ }
430
+
431
+ // Set the `allowOwnerMint` flag to false and initialize the hook.
432
+ defaultTierConfig.allowOwnerMint = false;
433
+ ForTest_JB721TiersHook hook = _initializeForTestHook(numberOfTiers);
434
+
435
+ vm.prank(owner);
436
+
437
+ // Expect the function call to revert with the specified error message.
438
+ vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_CantMintManually.selector, 1));
439
+
440
+ // Call the `mintFor` function to trigger the revert.
441
+ hook.mintFor(tiersToMint, beneficiary);
442
+ }
443
+ }