@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,1740 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "../utils/UnitTestSetup.sol";
5
+
6
+ contract Test_adjustTier_Unit is UnitTestSetup {
7
+ using stdStorage for StdStorage;
8
+
9
+ function test_adjustTiers_removeTiersMultipleTimes(
10
+ uint256 initialNumberOfTiers,
11
+ uint256 numberOfTiersToAdd,
12
+ uint256 seed
13
+ )
14
+ public
15
+ {
16
+ // Include adding X new tiers with 0 current tiers.
17
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 10);
18
+
19
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
20
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
21
+
22
+ // Sort tiers in ascending order.
23
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
24
+
25
+ // Initialize the hook with default tiers.
26
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
27
+
28
+ // Create the configs for the new tiers to add.
29
+ (JB721TierConfig[] memory tierConfigs,) =
30
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
31
+
32
+ // Remove 2 tiers and add the new ones. `_addDeleteTiers` calls `adjustTiers`.
33
+ uint256 tiersLeft = initialNumberOfTiers;
34
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 2, tierConfigs);
35
+
36
+ // Remove another 2 tiers but don't add any new ones.
37
+ _addDeleteTiers(hook, tiersLeft, 2, new JB721TierConfig[](0));
38
+
39
+ // Fecth a maximum of 100 tiers (will downsize).
40
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
41
+ // If the tiers have same category, then the the tiers added last will have a lower index in the array.
42
+ // Otherwise, the tiers would be sorted by categories.
43
+ for (uint256 i = 1; i < storedTiers.length; i++) {
44
+ if (storedTiers[i - 1].category == storedTiers[i].category) {
45
+ assertGt(storedTiers[i - 1].id, storedTiers[i].id, "Sorting error (if same category).");
46
+ } else {
47
+ assertLt(storedTiers[i - 1].category, storedTiers[i].category, "Sorting error (if different category).");
48
+ }
49
+ }
50
+ }
51
+
52
+ function test_tiersOf_recentlyAddedTiersFetchedFirstWhenSortedAfterTiersCleaned(
53
+ uint256 initialNumberOfTiers,
54
+ uint256 numberOfTiersToAdd,
55
+ uint256 seed
56
+ )
57
+ public
58
+ {
59
+ initialNumberOfTiers = bound(initialNumberOfTiers, 3, 14);
60
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 1, 15);
61
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
62
+
63
+ // Sort tiers in ascending order.
64
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
65
+
66
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
67
+
68
+ // Create the new tiers to add.
69
+ uint256 currentNumberOfTiers = initialNumberOfTiers;
70
+
71
+ (JB721TierConfig[] memory tierConfigsToAdd,) =
72
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
73
+
74
+ currentNumberOfTiers = _addDeleteTiers(hook, currentNumberOfTiers, 0, tierConfigsToAdd);
75
+ currentNumberOfTiers = _addDeleteTiers(hook, currentNumberOfTiers, 2, tierConfigsToAdd);
76
+
77
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
78
+
79
+ assertEq(storedTiers.length, currentNumberOfTiers, "Length mismatch.");
80
+ // If the tiers have same category, then the tiers added last will have a lower index in the array.
81
+ // Otherwise, the tiers would be sorted by categories.
82
+ for (uint256 i = 1; i < storedTiers.length; i++) {
83
+ if (storedTiers[i - 1].category == storedTiers[i].category) {
84
+ assertGt(storedTiers[i - 1].id, storedTiers[i].id, "Sorting error (if same category).");
85
+ } else {
86
+ assertLt(storedTiers[i - 1].category, storedTiers[i].category, "Sorting error (if different category).");
87
+ }
88
+ }
89
+ }
90
+
91
+ function test_tiersOf_recentlyAddedTiersFetchedFirstWhenSorted(
92
+ uint256 initialNumberOfTiers,
93
+ uint256 numberOfTiersToAdd,
94
+ uint256 seed
95
+ )
96
+ public
97
+ {
98
+ // Include adding X new tiers with 0 current tiers.
99
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 10);
100
+
101
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
102
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
103
+
104
+ // Sort tiers in ascending order.
105
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
106
+
107
+ // Initialize the hook with default tiers.
108
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
109
+
110
+ // Create the new tiers to add.
111
+ (JB721TierConfig[] memory tierConfigs,) =
112
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
113
+
114
+ // Add the new tiers.
115
+ uint256 tiersLeft = initialNumberOfTiers;
116
+
117
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigs);
118
+
119
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
120
+ // If the tiers have same category, then the tiers added last will have a lower index in the array.
121
+ // Otherwise, the tiers would be sorted by categories.
122
+ for (uint256 i = 1; i < storedTiers.length; i++) {
123
+ if (storedTiers[i - 1].category == storedTiers[i].category) {
124
+ assertGt(storedTiers[i - 1].id, storedTiers[i].id, "Sorting error (if same category).");
125
+ } else {
126
+ assertLt(storedTiers[i - 1].category, storedTiers[i].category, "Sorting error (if different category).");
127
+ }
128
+ }
129
+ }
130
+
131
+ function test_adjustTiers_addNewTiersWithNonSequentialCategories(
132
+ uint256 initialNumberOfTiers,
133
+ uint256 numberOfTiersToAdd,
134
+ uint256 seed
135
+ )
136
+ public
137
+ {
138
+ initialNumberOfTiers = bound(initialNumberOfTiers, 2, 10);
139
+
140
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
141
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
142
+
143
+ // Sort tiers in ascending order.
144
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
145
+
146
+ // Initialize the hook with default tiers.
147
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
148
+
149
+ JB721Tier[] memory defaultStoredTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
150
+
151
+ // Create the new tiers to add.
152
+ (JB721TierConfig[] memory tierConfigsToAdd, JB721Tier[] memory tiersToAdd) =
153
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd, 2);
154
+
155
+ // Add the new tiers.
156
+ uint256 tiersLeft = initialNumberOfTiers;
157
+
158
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigsToAdd);
159
+
160
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
161
+
162
+ // Check: was the expected number of tiers returned?
163
+ assertEq(storedTiers.length, tiersLeft, "Length mismatch.");
164
+
165
+ // Check: are all tiers in the new tiers (unsorted)?
166
+ assertTrue(_isIn(defaultStoredTiers, storedTiers), "Original tiers not stored."); // Original tiers
167
+ assertTrue(_isIn(tiersToAdd, storedTiers), "Added tiers not stored."); // New tiers
168
+
169
+ // Check: are all the tiers sorted?
170
+ for (uint256 i = 1; i < storedTiers.length; i++) {
171
+ assertLt(storedTiers[i - 1].category, storedTiers[i].category, "Sorting error.");
172
+ }
173
+ }
174
+
175
+ function test_adjustTiers_addNewTiers(
176
+ uint256 initialNumberOfTiers,
177
+ uint256 numberOfTiersToAdd,
178
+ uint256 seed
179
+ )
180
+ public
181
+ {
182
+ // Include adding X new tiers with 0 current tiers.
183
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 10);
184
+
185
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
186
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
187
+
188
+ // Sort tiers in ascending order.
189
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
190
+
191
+ // Initialize the hook with default tiers.
192
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
193
+
194
+ JB721Tier[] memory intialTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
195
+
196
+ // Create the new tiers to add.
197
+ (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory tiersAdded) =
198
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
199
+
200
+ // Add the new tiers.
201
+ uint256 tiersLeft = initialNumberOfTiers;
202
+
203
+ _addDeleteTiers(hook, tiersLeft, 0, tierConfigs);
204
+
205
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
206
+
207
+ // Check: was the expected number of tiers returned?
208
+ assertEq(storedTiers.length, intialTiers.length + tiersAdded.length, "Length mismatch.");
209
+
210
+ // Check: are all tiers in the new tiers (unsorted)?
211
+ assertTrue(_isIn(intialTiers, storedTiers), "original tiers not found"); // Original tiers
212
+ assertTrue(_isIn(tiersAdded, storedTiers), "new tiers not found"); // New tiers
213
+
214
+ // Check: are all the tiers sorted?
215
+ for (uint256 i = 1; i < storedTiers.length; i++) {
216
+ assertLe(storedTiers[i - 1].category, storedTiers[i].category, "Sorting error");
217
+ }
218
+ }
219
+
220
+ function test_adjustTiers_withSameCategoryMultipleTimes(
221
+ uint256 initialNumberOfTiers,
222
+ uint256 numberOfTiersToAdd,
223
+ uint256 seed
224
+ )
225
+ public
226
+ {
227
+ initialNumberOfTiers = bound(initialNumberOfTiers, 2, 10);
228
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
229
+
230
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
231
+
232
+ // Sort tiers in ascending order.
233
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
234
+
235
+ // Initialize the hook with default tiers of a given category.
236
+ defaultTierConfig.category = 100;
237
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
238
+
239
+ // Create new tiers to add with a new category.
240
+ defaultTierConfig.category = 101;
241
+ (JB721TierConfig[] memory tierConfigsToAdd, JB721Tier[] memory tiersToAdd) =
242
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
243
+
244
+ // Add the new tiers.
245
+ _addDeleteTiers(hook, 0, 0, tierConfigsToAdd);
246
+
247
+ (tierConfigsToAdd, tiersToAdd) = _createTiers(defaultTierConfig, numberOfTiersToAdd);
248
+
249
+ _addDeleteTiers(hook, 0, 0, tierConfigsToAdd);
250
+
251
+ // Check: are all tiers stored?
252
+ JB721Tier[] memory storedTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
253
+ assertEq(storedTiers.length, initialNumberOfTiers + pricesForTiersToAdd.length * 2);
254
+
255
+ // Check: are all tiers in the new tiers (unsorted)?
256
+ uint256[] memory categories = new uint256[](1);
257
+ categories[0] = 101;
258
+
259
+ JB721Tier[] memory stored101Tiers =
260
+ hook.STORE().tiersOf(address(hook), categories, false, 0, pricesForTiersToAdd.length * 2);
261
+
262
+ assertEq(stored101Tiers.length, pricesForTiersToAdd.length * 2);
263
+
264
+ // Check: are all the tiers in the initial tiers?
265
+ categories[0] = 100;
266
+ JB721Tier[] memory stored100Tiers = hook.STORE().tiersOf(address(hook), categories, false, 0, 100);
267
+ assertEq(stored100Tiers.length, initialNumberOfTiers);
268
+
269
+ // Check: are the tiers sorted correctly?
270
+ for (uint256 i; i < pricesForTiersToAdd.length; i++) {
271
+ assertGt(stored101Tiers[i].id, stored101Tiers[i + pricesForTiersToAdd.length].id);
272
+ }
273
+ }
274
+
275
+ function test_adjustTiers_withDifferentCategories(
276
+ uint256 initialNumberOfTiers,
277
+ uint256 numberOfTiersToAdd,
278
+ uint256 seed
279
+ )
280
+ public
281
+ {
282
+ // Include adding X new tiers with 0 current tiers.
283
+ initialNumberOfTiers = bound(initialNumberOfTiers, 1, 10);
284
+
285
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 1, 10);
286
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
287
+
288
+ // Sort tiers in ascending order.
289
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
290
+
291
+ // Initialize the hook with default tiers.
292
+ defaultTierConfig.category = 100;
293
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
294
+
295
+ // Create new tiers to add.
296
+ defaultTierConfig.category = 101;
297
+ (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory tiersAdded) =
298
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
299
+
300
+ // Add the new tiers.
301
+ uint256 tiersLeft = initialNumberOfTiers;
302
+
303
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigs);
304
+
305
+ defaultTierConfig.category = 102;
306
+ (tierConfigs, tiersAdded) =
307
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
308
+
309
+ // Add the new tiers.
310
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigs);
311
+
312
+ JB721Tier[] memory allStoredTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, 100);
313
+
314
+ uint256[] memory categories = new uint256[](1);
315
+ categories[0] = 102;
316
+ JB721Tier[] memory stored102Tiers = hook.STORE().tiersOf(address(hook), categories, false, 0, 100);
317
+
318
+ // Check: does the number of stored 102 tiers match the number of tiers that were added?
319
+ assertEq(stored102Tiers.length, pricesForTiersToAdd.length);
320
+
321
+ // Ensure that each stored 102 tier has category `102`.
322
+ for (uint256 i = 0; i < stored102Tiers.length; i++) {
323
+ assertEq(stored102Tiers[i].category, uint8(102));
324
+ }
325
+
326
+ categories[0] = 101;
327
+
328
+ JB721Tier[] memory stored101Tiers = hook.STORE().tiersOf(address(hook), categories, false, 0, 100);
329
+
330
+ // Check: does the number of stored 101 tiers match the number of tiers that were added?
331
+ assertEq(stored101Tiers.length, pricesForTiersToAdd.length);
332
+
333
+ // Check: does each stored 101 tier have category `101`?
334
+ for (uint256 i = 0; i < stored101Tiers.length; i++) {
335
+ assertEq(stored101Tiers[i].category, uint8(101));
336
+ }
337
+
338
+ // Ensure that the tiers are sorted.
339
+ for (uint256 i = 1; i < initialNumberOfTiers + pricesForTiersToAdd.length * 2; i++) {
340
+ assertGt(allStoredTiers[i].id, allStoredTiers[i - 1].id);
341
+ assertLe(allStoredTiers[i - 1].category, allStoredTiers[i].category);
342
+ }
343
+ }
344
+
345
+ function test_adjustTiers_withZeroCategory(
346
+ uint256 initialNumberOfTiers,
347
+ uint256 numberOfTiersToAdd,
348
+ uint256 seed
349
+ )
350
+ public
351
+ {
352
+ initialNumberOfTiers = bound(initialNumberOfTiers, 2, 10);
353
+
354
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 4, 14);
355
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
356
+
357
+ // Sort tiers in ascending order.
358
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
359
+
360
+ // Use category 0 for the default tiers.
361
+ defaultTierConfig.category = 0;
362
+
363
+ // Initialize the hook with default tiers.
364
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
365
+
366
+ // Create new tiers to add.
367
+ defaultTierConfig.category = 5;
368
+ (JB721TierConfig[] memory tierConfigsToAdd,) =
369
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd, 2);
370
+
371
+ // Add the new tiers.
372
+ uint256 tiersLeft = initialNumberOfTiers;
373
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigsToAdd);
374
+
375
+ // Get all stored tiers (category 0 gets all).
376
+ uint256[] memory categories = new uint256[](1);
377
+ categories[0] = 0;
378
+ JB721Tier[] memory allStoredTiers = hook.STORE().tiersOf(address(hook), categories, false, 0, 100);
379
+
380
+ // Check: does the number of stored tiers match the number of tiers that were added?
381
+ assertEq(allStoredTiers.length, initialNumberOfTiers);
382
+ // Check: does each stored tier have category `0`?
383
+ for (uint256 i = 0; i < allStoredTiers.length; i++) {
384
+ assertEq(allStoredTiers[i].category, uint8(0));
385
+ }
386
+ }
387
+
388
+ function test_adjustTiers_withDifferentCategoriesAndFetchedTogether(
389
+ uint256 initialNumberOfTiers,
390
+ uint256 numberOfTiersToAdd,
391
+ uint256 seed
392
+ )
393
+ public
394
+ {
395
+ initialNumberOfTiers = bound(initialNumberOfTiers, 1, 14);
396
+
397
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 1, 14);
398
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
399
+
400
+ // Sort tiers in ascending order.
401
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
402
+
403
+ // Initialize the hook with default tiers of category 100.
404
+ defaultTierConfig.category = 100;
405
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
406
+
407
+ // Create new tiers to add (with category 101).
408
+ defaultTierConfig.category = 101;
409
+ (JB721TierConfig[] memory tierConfigsToAdd,) =
410
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
411
+
412
+ // Add the 101 tiers.
413
+ uint256 tiersLeft = initialNumberOfTiers;
414
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigsToAdd);
415
+
416
+ // Create new tiers to add (with category 102).
417
+ defaultTierConfig.category = 102;
418
+ (tierConfigsToAdd,) =
419
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
420
+
421
+ // Add the 102 tiers.
422
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigsToAdd);
423
+
424
+ // Get the stored tiers with categories 100-102.
425
+ uint256[] memory categories = new uint256[](3);
426
+ categories[0] = 102;
427
+ categories[1] = 100;
428
+ categories[2] = 101;
429
+ JB721Tier[] memory allStoredTiers = hook.STORE().tiersOf(address(hook), categories, false, 0, 100);
430
+
431
+ // Check: are the correct number of tiers stored?
432
+ assertEq(
433
+ allStoredTiers.length, initialNumberOfTiers + pricesForTiersToAdd.length * 2, "Wrong total number of tiers."
434
+ );
435
+
436
+ uint256 maxIndexForTier100 = allStoredTiers.length - pricesForTiersToAdd.length;
437
+
438
+ // Check: do the tiers have the correct categories?
439
+ for (uint256 i = 0; i < pricesForTiersToAdd.length; i++) {
440
+ assertEq(allStoredTiers[i].category, uint8(102), "Wrong, first category (102).");
441
+ }
442
+
443
+ for (uint256 i = pricesForTiersToAdd.length; i < maxIndexForTier100; i++) {
444
+ assertEq(allStoredTiers[i].category, uint8(100), "Wrong, second category (100).");
445
+ }
446
+
447
+ for (uint256 i = maxIndexForTier100; i < allStoredTiers.length; i++) {
448
+ assertEq(allStoredTiers[i].category, uint8(101), "Wrong, third category (101).");
449
+ }
450
+ }
451
+
452
+ function test_adjustTiers_addNewTiers_fetchSpecificTier(
453
+ uint256 initialNumberOfTiers,
454
+ uint256 numberOfTiersToAdd,
455
+ uint256 seed
456
+ )
457
+ public
458
+ {
459
+ initialNumberOfTiers = bound(initialNumberOfTiers, 1, 14);
460
+
461
+ numberOfTiersToAdd = bound(numberOfTiersToAdd, 1, 14);
462
+ uint16[] memory pricesForTiersToAdd = _createArray(numberOfTiersToAdd, seed);
463
+
464
+ // Sort tiers in ascending order.
465
+ pricesForTiersToAdd = _sortArray(pricesForTiersToAdd);
466
+
467
+ // Initialize hook with default tiers from category 100.
468
+ defaultTierConfig.category = 100;
469
+ JB721TiersHook hook = _initHookDefaultTiers(initialNumberOfTiers);
470
+
471
+ // Create new tiers to add (with category 101).
472
+ defaultTierConfig.category = 101;
473
+ (JB721TierConfig[] memory tierConfigsToAdd,) =
474
+ _createTiers(defaultTierConfig, numberOfTiersToAdd, initialNumberOfTiers, pricesForTiersToAdd);
475
+
476
+ // Add the new tiers
477
+ uint256 tiersLeft = initialNumberOfTiers;
478
+ tiersLeft = _addDeleteTiers(hook, tiersLeft, 0, tierConfigsToAdd);
479
+
480
+ // Get the tiers from category 101.
481
+ uint256[] memory categories = new uint256[](1);
482
+ categories[0] = 101;
483
+ JB721Tier[] memory storedTiers = hook.STORE()
484
+ .tiersOf(address(hook), categories, false, 0, initialNumberOfTiers + pricesForTiersToAdd.length);
485
+
486
+ // Check: does the number of stored tiers match the number of tiers that were added?
487
+ assertEq(storedTiers.length, pricesForTiersToAdd.length);
488
+
489
+ // Check: do the tiers have category 101?
490
+ for (uint256 i = 0; i < storedTiers.length; i++) {
491
+ assertEq(storedTiers[i].category, uint8(101));
492
+ }
493
+ }
494
+
495
+ function test_adjustTiers_removeTiers(
496
+ uint256 initialNumberOfTiers,
497
+ uint256 seed,
498
+ uint256 numberOfTiersToRemove
499
+ )
500
+ public
501
+ {
502
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 14);
503
+ numberOfTiersToRemove = bound(numberOfTiersToRemove, 0, initialNumberOfTiers);
504
+
505
+ // Create random tiers to remove.
506
+ uint256[] memory tiersToRemove = new uint256[](numberOfTiersToRemove);
507
+
508
+ // Use the `seed` to generate new random tiers, and iterate on `i` to fill the `tiersToRemove` array.
509
+ for (uint256 i; i < numberOfTiersToRemove;) {
510
+ uint256 newTierCandidate = uint256(keccak256(abi.encode(seed))) % initialNumberOfTiers + 1;
511
+ bool invalidTier;
512
+ if (newTierCandidate != 0) {
513
+ for (uint256 j; j < numberOfTiersToRemove; j++) {
514
+ // Same value twice?
515
+ if (newTierCandidate == tiersToRemove[j]) {
516
+ invalidTier = true;
517
+ break;
518
+ }
519
+ }
520
+ if (!invalidTier) {
521
+ tiersToRemove[i] = newTierCandidate;
522
+ i++;
523
+ }
524
+ }
525
+ // Overflow to loop over (seed is fuzzed, and could start at `max(uint256)`).
526
+ unchecked {
527
+ seed++;
528
+ }
529
+ }
530
+
531
+ // Order the tiers to remove for event matching (which are ordered too).
532
+ tiersToRemove = _sortArray(tiersToRemove);
533
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
534
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
535
+
536
+ for (uint256 i; i < initialNumberOfTiers; i++) {
537
+ tierConfigs[i] = JB721TierConfig({
538
+ price: uint104((i + 1) * 10),
539
+ initialSupply: uint32(100),
540
+ votingUnits: uint16(0),
541
+ reserveFrequency: uint16(i),
542
+ reserveBeneficiary: reserveBeneficiary,
543
+ encodedIPFSUri: tokenUris[0],
544
+ category: uint24(100),
545
+ discountPercent: uint8(0),
546
+ allowOwnerMint: false,
547
+ useReserveBeneficiaryAsDefault: false,
548
+ transfersPausable: false,
549
+ useVotingUnits: true,
550
+ cannotBeRemoved: false,
551
+ cannotIncreaseDiscountPercent: false
552
+ });
553
+ tiers[i] = JB721Tier({
554
+ id: uint32(i + 1),
555
+ price: tierConfigs[i].price,
556
+ remainingSupply: tierConfigs[i].initialSupply,
557
+ initialSupply: tierConfigs[i].initialSupply,
558
+ votingUnits: tierConfigs[i].votingUnits,
559
+ reserveFrequency: tierConfigs[i].reserveFrequency,
560
+ reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary,
561
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
562
+ category: tierConfigs[i].category,
563
+ discountPercent: tierConfigs[i].discountPercent,
564
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
565
+ transfersPausable: tierConfigs[i].transfersPausable,
566
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
567
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
568
+ resolvedUri: ""
569
+ });
570
+ }
571
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
572
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
573
+ projectId,
574
+ IJBDirectory(mockJBDirectory),
575
+ name,
576
+ symbol,
577
+ IJBRulesets(mockJBRulesets),
578
+ baseUri,
579
+ IJB721TokenUriResolver(mockTokenUriResolver),
580
+ contractUri,
581
+ tierConfigs,
582
+ IJB721TiersHookStore(address(store)),
583
+ JB721TiersHookFlags({
584
+ preventOverspending: false,
585
+ noNewTiersWithReserves: false,
586
+ noNewTiersWithVotes: false,
587
+ noNewTiersWithOwnerMinting: true
588
+ })
589
+ );
590
+ hook.transferOwnership(owner);
591
+
592
+ // Will be resized later.
593
+ JB721TierConfig[] memory tierConfigsRemaining = new JB721TierConfig[](initialNumberOfTiers);
594
+ JB721Tier[] memory tiersRemaining = new JB721Tier[](initialNumberOfTiers);
595
+ for (uint256 i; i < tiers.length; i++) {
596
+ tierConfigsRemaining[i] = tierConfigs[i];
597
+ tiersRemaining[i] = tiers[i];
598
+ }
599
+
600
+ // Iterate through the remaining tiers and remove the ones in `tiersToRemove`.
601
+ // Do this by "swapping" the tier to remove with the last element in the array, and then "popping" that last
602
+ // element.
603
+ for (uint256 i; i < tiersRemaining.length;) {
604
+ bool swappedAndPopped;
605
+ for (uint256 j; j < tiersToRemove.length; j++) {
606
+ if (tiersRemaining[i].id == tiersToRemove[j]) {
607
+ // Swap and pop removed tiers.
608
+ tiersRemaining[i] = tiersRemaining[tiersRemaining.length - 1];
609
+ tierConfigsRemaining[i] = tierConfigsRemaining[tierConfigsRemaining.length - 1];
610
+ // Remove the last elelment / reduce array length by 1.
611
+ assembly ("memory-safe") {
612
+ mstore(tiersRemaining, sub(mload(tiersRemaining), 1))
613
+ mstore(tierConfigsRemaining, sub(mload(tierConfigsRemaining), 1))
614
+ }
615
+ swappedAndPopped = true;
616
+ break;
617
+ }
618
+ }
619
+ if (!swappedAndPopped) i++;
620
+ }
621
+
622
+ // Check: was `RemoveTier` emitted with the correct values?
623
+ for (uint256 i; i < tiersToRemove.length; i++) {
624
+ vm.expectEmit(true, false, false, true, address(hook));
625
+ emit RemoveTier(tiersToRemove[i], owner);
626
+ }
627
+ vm.prank(owner);
628
+ hook.adjustTiers(new JB721TierConfig[](0), tiersToRemove);
629
+ {
630
+ uint256 finalNumberOfTiers = initialNumberOfTiers - tiersToRemove.length;
631
+ JB721Tier[] memory storedTiers =
632
+ hook.test_store().tiersOf(address(hook), new uint256[](0), false, 0, finalNumberOfTiers);
633
+ // Check: are the expected number of tiers stored?
634
+ assertEq(storedTiers.length, finalNumberOfTiers);
635
+ // Check: are all of the remaining tiers still stored?
636
+ assertTrue(_isIn(tiersRemaining, storedTiers));
637
+ // Check: have all the removed tiers tiers been removed from storage?
638
+ assertTrue(_isIn(storedTiers, tiersRemaining));
639
+ }
640
+ }
641
+
642
+ function test_adjustTiers_addAndRemoveTiers() public {
643
+ uint256 initialNumberOfTiers = 5;
644
+ uint256 numberOfTiersToAdd = 5;
645
+ uint256 numberOfTiersToRemove = 3;
646
+ uint256[] memory tiersToAdd = new uint256[](numberOfTiersToAdd);
647
+ tiersToAdd[0] = 1;
648
+ tiersToAdd[1] = 4;
649
+ tiersToAdd[2] = 5;
650
+ tiersToAdd[3] = 6;
651
+ tiersToAdd[4] = 10;
652
+ uint256[] memory tierIdsToRemove = new uint256[](numberOfTiersToRemove);
653
+ tierIdsToRemove[0] = 1;
654
+ tierIdsToRemove[1] = 3;
655
+ tierIdsToRemove[2] = 4;
656
+ // Initial tiers configs and data.
657
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
658
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
659
+ for (uint256 i; i < initialNumberOfTiers; i++) {
660
+ tierConfigs[i] = JB721TierConfig({
661
+ price: uint104((i + 1) * 10),
662
+ initialSupply: uint32(100),
663
+ votingUnits: uint16(0),
664
+ reserveFrequency: uint16(i),
665
+ reserveBeneficiary: reserveBeneficiary,
666
+ encodedIPFSUri: tokenUris[0],
667
+ category: uint24(100),
668
+ discountPercent: uint8(0),
669
+ allowOwnerMint: false,
670
+ useReserveBeneficiaryAsDefault: false,
671
+ transfersPausable: false,
672
+ useVotingUnits: true,
673
+ cannotBeRemoved: false,
674
+ cannotIncreaseDiscountPercent: false
675
+ });
676
+ tiers[i] = JB721Tier({
677
+ id: uint32(i + 1),
678
+ price: tierConfigs[i].price,
679
+ remainingSupply: tierConfigs[i].initialSupply,
680
+ initialSupply: tierConfigs[i].initialSupply,
681
+ votingUnits: tierConfigs[i].votingUnits,
682
+ reserveFrequency: tierConfigs[i].reserveFrequency,
683
+ reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary,
684
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
685
+ category: tierConfigs[i].category,
686
+ discountPercent: tierConfigs[i].discountPercent,
687
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
688
+ transfersPausable: tierConfigs[i].transfersPausable,
689
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
690
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
691
+ resolvedUri: ""
692
+ });
693
+ }
694
+ // Deploy the hook and its store with the initial tiers.
695
+ vm.etch(hook_i, address(hook).code);
696
+ JB721TiersHook hook = JB721TiersHook(hook_i);
697
+ hook.initialize(
698
+ projectId,
699
+ name,
700
+ symbol,
701
+ baseUri,
702
+ IJB721TokenUriResolver(mockTokenUriResolver),
703
+ contractUri,
704
+ JB721InitTiersConfig({
705
+ tiers: tierConfigs,
706
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
707
+ decimals: 18,
708
+ prices: IJBPrices(address(0))
709
+ }),
710
+ JB721TiersHookFlags({
711
+ preventOverspending: false,
712
+ noNewTiersWithReserves: false,
713
+ noNewTiersWithVotes: false,
714
+ noNewTiersWithOwnerMinting: true
715
+ })
716
+ );
717
+ hook.transferOwnership(owner);
718
+
719
+ // -- Build expected removed/remaining tiers -- //
720
+ JB721TierConfig[] memory tierConfigsRemaining = new JB721TierConfig[](2);
721
+ JB721Tier[] memory tiersRemaining = new JB721Tier[](2);
722
+ uint256 arrayIndex;
723
+ for (uint256 i; i < initialNumberOfTiers; i++) {
724
+ // Tiers which will remain.
725
+ if (i + 1 != 1 && i + 1 != 3 && i + 1 != 4) {
726
+ tierConfigsRemaining[arrayIndex] = JB721TierConfig({
727
+ price: uint104((i + 1) * 10),
728
+ initialSupply: uint32(100),
729
+ votingUnits: uint16(0),
730
+ reserveFrequency: uint16(i),
731
+ reserveBeneficiary: reserveBeneficiary,
732
+ encodedIPFSUri: tokenUris[0],
733
+ category: uint24(100),
734
+ discountPercent: uint8(0),
735
+ allowOwnerMint: false,
736
+ useReserveBeneficiaryAsDefault: false,
737
+ transfersPausable: false,
738
+ useVotingUnits: true,
739
+ cannotBeRemoved: false,
740
+ cannotIncreaseDiscountPercent: false
741
+ });
742
+ tiersRemaining[arrayIndex] = JB721Tier({
743
+ id: uint32(i + 1),
744
+ price: tierConfigsRemaining[arrayIndex].price,
745
+ remainingSupply: tierConfigsRemaining[arrayIndex].initialSupply,
746
+ initialSupply: tierConfigsRemaining[arrayIndex].initialSupply,
747
+ votingUnits: tierConfigsRemaining[arrayIndex].votingUnits,
748
+ reserveFrequency: tierConfigsRemaining[arrayIndex].reserveFrequency,
749
+ reserveBeneficiary: i == 0 ? address(0) : tierConfigsRemaining[arrayIndex].reserveBeneficiary,
750
+ encodedIPFSUri: tierConfigsRemaining[arrayIndex].encodedIPFSUri,
751
+ category: tierConfigsRemaining[arrayIndex].category,
752
+ discountPercent: tierConfigsRemaining[arrayIndex].discountPercent,
753
+ allowOwnerMint: tierConfigsRemaining[arrayIndex].allowOwnerMint,
754
+ transfersPausable: tierConfigsRemaining[arrayIndex].transfersPausable,
755
+ cannotBeRemoved: tierConfigsRemaining[arrayIndex].cannotBeRemoved,
756
+ cannotIncreaseDiscountPercent: tierConfigsRemaining[arrayIndex].cannotIncreaseDiscountPercent,
757
+ resolvedUri: ""
758
+ });
759
+ arrayIndex++;
760
+ } else {
761
+ // Otherwise, part of the tiers removed:
762
+ // Check: was `RemoveTier` emitted with the correct values?
763
+ vm.expectEmit(true, false, false, true, address(hook));
764
+ emit RemoveTier(i + 1, owner);
765
+ }
766
+ }
767
+
768
+ // -- Build expected added tiers -- //
769
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberOfTiersToAdd);
770
+ JB721Tier[] memory tiersAdded = new JB721Tier[](numberOfTiersToAdd);
771
+ for (uint256 i; i < numberOfTiersToAdd; i++) {
772
+ tierConfigsToAdd[i] = JB721TierConfig({
773
+ price: uint104(tiersToAdd[i]) * 11,
774
+ initialSupply: uint32(100),
775
+ votingUnits: uint16(0),
776
+ reserveFrequency: uint16(0),
777
+ reserveBeneficiary: reserveBeneficiary,
778
+ encodedIPFSUri: tokenUris[0],
779
+ category: uint24(100 + i),
780
+ discountPercent: uint8(0),
781
+ allowOwnerMint: false,
782
+ useReserveBeneficiaryAsDefault: false,
783
+ transfersPausable: false,
784
+ useVotingUnits: true,
785
+ cannotBeRemoved: false,
786
+ cannotIncreaseDiscountPercent: false
787
+ });
788
+ tiersAdded[i] = JB721Tier({
789
+ id: uint32(tiers.length + (i + 1)),
790
+ price: tierConfigsToAdd[i].price,
791
+ remainingSupply: tierConfigsToAdd[i].initialSupply,
792
+ initialSupply: tierConfigsToAdd[i].initialSupply,
793
+ votingUnits: tierConfigsToAdd[i].votingUnits,
794
+ reserveFrequency: tierConfigsToAdd[i].reserveFrequency,
795
+ reserveBeneficiary: address(0),
796
+ encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri,
797
+ category: tierConfigsToAdd[i].category,
798
+ discountPercent: tierConfigsToAdd[i].discountPercent,
799
+ allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint,
800
+ transfersPausable: tierConfigsToAdd[i].transfersPausable,
801
+ cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved,
802
+ cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent,
803
+ resolvedUri: ""
804
+ });
805
+ vm.expectEmit(true, true, true, true, address(hook));
806
+ emit AddTier(tiersAdded[i].id, tierConfigsToAdd[i], owner);
807
+ }
808
+ vm.prank(owner);
809
+ hook.adjustTiers(tierConfigsToAdd, tierIdsToRemove);
810
+ JB721Tier[] memory storedTiers = hook.STORE()
811
+ .tiersOf(
812
+ address(hook),
813
+ new uint256[](0),
814
+ false,
815
+ 0,
816
+ 7 // 7 tiers remaining - hard-coded to avoid stack too deep.
817
+ );
818
+ // Check: are the expected number of tiers stored?
819
+ assertEq(storedTiers.length, 7);
820
+ // Check: are all non-deleted and added tiers in the new tiers (unsorted)?
821
+ assertTrue(_isIn(tiersRemaining, storedTiers)); // Original tiers
822
+ assertTrue(_isIn(tiersAdded, storedTiers)); // New tiers
823
+ // Check: are all the deleted tiers removed?
824
+ assertFalse(_isIn(tiers, storedTiers)); // Will emit `_isIn: incomplete inclusion but without failing assertion`
825
+ // Check: are all the tiers sorted?
826
+ for (uint256 j = 1; j < storedTiers.length; j++) {
827
+ assertLe(storedTiers[j - 1].category, storedTiers[j].category);
828
+ }
829
+ }
830
+
831
+ function test_adjustTiers_revertIfAddingWithVotingPower(
832
+ uint256 initialNumberOfTiers,
833
+ uint256 numberTiersToAdd
834
+ )
835
+ public
836
+ {
837
+ // Include adding X new tiers with 0 current tiers.
838
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
839
+ numberTiersToAdd = bound(numberTiersToAdd, 1, 15);
840
+
841
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
842
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
843
+ for (uint256 i; i < initialNumberOfTiers; i++) {
844
+ tierConfigs[i] = JB721TierConfig({
845
+ price: uint104((i + 1) * 10),
846
+ initialSupply: uint32(100),
847
+ votingUnits: uint16(0),
848
+ reserveFrequency: uint16(i),
849
+ reserveBeneficiary: reserveBeneficiary,
850
+ encodedIPFSUri: tokenUris[0],
851
+ category: uint24(100),
852
+ discountPercent: uint8(0),
853
+ allowOwnerMint: false,
854
+ useReserveBeneficiaryAsDefault: false,
855
+ transfersPausable: false,
856
+ useVotingUnits: true,
857
+ cannotBeRemoved: false,
858
+ cannotIncreaseDiscountPercent: false
859
+ });
860
+ tiers[i] = JB721Tier({
861
+ id: uint32(i + 1),
862
+ price: tierConfigs[i].price,
863
+ remainingSupply: tierConfigs[i].initialSupply,
864
+ initialSupply: tierConfigs[i].initialSupply,
865
+ votingUnits: tierConfigs[i].votingUnits,
866
+ reserveFrequency: tierConfigs[i].reserveFrequency,
867
+ reserveBeneficiary: tierConfigs[i].reserveBeneficiary,
868
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
869
+ category: tierConfigs[i].category,
870
+ discountPercent: tierConfigs[i].discountPercent,
871
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
872
+ transfersPausable: tierConfigs[i].transfersPausable,
873
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
874
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
875
+ resolvedUri: ""
876
+ });
877
+ }
878
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
879
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
880
+ projectId,
881
+ IJBDirectory(mockJBDirectory),
882
+ name,
883
+ symbol,
884
+ IJBRulesets(mockJBRulesets),
885
+ baseUri,
886
+ IJB721TokenUriResolver(mockTokenUriResolver),
887
+ contractUri,
888
+ tierConfigs,
889
+ IJB721TiersHookStore(address(store)),
890
+ JB721TiersHookFlags({
891
+ preventOverspending: false,
892
+ noNewTiersWithReserves: false,
893
+ noNewTiersWithVotes: true,
894
+ noNewTiersWithOwnerMinting: true
895
+ })
896
+ );
897
+ hook.transferOwnership(owner);
898
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
899
+ JB721Tier[] memory tiersAdded = new JB721Tier[](numberTiersToAdd);
900
+ for (uint256 i; i < numberTiersToAdd; i++) {
901
+ tierConfigsToAdd[i] = JB721TierConfig({
902
+ price: uint104(0),
903
+ initialSupply: uint32(100),
904
+ votingUnits: uint16(i + 1),
905
+ reserveFrequency: uint16(i),
906
+ reserveBeneficiary: reserveBeneficiary,
907
+ encodedIPFSUri: tokenUris[0],
908
+ category: uint24(100),
909
+ discountPercent: uint8(0),
910
+ allowOwnerMint: false,
911
+ useReserveBeneficiaryAsDefault: false,
912
+ transfersPausable: false,
913
+ useVotingUnits: true,
914
+ cannotBeRemoved: false,
915
+ cannotIncreaseDiscountPercent: false
916
+ });
917
+ tiersAdded[i] = JB721Tier({
918
+ id: uint32(tiers.length + (i + 1)),
919
+ price: tierConfigsToAdd[i].price,
920
+ remainingSupply: tierConfigsToAdd[i].initialSupply,
921
+ initialSupply: tierConfigsToAdd[i].initialSupply,
922
+ votingUnits: tierConfigsToAdd[i].votingUnits,
923
+ reserveFrequency: tierConfigsToAdd[i].reserveFrequency,
924
+ reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary,
925
+ encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri,
926
+ category: tierConfigsToAdd[i].category,
927
+ discountPercent: tierConfigsToAdd[i].discountPercent,
928
+ allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint,
929
+ transfersPausable: tierConfigsToAdd[i].transfersPausable,
930
+ cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved,
931
+ cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent,
932
+ resolvedUri: ""
933
+ });
934
+ }
935
+ vm.expectRevert(
936
+ abi.encodeWithSelector(
937
+ JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector, initialNumberOfTiers + 1
938
+ )
939
+ );
940
+ vm.prank(owner);
941
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
942
+ }
943
+
944
+ function test_adjustTiers_revertIfAddingWithReserveFrequency(
945
+ uint256 initialNumberOfTiers,
946
+ uint256 numberTiersToAdd
947
+ )
948
+ public
949
+ {
950
+ // Include adding X new tiers with 0 current tiers.
951
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
952
+ numberTiersToAdd = bound(numberTiersToAdd, 1, 15);
953
+
954
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
955
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
956
+ for (uint256 i; i < initialNumberOfTiers; i++) {
957
+ tierConfigs[i] = JB721TierConfig({
958
+ price: uint104((i + 1) * 10),
959
+ initialSupply: uint32(100),
960
+ votingUnits: uint16(0),
961
+ reserveFrequency: uint16(i),
962
+ reserveBeneficiary: reserveBeneficiary,
963
+ encodedIPFSUri: tokenUris[0],
964
+ category: uint24(100),
965
+ discountPercent: uint8(0),
966
+ allowOwnerMint: false,
967
+ useReserveBeneficiaryAsDefault: false,
968
+ transfersPausable: false,
969
+ useVotingUnits: true,
970
+ cannotBeRemoved: false,
971
+ cannotIncreaseDiscountPercent: false
972
+ });
973
+ tiers[i] = JB721Tier({
974
+ id: uint32(i + 1),
975
+ price: tierConfigs[i].price,
976
+ remainingSupply: tierConfigs[i].initialSupply,
977
+ initialSupply: tierConfigs[i].initialSupply,
978
+ votingUnits: tierConfigs[i].votingUnits,
979
+ reserveFrequency: tierConfigs[i].reserveFrequency,
980
+ reserveBeneficiary: tierConfigs[i].reserveBeneficiary,
981
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
982
+ category: tierConfigs[i].category,
983
+ discountPercent: tierConfigs[i].discountPercent,
984
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
985
+ transfersPausable: tierConfigs[i].transfersPausable,
986
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
987
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
988
+ resolvedUri: ""
989
+ });
990
+ }
991
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
992
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
993
+ projectId,
994
+ IJBDirectory(mockJBDirectory),
995
+ name,
996
+ symbol,
997
+ IJBRulesets(mockJBRulesets),
998
+ baseUri,
999
+ IJB721TokenUriResolver(mockTokenUriResolver),
1000
+ contractUri,
1001
+ tierConfigs,
1002
+ IJB721TiersHookStore(address(store)),
1003
+ JB721TiersHookFlags({
1004
+ preventOverspending: false,
1005
+ noNewTiersWithReserves: true, // <-- This is the flag we're testing.
1006
+ noNewTiersWithVotes: false,
1007
+ noNewTiersWithOwnerMinting: true
1008
+ })
1009
+ );
1010
+ hook.transferOwnership(owner);
1011
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
1012
+ JB721Tier[] memory tiersAdded = new JB721Tier[](numberTiersToAdd);
1013
+ for (uint256 i; i < numberTiersToAdd; i++) {
1014
+ tierConfigsToAdd[i] = JB721TierConfig({
1015
+ price: uint104((i + 1) * 100),
1016
+ initialSupply: uint32(100),
1017
+ votingUnits: uint16(0),
1018
+ reserveFrequency: uint16(i + 1),
1019
+ reserveBeneficiary: reserveBeneficiary,
1020
+ encodedIPFSUri: tokenUris[0],
1021
+ category: uint24(100),
1022
+ discountPercent: uint8(0),
1023
+ allowOwnerMint: false,
1024
+ useReserveBeneficiaryAsDefault: false,
1025
+ transfersPausable: false,
1026
+ useVotingUnits: true,
1027
+ cannotBeRemoved: false,
1028
+ cannotIncreaseDiscountPercent: false
1029
+ });
1030
+ tiersAdded[i] = JB721Tier({
1031
+ id: uint32(tiers.length + (i + 1)),
1032
+ price: tierConfigsToAdd[i].price,
1033
+ remainingSupply: tierConfigsToAdd[i].initialSupply,
1034
+ initialSupply: tierConfigsToAdd[i].initialSupply,
1035
+ votingUnits: tierConfigsToAdd[i].votingUnits,
1036
+ reserveFrequency: tierConfigsToAdd[i].reserveFrequency,
1037
+ reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary,
1038
+ encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri,
1039
+ category: tierConfigsToAdd[i].category,
1040
+ discountPercent: tierConfigsToAdd[i].discountPercent,
1041
+ allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint,
1042
+ transfersPausable: tierConfigsToAdd[i].transfersPausable,
1043
+ cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved,
1044
+ cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent,
1045
+ resolvedUri: ""
1046
+ });
1047
+ }
1048
+
1049
+ // Expect the `adjustTiers` call to revert because of the `noNewTiersWithReserves` flag.
1050
+ vm.expectRevert(
1051
+ abi.encodeWithSelector(
1052
+ JB721TiersHookStore.JB721TiersHookStore_ReserveFrequencyNotAllowed.selector, initialNumberOfTiers + 1
1053
+ )
1054
+ );
1055
+ vm.prank(owner);
1056
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
1057
+ }
1058
+
1059
+ function test_adjustTiers_revertIfCannotRemoveTier() public {
1060
+ uint256 initialNumberOfTiers = 2;
1061
+ uint256 numberOfTiersToRemove = 1;
1062
+ uint256[] memory tierIdsToRemove = new uint256[](numberOfTiersToRemove);
1063
+ tierIdsToRemove[0] = 1;
1064
+ // Initial tiers configs and data.
1065
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
1066
+ tierConfigs[0] = JB721TierConfig({
1067
+ price: 10,
1068
+ initialSupply: uint32(100),
1069
+ votingUnits: uint16(0),
1070
+ reserveFrequency: uint16(0),
1071
+ reserveBeneficiary: reserveBeneficiary,
1072
+ encodedIPFSUri: tokenUris[0],
1073
+ category: uint24(100),
1074
+ discountPercent: uint8(0),
1075
+ allowOwnerMint: false,
1076
+ useReserveBeneficiaryAsDefault: false,
1077
+ transfersPausable: false,
1078
+ useVotingUnits: true,
1079
+ cannotBeRemoved: true,
1080
+ cannotIncreaseDiscountPercent: false
1081
+ });
1082
+ tierConfigs[1] = JB721TierConfig({
1083
+ price: 10,
1084
+ initialSupply: uint32(100),
1085
+ votingUnits: uint16(0),
1086
+ reserveFrequency: uint16(0),
1087
+ reserveBeneficiary: reserveBeneficiary,
1088
+ encodedIPFSUri: tokenUris[0],
1089
+ category: uint24(100),
1090
+ discountPercent: uint8(0),
1091
+ allowOwnerMint: false,
1092
+ useReserveBeneficiaryAsDefault: false,
1093
+ transfersPausable: false,
1094
+ useVotingUnits: true,
1095
+ cannotBeRemoved: false,
1096
+ cannotIncreaseDiscountPercent: false
1097
+ });
1098
+ // Deploy the hook and its store with the initial tiers.
1099
+ vm.etch(hook_i, address(hook).code);
1100
+ JB721TiersHook hook = JB721TiersHook(hook_i);
1101
+ hook.initialize(
1102
+ projectId,
1103
+ name,
1104
+ symbol,
1105
+ baseUri,
1106
+ IJB721TokenUriResolver(mockTokenUriResolver),
1107
+ contractUri,
1108
+ JB721InitTiersConfig({
1109
+ tiers: tierConfigs,
1110
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1111
+ decimals: 18,
1112
+ prices: IJBPrices(address(0))
1113
+ }),
1114
+ JB721TiersHookFlags({
1115
+ preventOverspending: false,
1116
+ noNewTiersWithReserves: false,
1117
+ noNewTiersWithVotes: false,
1118
+ noNewTiersWithOwnerMinting: true
1119
+ })
1120
+ );
1121
+ hook.transferOwnership(owner);
1122
+
1123
+ // Expect the `adjustTiers` call to revert because cannot remove tier.
1124
+ vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_CantRemoveTier.selector, 1));
1125
+ vm.prank(owner);
1126
+ hook.adjustTiers(new JB721TierConfig[](0), tierIdsToRemove);
1127
+ }
1128
+
1129
+ function test_adjustTiers_revertIfEmptyQuantity(uint256 initialNumberOfTiers, uint256 numberTiersToAdd) public {
1130
+ // Include adding X new tiers with 0 current tiers.
1131
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
1132
+ numberTiersToAdd = bound(numberTiersToAdd, 1, 15);
1133
+
1134
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
1135
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
1136
+ for (uint256 i; i < initialNumberOfTiers; i++) {
1137
+ tierConfigs[i] = JB721TierConfig({
1138
+ price: uint104((i + 1) * 10),
1139
+ initialSupply: uint32(100),
1140
+ votingUnits: uint16(0),
1141
+ reserveFrequency: uint16(i),
1142
+ reserveBeneficiary: reserveBeneficiary,
1143
+ encodedIPFSUri: tokenUris[0],
1144
+ category: uint24(100),
1145
+ discountPercent: uint8(0),
1146
+ allowOwnerMint: false,
1147
+ useReserveBeneficiaryAsDefault: false,
1148
+ transfersPausable: false,
1149
+ useVotingUnits: true,
1150
+ cannotBeRemoved: false,
1151
+ cannotIncreaseDiscountPercent: false
1152
+ });
1153
+ tiers[i] = JB721Tier({
1154
+ id: uint32(i + 1),
1155
+ price: tierConfigs[i].price,
1156
+ remainingSupply: tierConfigs[i].initialSupply,
1157
+ initialSupply: tierConfigs[i].initialSupply,
1158
+ votingUnits: tierConfigs[i].votingUnits,
1159
+ reserveFrequency: tierConfigs[i].reserveFrequency,
1160
+ reserveBeneficiary: tierConfigs[i].reserveBeneficiary,
1161
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
1162
+ category: tierConfigs[i].category,
1163
+ discountPercent: tierConfigs[i].discountPercent,
1164
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
1165
+ transfersPausable: tierConfigs[i].transfersPausable,
1166
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
1167
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
1168
+ resolvedUri: ""
1169
+ });
1170
+ }
1171
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
1172
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
1173
+ projectId,
1174
+ IJBDirectory(mockJBDirectory),
1175
+ name,
1176
+ symbol,
1177
+ IJBRulesets(mockJBRulesets),
1178
+ baseUri,
1179
+ IJB721TokenUriResolver(mockTokenUriResolver),
1180
+ contractUri,
1181
+ tierConfigs,
1182
+ IJB721TiersHookStore(address(store)),
1183
+ JB721TiersHookFlags({
1184
+ preventOverspending: false,
1185
+ noNewTiersWithReserves: false,
1186
+ noNewTiersWithVotes: false,
1187
+ noNewTiersWithOwnerMinting: true
1188
+ })
1189
+ );
1190
+ hook.transferOwnership(owner);
1191
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
1192
+ JB721Tier[] memory tiersAdded = new JB721Tier[](numberTiersToAdd);
1193
+ for (uint256 i; i < numberTiersToAdd; i++) {
1194
+ tierConfigsToAdd[i] = JB721TierConfig({
1195
+ price: uint104((i + 1) * 100),
1196
+ initialSupply: uint32(0), // <-- This is the value we're testing.
1197
+ votingUnits: uint16(0),
1198
+ reserveFrequency: uint16(0),
1199
+ reserveBeneficiary: reserveBeneficiary,
1200
+ encodedIPFSUri: tokenUris[0],
1201
+ category: uint24(100),
1202
+ discountPercent: uint8(0),
1203
+ allowOwnerMint: false,
1204
+ useReserveBeneficiaryAsDefault: false,
1205
+ transfersPausable: false,
1206
+ useVotingUnits: false,
1207
+ cannotBeRemoved: false,
1208
+ cannotIncreaseDiscountPercent: false
1209
+ });
1210
+ tiersAdded[i] = JB721Tier({
1211
+ id: uint32(tiers.length + (i + 1)),
1212
+ price: tierConfigsToAdd[i].price,
1213
+ remainingSupply: tierConfigsToAdd[i].initialSupply,
1214
+ initialSupply: tierConfigsToAdd[i].initialSupply,
1215
+ votingUnits: tierConfigsToAdd[i].votingUnits,
1216
+ reserveFrequency: tierConfigsToAdd[i].reserveFrequency,
1217
+ reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary,
1218
+ encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri,
1219
+ category: tierConfigsToAdd[i].category,
1220
+ discountPercent: tierConfigsToAdd[i].discountPercent,
1221
+ allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint,
1222
+ transfersPausable: tierConfigsToAdd[i].transfersPausable,
1223
+ cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved,
1224
+ cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent,
1225
+ resolvedUri: ""
1226
+ });
1227
+ }
1228
+
1229
+ // Expect the `adjustTiers` call to revert because of the `initialSupply` of 0.
1230
+ vm.expectRevert(
1231
+ abi.encodeWithSelector(
1232
+ JB721TiersHookStore.JB721TiersHookStore_ZeroInitialSupply.selector, initialNumberOfTiers + 1
1233
+ )
1234
+ );
1235
+ vm.prank(owner);
1236
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
1237
+ }
1238
+
1239
+ function test_adjustTiers_revertIfInvalidCategorySortOrder(
1240
+ uint256 initialNumberOfTiers,
1241
+ uint256 numberTiersToAdd
1242
+ )
1243
+ public
1244
+ {
1245
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
1246
+ numberTiersToAdd = bound(numberTiersToAdd, 2, 15);
1247
+
1248
+ ForTest_JB721TiersHook hook = _initializeForTestHook(initialNumberOfTiers);
1249
+
1250
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
1251
+ for (uint256 i; i < numberTiersToAdd; i++) {
1252
+ tierConfigsToAdd[i] = JB721TierConfig({
1253
+ price: uint104((i + 1) * 100),
1254
+ initialSupply: uint32(100),
1255
+ votingUnits: uint16(0),
1256
+ reserveFrequency: uint16(i),
1257
+ reserveBeneficiary: reserveBeneficiary,
1258
+ encodedIPFSUri: tokenUris[0],
1259
+ category: uint24(100),
1260
+ discountPercent: uint8(0),
1261
+ allowOwnerMint: false,
1262
+ useReserveBeneficiaryAsDefault: false,
1263
+ transfersPausable: false,
1264
+ cannotBeRemoved: false,
1265
+ useVotingUnits: true,
1266
+ cannotIncreaseDiscountPercent: false
1267
+ });
1268
+ }
1269
+ // Set the second to last tier to have a category of `99`, which is less than the last tier's category of `100`.
1270
+ tierConfigsToAdd[numberTiersToAdd - 1].category = uint8(99);
1271
+
1272
+ // Expect the `adjustTiers` call to revert because of the invalid category sort order.
1273
+ vm.expectRevert(
1274
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InvalidCategorySortOrder.selector, 99, 100)
1275
+ );
1276
+ vm.prank(owner);
1277
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
1278
+ }
1279
+
1280
+ function test_adjustTiers_revertIfMoreVotingUnitsNotAllowedWithPriceChange(
1281
+ uint256 initialNumberOfTiers,
1282
+ uint256 numberTiersToAdd
1283
+ )
1284
+ public
1285
+ {
1286
+ // Include adding X new tiers with 0 current tiers.
1287
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
1288
+ numberTiersToAdd = bound(numberTiersToAdd, 1, 15);
1289
+
1290
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
1291
+ for (uint256 i; i < initialNumberOfTiers; i++) {
1292
+ tierConfigs[i] = JB721TierConfig({
1293
+ price: uint104((i + 1) * 10),
1294
+ initialSupply: uint32(100),
1295
+ votingUnits: uint16(0),
1296
+ reserveFrequency: uint16(i),
1297
+ reserveBeneficiary: reserveBeneficiary,
1298
+ encodedIPFSUri: tokenUris[0],
1299
+ category: uint24(100),
1300
+ discountPercent: uint8(0),
1301
+ allowOwnerMint: false,
1302
+ useReserveBeneficiaryAsDefault: false,
1303
+ transfersPausable: false,
1304
+ useVotingUnits: false,
1305
+ cannotBeRemoved: false,
1306
+ cannotIncreaseDiscountPercent: false
1307
+ });
1308
+ }
1309
+
1310
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
1311
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
1312
+ projectId,
1313
+ IJBDirectory(mockJBDirectory),
1314
+ name,
1315
+ symbol,
1316
+ IJBRulesets(mockJBRulesets),
1317
+ baseUri,
1318
+ IJB721TokenUriResolver(mockTokenUriResolver),
1319
+ contractUri,
1320
+ tierConfigs,
1321
+ IJB721TiersHookStore(address(store)),
1322
+ JB721TiersHookFlags({
1323
+ preventOverspending: false,
1324
+ noNewTiersWithReserves: false,
1325
+ noNewTiersWithVotes: true, // <-- This is the flag we're testing.
1326
+ noNewTiersWithOwnerMinting: true
1327
+ })
1328
+ );
1329
+ hook.transferOwnership(owner);
1330
+
1331
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
1332
+ for (uint256 i; i < numberTiersToAdd; i++) {
1333
+ tierConfigsToAdd[i] = JB721TierConfig({
1334
+ price: uint104((i + 1) * 100),
1335
+ initialSupply: uint32(100),
1336
+ votingUnits: uint16(0),
1337
+ reserveFrequency: uint16(i),
1338
+ reserveBeneficiary: reserveBeneficiary,
1339
+ encodedIPFSUri: tokenUris[0],
1340
+ category: uint24(100),
1341
+ discountPercent: uint8(0),
1342
+ allowOwnerMint: false,
1343
+ useReserveBeneficiaryAsDefault: false,
1344
+ transfersPausable: false,
1345
+ useVotingUnits: false, // <-- If false, voting power is based on tier price
1346
+ cannotBeRemoved: false,
1347
+ cannotIncreaseDiscountPercent: false
1348
+ });
1349
+ }
1350
+
1351
+ // Expect the `adjustTiers` call to revert because of the `noNewTiersWithVotes` flag.
1352
+ vm.expectRevert(
1353
+ abi.encodeWithSelector(
1354
+ JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector, initialNumberOfTiers + 1
1355
+ )
1356
+ );
1357
+ vm.prank(owner);
1358
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
1359
+ }
1360
+
1361
+ function test_adjustTiers_revertIfMoreVotingUnitsNotAllowedUsingVotingUnits(
1362
+ uint256 initialNumberOfTiers,
1363
+ uint256 numberTiersToAdd
1364
+ )
1365
+ public
1366
+ {
1367
+ // Include adding X new tiers with 0 current tiers.
1368
+ initialNumberOfTiers = bound(initialNumberOfTiers, 0, 15);
1369
+ numberTiersToAdd = bound(numberTiersToAdd, 1, 15);
1370
+
1371
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
1372
+ for (uint256 i; i < initialNumberOfTiers; i++) {
1373
+ tierConfigs[i] = JB721TierConfig({
1374
+ price: uint104((i + 1) * 10),
1375
+ initialSupply: uint32(100),
1376
+ votingUnits: uint16(0),
1377
+ reserveFrequency: uint16(i),
1378
+ reserveBeneficiary: reserveBeneficiary,
1379
+ encodedIPFSUri: tokenUris[0],
1380
+ category: uint24(100),
1381
+ discountPercent: uint8(0),
1382
+ allowOwnerMint: false,
1383
+ useReserveBeneficiaryAsDefault: false,
1384
+ transfersPausable: false,
1385
+ useVotingUnits: true,
1386
+ cannotBeRemoved: false,
1387
+ cannotIncreaseDiscountPercent: false
1388
+ });
1389
+ }
1390
+
1391
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
1392
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
1393
+ projectId,
1394
+ IJBDirectory(mockJBDirectory),
1395
+ name,
1396
+ symbol,
1397
+ IJBRulesets(mockJBRulesets),
1398
+ baseUri,
1399
+ IJB721TokenUriResolver(mockTokenUriResolver),
1400
+ contractUri,
1401
+ tierConfigs,
1402
+ IJB721TiersHookStore(address(store)),
1403
+ JB721TiersHookFlags({
1404
+ preventOverspending: false,
1405
+ noNewTiersWithReserves: false,
1406
+ noNewTiersWithVotes: true, // <-- This is the flag we're testing.
1407
+ noNewTiersWithOwnerMinting: true
1408
+ })
1409
+ );
1410
+ hook.transferOwnership(owner);
1411
+
1412
+ JB721TierConfig[] memory tierConfigsToAdd = new JB721TierConfig[](numberTiersToAdd);
1413
+ for (uint256 i; i < numberTiersToAdd; i++) {
1414
+ tierConfigsToAdd[i] = JB721TierConfig({
1415
+ price: uint104((i + 1) * 100),
1416
+ initialSupply: uint32(100),
1417
+ votingUnits: uint16(0),
1418
+ reserveFrequency: uint16(i),
1419
+ reserveBeneficiary: reserveBeneficiary,
1420
+ encodedIPFSUri: tokenUris[0],
1421
+ category: uint24(100),
1422
+ discountPercent: uint8(0),
1423
+ allowOwnerMint: false,
1424
+ useReserveBeneficiaryAsDefault: false,
1425
+ transfersPausable: false,
1426
+ useVotingUnits: true, // <-- If false, voting power is based on tier price
1427
+ cannotBeRemoved: false,
1428
+ cannotIncreaseDiscountPercent: false
1429
+ });
1430
+ }
1431
+
1432
+ // One new tier has a non-0 voting power
1433
+ tierConfigsToAdd[numberTiersToAdd - 1].votingUnits = uint16(1);
1434
+
1435
+ // Expect the `adjustTiers` call to revert because of the `noNewTiersWithVotes` flag.
1436
+ vm.expectRevert(
1437
+ abi.encodeWithSelector(
1438
+ JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector,
1439
+ initialNumberOfTiers + numberTiersToAdd
1440
+ )
1441
+ );
1442
+ vm.prank(owner);
1443
+ hook.adjustTiers(tierConfigsToAdd, new uint256[](0));
1444
+ }
1445
+
1446
+ function test_cleanTiers_removeInactiveTiers(
1447
+ uint256 initialNumberOfTiers,
1448
+ uint256 seed,
1449
+ uint256 numberOfTiersToRemove
1450
+ )
1451
+ public
1452
+ {
1453
+ // Include adding X new tiers with 0 current tiers.
1454
+ initialNumberOfTiers = bound(initialNumberOfTiers, 1, 15);
1455
+ numberOfTiersToRemove = bound(numberOfTiersToRemove, 0, initialNumberOfTiers - 1);
1456
+
1457
+ // Create random tiers to remove.
1458
+ uint256[] memory tiersToRemove = new uint256[](numberOfTiersToRemove);
1459
+ // Use `seed` to generate new random tiers, and iterate on `i` to fill the `tiersToRemove` array.
1460
+ for (uint256 i; i < numberOfTiersToRemove;) {
1461
+ uint256 newTierCandidate = uint256(keccak256(abi.encode(seed))) % initialNumberOfTiers;
1462
+ bool invalidTier;
1463
+ if (newTierCandidate != 0) {
1464
+ for (uint256 j; j < numberOfTiersToRemove; j++) {
1465
+ // Same value twice?
1466
+ if (newTierCandidate == tiersToRemove[j]) {
1467
+ invalidTier = true;
1468
+ break;
1469
+ }
1470
+ }
1471
+ if (!invalidTier) {
1472
+ tiersToRemove[i] = newTierCandidate;
1473
+ i++;
1474
+ }
1475
+ }
1476
+ // Overflow to loop over (the seed is fuzzed, and may start at max(uint256)).
1477
+ unchecked {
1478
+ seed++;
1479
+ }
1480
+ }
1481
+ // Order the tiers to remove for event matching (which are ordered too).
1482
+ tiersToRemove = _sortArray(tiersToRemove);
1483
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](initialNumberOfTiers);
1484
+ JB721Tier[] memory tiers = new JB721Tier[](initialNumberOfTiers);
1485
+ for (uint256 i; i < initialNumberOfTiers; i++) {
1486
+ tierConfigs[i] = JB721TierConfig({
1487
+ price: uint104((i + 1) * 10),
1488
+ initialSupply: uint32(100),
1489
+ votingUnits: uint16(0),
1490
+ reserveFrequency: uint16(i),
1491
+ reserveBeneficiary: reserveBeneficiary,
1492
+ encodedIPFSUri: tokenUris[0],
1493
+ category: uint24(100),
1494
+ discountPercent: uint8(0),
1495
+ allowOwnerMint: false,
1496
+ useReserveBeneficiaryAsDefault: false,
1497
+ transfersPausable: false,
1498
+ useVotingUnits: false,
1499
+ cannotBeRemoved: false,
1500
+ cannotIncreaseDiscountPercent: false
1501
+ });
1502
+ tiers[i] = JB721Tier({
1503
+ id: uint32(i + 1),
1504
+ price: tierConfigs[i].price,
1505
+ remainingSupply: tierConfigs[i].initialSupply,
1506
+ initialSupply: tierConfigs[i].initialSupply,
1507
+ votingUnits: tierConfigs[i].votingUnits,
1508
+ reserveFrequency: tierConfigs[i].reserveFrequency,
1509
+ reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary,
1510
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
1511
+ category: tierConfigs[i].category,
1512
+ discountPercent: tierConfigs[i].discountPercent,
1513
+ allowOwnerMint: tierConfigs[i].allowOwnerMint,
1514
+ transfersPausable: tierConfigs[i].transfersPausable,
1515
+ cannotBeRemoved: tierConfigs[i].cannotBeRemoved,
1516
+ cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent,
1517
+ resolvedUri: ""
1518
+ });
1519
+ }
1520
+ ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore();
1521
+ ForTest_JB721TiersHook hook = new ForTest_JB721TiersHook(
1522
+ projectId,
1523
+ IJBDirectory(mockJBDirectory),
1524
+ name,
1525
+ symbol,
1526
+ IJBRulesets(mockJBRulesets),
1527
+ baseUri,
1528
+ IJB721TokenUriResolver(mockTokenUriResolver),
1529
+ contractUri,
1530
+ tierConfigs,
1531
+ IJB721TiersHookStore(address(store)),
1532
+ JB721TiersHookFlags({
1533
+ preventOverspending: false,
1534
+ noNewTiersWithReserves: false,
1535
+ noNewTiersWithVotes: false,
1536
+ noNewTiersWithOwnerMinting: true
1537
+ })
1538
+ );
1539
+ hook.transferOwnership(owner);
1540
+ // Will be resized later
1541
+ JB721TierConfig[] memory tierConfigsRemaining = new JB721TierConfig[](initialNumberOfTiers);
1542
+ JB721Tier[] memory tiersRemaining = new JB721Tier[](initialNumberOfTiers);
1543
+ for (uint256 i; i < tiers.length; i++) {
1544
+ tierConfigsRemaining[i] = tierConfigs[i];
1545
+ tiersRemaining[i] = tiers[i];
1546
+ }
1547
+
1548
+ // Iterate through the remaining tiers and remove the ones in `tiersToRemove`.
1549
+ // Do this by "swapping" the tier to remove with the last element in the array, and then "popping" that last
1550
+ // element.
1551
+ for (uint256 i; i < tiersRemaining.length;) {
1552
+ bool swappedAndPopped;
1553
+ for (uint256 j; j < tiersToRemove.length; j++) {
1554
+ if (tiersRemaining[i].id == tiersToRemove[j]) {
1555
+ // Swap and pop tiers removed
1556
+ tiersRemaining[i] = tiersRemaining[tiersRemaining.length - 1];
1557
+ tierConfigsRemaining[i] = tierConfigsRemaining[tierConfigsRemaining.length - 1];
1558
+ // Remove the last elelment / reduce array length by 1
1559
+ assembly ("memory-safe") {
1560
+ mstore(tiersRemaining, sub(mload(tiersRemaining), 1))
1561
+ mstore(tierConfigsRemaining, sub(mload(tierConfigsRemaining), 1))
1562
+ }
1563
+ swappedAndPopped = true;
1564
+ break;
1565
+ }
1566
+ }
1567
+ if (!swappedAndPopped) i++;
1568
+ }
1569
+
1570
+ vm.prank(owner);
1571
+ hook.adjustTiers(new JB721TierConfig[](0), tiersToRemove);
1572
+ JB721Tier[] memory tiersListDump = hook.test_store().ForTest_dumpTiersList(address(hook));
1573
+ // Check: are all of the tiers are still in the linked list (both active and inactive)?
1574
+ assertTrue(_isIn(tiers, tiersListDump));
1575
+ // Check: does the linked list include only the tiers (active and inactives)?
1576
+ assertTrue(_isIn(tiersListDump, tiers));
1577
+ // Check: was the correct event emitted?
1578
+ vm.expectEmit(true, false, false, true, address(hook.test_store()));
1579
+ emit CleanTiers(address(hook), beneficiary);
1580
+ vm.startPrank(beneficiary);
1581
+ hook.test_store().cleanTiers(address(hook));
1582
+ vm.stopPrank();
1583
+ tiersListDump = hook.test_store().ForTest_dumpTiersList(address(hook));
1584
+ // Check: is the correct number of tiers remaining?
1585
+ assertEq(tiersListDump.length, initialNumberOfTiers - numberOfTiersToRemove);
1586
+ // Check: are all of the remaining tiers in the dump?
1587
+ assertTrue(_isIn(tiersRemaining, tiersListDump));
1588
+ // Check: does the dump include any tiers which shouldn't be remaining?
1589
+ assertTrue(_isIn(tiersListDump, tiersRemaining));
1590
+ }
1591
+
1592
+ function test_tiersOf_emptyArrayIfNoInitializedTiers(uint256 size) public {
1593
+ // Initialize a hook without default tiers.
1594
+ JB721TiersHook hook = _initHookDefaultTiers(0);
1595
+
1596
+ // Try to get `size` tiers
1597
+ JB721Tier[] memory intialTiers = hook.STORE().tiersOf(address(hook), new uint256[](0), false, 0, size);
1598
+
1599
+ // Check: does the array have a length of 0?
1600
+ assertEq(intialTiers.length, 0, "Length mismatch.");
1601
+ }
1602
+
1603
+ function test_setDiscountPercentOf_revertIfCannotIncreaseDiscount() public {
1604
+ // Initial tier config and data.
1605
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
1606
+ tierConfigs[0] = JB721TierConfig({
1607
+ price: 10,
1608
+ initialSupply: uint32(100),
1609
+ votingUnits: uint16(0),
1610
+ reserveFrequency: uint16(0),
1611
+ reserveBeneficiary: reserveBeneficiary,
1612
+ encodedIPFSUri: tokenUris[0],
1613
+ category: uint24(100),
1614
+ discountPercent: uint8(0),
1615
+ allowOwnerMint: false,
1616
+ useReserveBeneficiaryAsDefault: false,
1617
+ transfersPausable: false,
1618
+ useVotingUnits: true,
1619
+ cannotBeRemoved: true,
1620
+ cannotIncreaseDiscountPercent: true
1621
+ });
1622
+ // Deploy the hook and its store with the initial tiers.
1623
+ vm.etch(hook_i, address(hook).code);
1624
+ JB721TiersHook hook = JB721TiersHook(hook_i);
1625
+ hook.initialize(
1626
+ projectId,
1627
+ name,
1628
+ symbol,
1629
+ baseUri,
1630
+ IJB721TokenUriResolver(mockTokenUriResolver),
1631
+ contractUri,
1632
+ JB721InitTiersConfig({
1633
+ tiers: tierConfigs,
1634
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1635
+ decimals: 18,
1636
+ prices: IJBPrices(address(0))
1637
+ }),
1638
+ JB721TiersHookFlags({
1639
+ preventOverspending: false,
1640
+ noNewTiersWithReserves: false,
1641
+ noNewTiersWithVotes: false,
1642
+ noNewTiersWithOwnerMinting: true
1643
+ })
1644
+ );
1645
+ hook.transferOwnership(owner);
1646
+
1647
+ // Expect the `setDiscountPercentOf` call to revert because of the flag.
1648
+ vm.expectRevert(
1649
+ abi.encodeWithSelector(
1650
+ JB721TiersHookStore.JB721TiersHookStore_DiscountPercentIncreaseNotAllowed.selector, 100, 0
1651
+ )
1652
+ );
1653
+ vm.prank(owner);
1654
+ // Attempt to increase the discount of the first tier to 100%
1655
+ hook.setDiscountPercentOf(1, 100);
1656
+ }
1657
+
1658
+ function test_setDiscountPercentsOf_revertIfCannotIncreaseDiscounts() public {
1659
+ // Initial tier config and data.
1660
+ JB721TierConfig[] memory initialConfig = new JB721TierConfig[](2);
1661
+ initialConfig[0] = JB721TierConfig({
1662
+ price: 10,
1663
+ initialSupply: uint32(100),
1664
+ votingUnits: uint16(0),
1665
+ reserveFrequency: uint16(0),
1666
+ reserveBeneficiary: reserveBeneficiary,
1667
+ encodedIPFSUri: tokenUris[0],
1668
+ category: uint24(100),
1669
+ discountPercent: uint8(0),
1670
+ allowOwnerMint: false,
1671
+ useReserveBeneficiaryAsDefault: false,
1672
+ transfersPausable: false,
1673
+ useVotingUnits: true,
1674
+ cannotBeRemoved: true,
1675
+ cannotIncreaseDiscountPercent: true
1676
+ });
1677
+ initialConfig[1] = JB721TierConfig({
1678
+ price: 10,
1679
+ initialSupply: uint32(100),
1680
+ votingUnits: uint16(0),
1681
+ reserveFrequency: uint16(0),
1682
+ reserveBeneficiary: reserveBeneficiary,
1683
+ encodedIPFSUri: tokenUris[0],
1684
+ category: uint24(100),
1685
+ discountPercent: uint8(0),
1686
+ allowOwnerMint: false,
1687
+ useReserveBeneficiaryAsDefault: false,
1688
+ transfersPausable: false,
1689
+ useVotingUnits: true,
1690
+ cannotBeRemoved: true,
1691
+ cannotIncreaseDiscountPercent: false
1692
+ });
1693
+
1694
+ // Deploy the hook and its store with the initial tiers.
1695
+ vm.etch(hook_i, address(hook).code);
1696
+ JB721TiersHook hook = JB721TiersHook(hook_i);
1697
+ hook.initialize(
1698
+ projectId,
1699
+ name,
1700
+ symbol,
1701
+ baseUri,
1702
+ IJB721TokenUriResolver(mockTokenUriResolver),
1703
+ contractUri,
1704
+ JB721InitTiersConfig({
1705
+ tiers: initialConfig,
1706
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1707
+ decimals: 18,
1708
+ prices: IJBPrices(address(0))
1709
+ }),
1710
+ JB721TiersHookFlags({
1711
+ preventOverspending: false,
1712
+ noNewTiersWithReserves: false,
1713
+ noNewTiersWithVotes: false,
1714
+ noNewTiersWithOwnerMinting: true
1715
+ })
1716
+ );
1717
+ hook.transferOwnership(owner);
1718
+
1719
+ // Build calldata for increasing multiple tier discounts at once.
1720
+ JB721TiersSetDiscountPercentConfig[] memory discountCalldata = new JB721TiersSetDiscountPercentConfig[](2);
1721
+ discountCalldata[0] = JB721TiersSetDiscountPercentConfig({
1722
+ tierId: 1,
1723
+ discountPercent: 100 // invalid
1724
+ });
1725
+
1726
+ discountCalldata[1] = JB721TiersSetDiscountPercentConfig({
1727
+ tierId: 2,
1728
+ discountPercent: 100 // valid
1729
+ });
1730
+
1731
+ // Expect the `setDiscountPercentsOf` call to revert because of the flag.
1732
+ vm.expectRevert(
1733
+ abi.encodeWithSelector(
1734
+ JB721TiersHookStore.JB721TiersHookStore_DiscountPercentIncreaseNotAllowed.selector, 100, 0
1735
+ )
1736
+ );
1737
+ vm.prank(owner);
1738
+ hook.setDiscountPercentsOf(discountCalldata);
1739
+ }
1740
+ }