@bananapus/721-hook-v6 0.0.41 → 0.0.43

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 (77) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +60 -18
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +4 -1
  8. package/src/JB721TiersHookProjectDeployer.sol +68 -30
  9. package/src/JB721TiersHookStore.sol +1 -4
  10. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  11. package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
  12. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
  13. package/test/utils/AccessJBLib.sol +49 -0
  14. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  15. package/test/utils/TestBaseWorkflow.sol +213 -0
  16. package/test/utils/UnitTestSetup.sol +805 -0
  17. package/.gas-snapshot +0 -152
  18. package/ADMINISTRATION.md +0 -87
  19. package/ARCHITECTURE.md +0 -98
  20. package/AUDIT_INSTRUCTIONS.md +0 -77
  21. package/RISKS.md +0 -118
  22. package/SKILLS.md +0 -43
  23. package/STYLE_GUIDE.md +0 -610
  24. package/USER_JOURNEYS.md +0 -121
  25. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  26. package/slither-ci.config.json +0 -10
  27. package/test/721HookAttacks.t.sol +0 -408
  28. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  29. package/test/Fork.t.sol +0 -2346
  30. package/test/TestAuditGaps.sol +0 -1075
  31. package/test/TestCheckpoints.t.sol +0 -341
  32. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  33. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  34. package/test/audit/AuditRegressions.t.sol +0 -83
  35. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  36. package/test/audit/FreshAudit.t.sol +0 -197
  37. package/test/audit/FutureTierPoC.t.sol +0 -39
  38. package/test/audit/FutureTierRemoval.t.sol +0 -47
  39. package/test/audit/Pass12L18.t.sol +0 -80
  40. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  41. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  42. package/test/audit/RepoFindings.t.sol +0 -195
  43. package/test/audit/ReserveActivation.t.sol +0 -87
  44. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  45. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  46. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  47. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  48. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  49. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  50. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  51. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  52. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  53. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  54. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  55. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  56. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  57. package/test/regression/CacheTierLookup.t.sol +0 -190
  58. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  59. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  60. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  61. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  62. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  63. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  64. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  65. package/test/unit/JBBitmap.t.sol +0 -170
  66. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  67. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  68. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  69. package/test/unit/deployer_Unit.t.sol +0 -114
  70. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  71. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  72. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  73. package/test/unit/pay_Unit.t.sol +0 -1661
  74. package/test/unit/redeem_Unit.t.sol +0 -473
  75. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  76. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  77. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -0,0 +1,805 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
+ import "../utils/ForTest_JB721TiersHook.sol";
8
+
9
+ // forge-lint: disable-next-line(unaliased-plain-import)
10
+ import "../../src/JB721TiersHookDeployer.sol";
11
+ // forge-lint: disable-next-line(unaliased-plain-import)
12
+ import "../../src/JB721TiersHook.sol";
13
+ // forge-lint: disable-next-line(unaliased-plain-import)
14
+ import "../../src/abstract/JB721Hook.sol";
15
+ // forge-lint: disable-next-line(unaliased-plain-import)
16
+ import "../../src/JB721TiersHookStore.sol";
17
+
18
+ import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
19
+ import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
20
+
21
+ // forge-lint: disable-next-line(unaliased-plain-import)
22
+ import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
23
+ // forge-lint: disable-next-line(unaliased-plain-import)
24
+ import "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
25
+ // forge-lint: disable-next-line(unaliased-plain-import)
26
+ import "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
+ // forge-lint: disable-next-line(unaliased-plain-import)
28
+ import "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
29
+ // forge-lint: disable-next-line(unaliased-plain-import)
30
+ import "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
31
+ // forge-lint: disable-next-line(unaliased-plain-import)
32
+ import "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
33
+ // forge-lint: disable-next-line(unaliased-plain-import)
34
+ import "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
35
+ // forge-lint: disable-next-line(unaliased-plain-import)
36
+ import "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
37
+ // forge-lint: disable-next-line(unaliased-plain-import)
38
+ import "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
39
+ // forge-lint: disable-next-line(unaliased-plain-import)
40
+ import "@bananapus/core-v6/src/structs/JBSplit.sol";
41
+ // forge-lint: disable-next-line(unaliased-plain-import)
42
+ import "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
43
+ // forge-lint: disable-next-line(unaliased-plain-import)
44
+ import "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
45
+
46
+ // forge-lint: disable-next-line(unaliased-plain-import)
47
+ import "../../src/structs/JBLaunchProjectConfig.sol";
48
+ // forge-lint: disable-next-line(unaliased-plain-import)
49
+ import "../../src/structs/JBPayDataHookRulesetMetadata.sol";
50
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
51
+ import {JB721TierFlags} from "../../src/structs/JB721TierFlags.sol";
52
+
53
+ // forge-lint: disable-next-line(unaliased-plain-import)
54
+ import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
55
+
56
+ // forge-lint: disable-next-line(unaliased-plain-import)
57
+ import "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
58
+ // forge-lint: disable-next-line(unaliased-plain-import)
59
+ import "@bananapus/core-v6/src/libraries/JBConstants.sol";
60
+ // forge-lint: disable-next-line(unaliased-plain-import)
61
+ import "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
62
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
63
+
64
+ contract UnitTestSetup is Test {
65
+ address beneficiary;
66
+ address owner;
67
+ address reserveBeneficiary;
68
+ // forge-lint: disable-next-line(mixed-case-variable)
69
+ address mockJBController;
70
+ // forge-lint: disable-next-line(mixed-case-variable)
71
+ address mockJBDirectory;
72
+ // forge-lint: disable-next-line(mixed-case-variable)
73
+ address mockJBPrices;
74
+ // forge-lint: disable-next-line(mixed-case-variable)
75
+ address mockJBRulesets;
76
+ address mockTokenUriResolver;
77
+ address mockTerminalAddress;
78
+ // forge-lint: disable-next-line(mixed-case-variable)
79
+ address mockJBProjects;
80
+ // forge-lint: disable-next-line(mixed-case-variable)
81
+ address mockJBPermissions;
82
+ // forge-lint: disable-next-line(mixed-case-variable)
83
+ address mockJBSplits;
84
+
85
+ string name = "NAME";
86
+ string symbol = "SYM";
87
+ string baseUri = "http://www.null.com/";
88
+ string contractUri = "ipfs://null";
89
+ string rulesetMemo = "meemoo";
90
+
91
+ uint256 projectId = 69;
92
+
93
+ uint256 constant SURPLUS = 10e18;
94
+
95
+ uint256 constant CASH_OUT_TAX_RATE = 6000; // 60%
96
+
97
+ // forge-lint: disable-next-line(screaming-snake-case-const)
98
+ address constant trustedForwarder = address(123_456);
99
+
100
+ JB721TierConfig defaultTierConfig;
101
+
102
+ // To generate:
103
+ // NodeJS: function con(hash) { Buffer.from(bs58.decode(hash).slice(2)).toString('hex') }
104
+ // JavaScript: 0x${bs58.decode(hash).slice(2).toString('hex')})
105
+ bytes32[] tokenUris = [
106
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
107
+ bytes32(0xf5d60fc6f462f6176982833f5e2ca222a2ced265fa94e4ce1c477d74910250ed),
108
+ bytes32(0x4258512cfb09993d9f3613a59ffc592a5593abf3c06ed57a22656c5fbca4de23),
109
+ bytes32(0xae7035a8ef12433adbf4a55f2063696972bcf50434fe70ee6d8ab78f83e358c8),
110
+ bytes32(0xae7035a8ef12433adbf4a55f2faabecff3446276fdbc6f6209e6bba25ee358c8),
111
+ bytes32(0xae7035a8ef1242fc4b803a9284453843f278307462311f8b8b90fddfcbe358c8),
112
+ bytes32(0xae824fb9f7de128f66cb5e224e4f8c65f37c479ee6ec7193c8741d6f997f5a18),
113
+ bytes32(0xae7035a8f8d14617dd6d904265fe7d84a493c628385ffba7016d6463c852e8c8),
114
+ bytes32(0xae7035a8ef12433adbf4a55f2063696972bcf50434fe70ee6d8ab78f74adbbf7),
115
+ bytes32(0xae7035a8ef12433adbf4a55f2063696972bcf51c38098273db23452d955758c8)
116
+ ];
117
+
118
+ string[] theoreticHashes = [
119
+ "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz",
120
+ "QmetHutWQPz3qfu5jhTi1bqbRZXt8zAJaqqxkiJoJCX9DN",
121
+ "QmSodj3RSrXKPy3WRFSPz8HRDVyRdrBtjxBfoiWBgtGugN",
122
+ "Qma5atSTeoKJkcXe2R7gdmcvPvLJJkh2jd4cDZeM1wnFgK",
123
+ "Qma5atSTeoKJkcXe2R7typcvPvLJJkh2jd4cDZeM1wnFgK",
124
+ "Qma5atSTeoKJKSQSDFcgdmcvPvLJJkh2jd4cDZeM1wnFgK",
125
+ "Qma5rtytgfdgzrg4345RFGdfbzert345rfgvs5YRtSTkcX",
126
+ "Qma5atSTkcXe2R7gdmcvPvLJJkh2234234QcDZeM1wnFgK",
127
+ "Qma5atSTeoKJkcXe2R7gdmcvPvLJJkh2jd4cDZeM1ZERze",
128
+ "Qma5atSTeoKJkcXe2R7gdmcvPvLJLkh2jd4cDZeM1wnFgK"
129
+ ];
130
+
131
+ JB721TierConfig[] tiers;
132
+ JB721TiersHookStore store;
133
+ JB721TiersHook hook;
134
+ JB721TiersHook hookOrigin;
135
+ JBAddressRegistry addressRegistry;
136
+ JB721TiersHookDeployer jbHookDeployer;
137
+ MetadataResolverHelper metadataHelper;
138
+
139
+ // forge-lint: disable-next-line(mixed-case-variable)
140
+ address hook_i = address(bytes20(keccak256("hook_implementation")));
141
+
142
+ event Mint(
143
+ uint256 indexed tokenId,
144
+ uint256 indexed tierId,
145
+ address indexed beneficiary,
146
+ uint256 totalAmountPaid,
147
+ address caller
148
+ );
149
+ event MintReservedNft(uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, address caller);
150
+ event AddTier(uint256 indexed tierId, JB721TierConfig tier, address caller);
151
+ event RemoveTier(uint256 indexed tierId, address caller);
152
+ event CleanTiers(address indexed nft, address caller);
153
+ event AddPayCredits(
154
+ uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller
155
+ );
156
+ event UsePayCredits(
157
+ uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller
158
+ );
159
+
160
+ function setUp() public virtual {
161
+ beneficiary = makeAddr("beneficiary");
162
+ owner = makeAddr("owner");
163
+ reserveBeneficiary = makeAddr("reserveBeneficiary");
164
+ mockJBDirectory = makeAddr("mockJBDirectory");
165
+ mockJBPrices = makeAddr("mockJBPrices");
166
+ mockJBRulesets = makeAddr("mockJBRulesets");
167
+ mockTerminalAddress = makeAddr("mockTerminalAddress");
168
+ mockJBProjects = makeAddr("mockJBProjects");
169
+ mockJBPermissions = makeAddr("mockJBPermissions");
170
+ mockJBSplits = makeAddr("mockJBSplits");
171
+ mockJBController = makeAddr("mockJBController");
172
+ mockTokenUriResolver = address(0);
173
+
174
+ vm.etch(mockJBDirectory, new bytes(0x69));
175
+ vm.etch(mockJBRulesets, new bytes(0x69));
176
+ vm.etch(mockTokenUriResolver, new bytes(0x69));
177
+ vm.etch(mockTerminalAddress, new bytes(0x69));
178
+ vm.etch(mockJBProjects, new bytes(0x69));
179
+ vm.etch(mockJBController, new bytes(0x69));
180
+
181
+ // Set default tier config field-by-field (avoids nested dynamic array storage copy).
182
+ defaultTierConfig.reserveFrequency = uint16(10);
183
+ defaultTierConfig.reserveBeneficiary = reserveBeneficiary;
184
+ defaultTierConfig.category = type(uint24).max;
185
+ defaultTierConfig.flags.useVotingUnits = true;
186
+
187
+ // Create 10 tiers, each with 100 NFTs available to mint.
188
+ for (uint256 i; i < 10; i++) {
189
+ tiers.push(); // Push default-initialized struct (avoids nested dynamic array storage copy).
190
+ tiers[i].price = uint104((i + 1) * 10); // The price is `tierId` * 10.
191
+ tiers[i].initialSupply = uint32(100);
192
+ tiers[i].reserveBeneficiary = reserveBeneficiary;
193
+ tiers[i].encodedIPFSUri = tokenUris[i];
194
+ tiers[i].category = uint24(100);
195
+ tiers[i].flags.useVotingUnits = true;
196
+ }
197
+ vm.mockCall(
198
+ mockJBRulesets,
199
+ abi.encodeCall(IJBRulesets.currentOf, projectId),
200
+ abi.encode(
201
+ JBRuleset({
202
+ cycleNumber: 1,
203
+ id: uint48(block.timestamp),
204
+ basedOnId: 0,
205
+ start: uint48(block.timestamp),
206
+ duration: 600,
207
+ weight: 10e18,
208
+ weightCutPercent: 0,
209
+ approvalHook: IJBRulesetApprovalHook(address(0)),
210
+ metadata: JBRulesetMetadataResolver.packRulesetMetadata(
211
+ JBRulesetMetadata({
212
+ reservedPercent: 5000, //50%
213
+ cashOutTaxRate: 6000, //60%
214
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
215
+ pausePay: false,
216
+ pauseCreditTransfers: false,
217
+ allowOwnerMinting: true,
218
+ allowSetCustomToken: false,
219
+ allowTerminalMigration: false,
220
+ allowSetTerminals: false,
221
+ allowSetController: false,
222
+ allowAddAccountingContext: false,
223
+ allowAddPriceFeed: false,
224
+ ownerMustSendPayouts: false,
225
+ holdFees: false,
226
+ useTotalSurplusForCashOuts: false,
227
+ useDataHookForPay: true,
228
+ useDataHookForCashOut: true,
229
+ dataHook: address(0),
230
+ metadata: 0x00
231
+ })
232
+ )
233
+ })
234
+ )
235
+ );
236
+
237
+ vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
238
+ vm.mockCall(
239
+ mockJBDirectory, abi.encodeWithSelector(IJBPermissioned.PERMISSIONS.selector), abi.encode(mockJBPermissions)
240
+ );
241
+
242
+ store = new JB721TiersHookStore();
243
+ JB721CheckpointsDeployer checkpointsDeployer = new JB721CheckpointsDeployer(store);
244
+ hookOrigin = new JB721TiersHook(
245
+ IJBDirectory(mockJBDirectory),
246
+ IJBPermissions(mockJBPermissions),
247
+ IJBPrices(mockJBPrices),
248
+ IJBRulesets(mockJBRulesets),
249
+ IJB721TiersHookStore(store),
250
+ IJBSplits(mockJBSplits),
251
+ IJB721CheckpointsDeployer(address(checkpointsDeployer)),
252
+ trustedForwarder
253
+ );
254
+ addressRegistry = new JBAddressRegistry();
255
+ jbHookDeployer = new JB721TiersHookDeployer(hookOrigin, store, addressRegistry, trustedForwarder);
256
+ JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
257
+ name: name,
258
+ symbol: symbol,
259
+ baseUri: baseUri,
260
+ tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
261
+ contractUri: contractUri,
262
+ tiersConfig: JB721InitTiersConfig({
263
+ tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
264
+ }),
265
+ flags: JB721TiersHookFlags({
266
+ preventOverspending: false,
267
+ issueTokensForSplits: false,
268
+ noNewTiersWithReserves: true,
269
+ noNewTiersWithVotes: true,
270
+ noNewTiersWithOwnerMinting: true
271
+ })
272
+ });
273
+
274
+ hook = JB721TiersHook(address(jbHookDeployer.deployHookFor(projectId, hookConfig, bytes32(0))));
275
+ hook.transferOwnership(owner);
276
+
277
+ metadataHelper = new MetadataResolverHelper();
278
+ }
279
+
280
+ // forge-lint: disable-next-line(mixed-case-function)
281
+ function USD() internal pure returns (uint256) {
282
+ return JBCurrencyIds.USD;
283
+ }
284
+
285
+ // forge-lint: disable-next-line(mixed-case-function)
286
+ function NATIVE_TOKEN() internal pure returns (address) {
287
+ return JBConstants.NATIVE_TOKEN;
288
+ }
289
+
290
+ // forge-lint: disable-next-line(mixed-case-function)
291
+ function MAX_FEE() internal pure returns (uint256) {
292
+ return JBConstants.MAX_FEE;
293
+ }
294
+
295
+ // forge-lint: disable-next-line(mixed-case-function)
296
+ function MAX_RESERVED_PERCENT() internal pure returns (uint256) {
297
+ return JBConstants.MAX_RESERVED_PERCENT;
298
+ }
299
+
300
+ // forge-lint: disable-next-line(mixed-case-function)
301
+ function MAX_CASH_OUT_TAX_RATE() internal pure returns (uint256) {
302
+ return JBConstants.MAX_CASH_OUT_TAX_RATE;
303
+ }
304
+
305
+ // forge-lint: disable-next-line(mixed-case-function)
306
+ function MAX_WEIGHT_CUT_PERCENT() internal pure returns (uint256) {
307
+ return JBConstants.MAX_WEIGHT_CUT_PERCENT;
308
+ }
309
+
310
+ // forge-lint: disable-next-line(mixed-case-function)
311
+ function SPLITS_TOTAL_PERCENT() internal pure returns (uint256) {
312
+ return JBConstants.SPLITS_TOTAL_PERCENT;
313
+ }
314
+
315
+ // *------------------*
316
+ // * INTERNAL HELPERS *
317
+ // *------------------*
318
+
319
+ // Compare two `JB721Tier`s.
320
+ function assertEq(JB721Tier memory first, JB721Tier memory second) internal {
321
+ assertEq(first.id, second.id);
322
+ assertEq(first.price, second.price);
323
+ assertEq(first.remainingSupply, second.remainingSupply);
324
+ assertEq(first.initialSupply, second.initialSupply);
325
+ assertEq(first.votingUnits, second.votingUnits);
326
+ assertEq(first.reserveFrequency, second.reserveFrequency);
327
+ assertEq(first.reserveBeneficiary, second.reserveBeneficiary);
328
+ assertEq(first.encodedIPFSUri, second.encodedIPFSUri);
329
+ }
330
+
331
+ // Compare two arrays of `JB721Tier`s.
332
+ function assertEq(JB721Tier[] memory first, JB721Tier[] memory second) internal {
333
+ assertEq(first.length, second.length);
334
+ for (uint256 i; i < first.length; i++) {
335
+ assertEq(first[i].id, second[i].id);
336
+ assertEq(first[i].price, second[i].price);
337
+ assertEq(first[i].remainingSupply, second[i].remainingSupply);
338
+ assertEq(first[i].initialSupply, second[i].initialSupply);
339
+ assertEq(first[i].votingUnits, second[i].votingUnits);
340
+ assertEq(first[i].reserveFrequency, second[i].reserveFrequency);
341
+ assertEq(first[i].reserveBeneficiary, second[i].reserveBeneficiary);
342
+ assertEq(first[i].encodedIPFSUri, second[i].encodedIPFSUri);
343
+ }
344
+ }
345
+
346
+ function mockAndExpect(address target, bytes memory mockCalldata, bytes memory returnData) internal {
347
+ vm.mockCall(target, mockCalldata, returnData);
348
+ vm.expectCall(target, mockCalldata);
349
+ }
350
+
351
+ // Generate `tokenId`s based on a `tierId` and a `tokenNumber` within the tier.
352
+ function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
353
+ return (tierId * 1_000_000_000) + tokenNumber;
354
+ }
355
+
356
+ // Check if every elements from smol is in bigg.
357
+ function _isIn(JB721Tier[] memory smol, JB721Tier[] memory bigg) internal returns (bool) {
358
+ // smol cannot be bigger than bigg.
359
+ if (smol.length > bigg.length) {
360
+ emit log("_isIn: smol too big");
361
+ return false;
362
+ }
363
+ if (smol.length == 0) return true;
364
+ uint256 count;
365
+ // Iterate on every smol element.
366
+ for (uint256 smolIter; smolIter < smol.length; smolIter++) {
367
+ // Compare it with every bigg element until...
368
+ for (uint256 biggIter; biggIter < bigg.length; biggIter++) {
369
+ // ... the same element is found, then break to go to the next smol element.
370
+ if (_compareTiers(smol[smolIter], bigg[biggIter])) {
371
+ count += smolIter + 1; // 1-indexed, as the length.
372
+ break;
373
+ }
374
+ }
375
+ }
376
+ // Ensure that all the smol indexes have been iterated on (i.e. we've seen `sum(1, 2, ..., smol.length)`
377
+ // elements).
378
+ if (count == (smol.length * (smol.length + 1)) / 2) {
379
+ return true;
380
+ } else {
381
+ emit log("_isIn: incomplete inclusion");
382
+ emit log_uint(count);
383
+ emit log_uint(smol.length);
384
+ return false;
385
+ }
386
+ }
387
+
388
+ function _compareTiers(JB721Tier memory first, JB721Tier memory second) internal pure returns (bool) {
389
+ return (first.id == second.id && first.price == second.price && first.remainingSupply == second.remainingSupply
390
+ && first.initialSupply == second.initialSupply && first.votingUnits == second.votingUnits
391
+ && first.reserveFrequency == second.reserveFrequency
392
+ && first.reserveBeneficiary == second.reserveBeneficiary
393
+ && first.encodedIPFSUri == second.encodedIPFSUri
394
+ && keccak256(abi.encodePacked(first.resolvedUri)) == keccak256(abi.encodePacked(second.resolvedUri)));
395
+ }
396
+
397
+ // Simple selection sort for an array of `uint256` values.
398
+ function _sortArray(uint256[] memory arr) internal pure returns (uint256[] memory) {
399
+ for (uint256 i; i < arr.length; i++) {
400
+ uint256 minIndex = i;
401
+ uint256 minValue = arr[i];
402
+ for (uint256 j = i; j < arr.length; j++) {
403
+ if (arr[j] < minValue) {
404
+ minIndex = j;
405
+ minValue = arr[j];
406
+ }
407
+ }
408
+ if (minIndex != i) (arr[i], arr[minIndex]) = (arr[minIndex], arr[i]);
409
+ }
410
+ return arr;
411
+ }
412
+
413
+ // Simple selection sort for an array of `uint16` values.
414
+ function _sortArray(uint16[] memory arr) internal pure returns (uint16[] memory) {
415
+ for (uint256 i; i < arr.length; i++) {
416
+ uint256 minIndex = i;
417
+ uint16 minValue = arr[i];
418
+ for (uint256 j = i; j < arr.length; j++) {
419
+ if (arr[j] < minValue) {
420
+ minIndex = j;
421
+ minValue = arr[j];
422
+ }
423
+ }
424
+ if (minIndex != i) (arr[i], arr[minIndex]) = (arr[minIndex], arr[i]);
425
+ }
426
+ return arr;
427
+ }
428
+
429
+ // Simple selection sort for an array of `uint8` values.
430
+ function _sortArray(uint8[] memory arr) internal pure returns (uint8[] memory) {
431
+ for (uint256 i; i < arr.length; i++) {
432
+ uint256 minIndex = i;
433
+ uint8 minValue = arr[i];
434
+ for (uint256 j = i; j < arr.length; j++) {
435
+ if (arr[j] < minValue) {
436
+ minIndex = j;
437
+ minValue = arr[j];
438
+ }
439
+ }
440
+ if (minIndex != i) (arr[i], arr[minIndex]) = (arr[minIndex], arr[i]);
441
+ }
442
+ return arr;
443
+ }
444
+
445
+ // Create a random array of `uint16` values based on a given `seed`.
446
+ function _createArray(uint256 length, uint256 seed) internal pure returns (uint16[] memory) {
447
+ uint16[] memory out = new uint16[](length);
448
+
449
+ for (uint256 i; i < length; i++) {
450
+ out[i] = uint16(uint256(keccak256(abi.encode(seed, i))));
451
+ }
452
+
453
+ return out;
454
+ }
455
+
456
+ // Create an array of `JB721TierConfig`s and `JB721Tier`s using the provided and the default `prices`, `initialId`,
457
+ // and `categoryIncrement`.
458
+ function _createTiers(
459
+ JB721TierConfig memory tierConfig,
460
+ uint256 numberOfTiers
461
+ )
462
+ internal
463
+ view
464
+ returns (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory newTiers)
465
+ {
466
+ return _createTiers(tierConfig, numberOfTiers, 0, new uint16[](numberOfTiers), 0);
467
+ }
468
+
469
+ // Create an array of `JB721TierConfig`s and `JB721Tier`s using the provided and the default `prices` and
470
+ // `initialId`.
471
+ function _createTiers(
472
+ JB721TierConfig memory tierConfig,
473
+ uint256 numberOfTiers,
474
+ uint256 categoryIncrement
475
+ )
476
+ internal
477
+ view
478
+ returns (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory newTiers)
479
+ {
480
+ return _createTiers(tierConfig, numberOfTiers, 0, new uint16[](numberOfTiers), categoryIncrement);
481
+ }
482
+
483
+ // Create an array of `JB721TierConfig`s and `JB721Tier`s using the provided and the default `categoryIncrement`.
484
+ function _createTiers(
485
+ JB721TierConfig memory tierConfig,
486
+ uint256 numberOfTiers,
487
+ uint256 initialId,
488
+ uint16[] memory prices
489
+ )
490
+ internal
491
+ view
492
+ returns (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory newTiers)
493
+ {
494
+ return _createTiers(tierConfig, numberOfTiers, initialId, prices, 0);
495
+ }
496
+
497
+ // Create an array of `JB721TierConfig`s and `JB721Tier`s using the provided parameters.
498
+ function _createTiers(
499
+ JB721TierConfig memory tierConfig,
500
+ uint256 numberOfTiers,
501
+ uint256 initialId,
502
+ uint16[] memory prices,
503
+ uint256 categoryIncrement
504
+ )
505
+ internal
506
+ view
507
+ returns (JB721TierConfig[] memory tierConfigs, JB721Tier[] memory newTiers)
508
+ {
509
+ tierConfigs = new JB721TierConfig[](numberOfTiers);
510
+ newTiers = new JB721Tier[](numberOfTiers);
511
+
512
+ for (uint256 i; i < numberOfTiers; i++) {
513
+ tierConfigs[i] = JB721TierConfig({
514
+ price: prices[i] == 0 ? uint16((i + 1) * 10) : prices[i], // The price is `tierId` * 10.
515
+ initialSupply: tierConfig.initialSupply == 0 ? uint32(100) : tierConfig.initialSupply,
516
+ votingUnits: tierConfig.votingUnits,
517
+ reserveFrequency: tierConfig.reserveFrequency,
518
+ reserveBeneficiary: reserveBeneficiary,
519
+ encodedIPFSUri: i < tokenUris.length ? tokenUris[i] : tokenUris[0],
520
+ category: categoryIncrement == 0
521
+ // forge-lint: disable-next-line(unsafe-typecast)
522
+ ? tierConfig.category == type(uint24).max ? uint24(i * 2 + 1) : tierConfig.category
523
+ // forge-lint: disable-next-line(unsafe-typecast)
524
+ : uint24(i * 2 + categoryIncrement),
525
+ discountPercent: tierConfig.discountPercent,
526
+ flags: JB721TierConfigFlags({
527
+ allowOwnerMint: tierConfig.flags.allowOwnerMint,
528
+ useReserveBeneficiaryAsDefault: tierConfig.flags.useReserveBeneficiaryAsDefault,
529
+ transfersPausable: tierConfig.flags.transfersPausable,
530
+ useVotingUnits: tierConfig.flags.useVotingUnits,
531
+ cantBeRemoved: tierConfig.flags.cantBeRemoved,
532
+ cantIncreaseDiscountPercent: tierConfig.flags.cantIncreaseDiscountPercent,
533
+ cantBuyWithCredits: tierConfig.flags.cantBuyWithCredits
534
+ }),
535
+ splitPercent: tierConfig.splitPercent,
536
+ splits: new JBSplit[](0)
537
+ });
538
+
539
+ newTiers[i] = JB721Tier({
540
+ // forge-lint: disable-next-line(unsafe-typecast)
541
+ id: uint32(initialId + i + 1),
542
+ price: tierConfigs[i].price,
543
+ remainingSupply: tierConfigs[i].initialSupply,
544
+ initialSupply: tierConfigs[i].initialSupply,
545
+ votingUnits: tierConfigs[i].votingUnits,
546
+ reserveFrequency: tierConfigs[i].reserveFrequency,
547
+ reserveBeneficiary: tierConfigs[i].reserveBeneficiary,
548
+ encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
549
+ category: tierConfigs[i].category,
550
+ discountPercent: tierConfigs[i].discountPercent,
551
+ flags: JB721TierFlags({
552
+ allowOwnerMint: tierConfigs[i].flags.allowOwnerMint,
553
+ transfersPausable: tierConfigs[i].flags.transfersPausable,
554
+ cantBeRemoved: tierConfigs[i].flags.cantBeRemoved,
555
+ cantIncreaseDiscountPercent: tierConfigs[i].flags.cantIncreaseDiscountPercent,
556
+ cantBuyWithCredits: tierConfigs[i].flags.cantBuyWithCredits
557
+ }),
558
+ splitPercent: tierConfigs[i].splitPercent,
559
+ resolvedUri: defaultTierConfig.encodedIPFSUri == bytes32(0)
560
+ ? ""
561
+ : string(abi.encodePacked("resolverURI", _generateTokenId(initialId + i + 1, 0)))
562
+ });
563
+ }
564
+ }
565
+
566
+ // Add the specified tiers to the hook, and remove the specified number of tiers from the hook. Uses `adjustTiers`.
567
+ function _addDeleteTiers(
568
+ JB721TiersHook tiersHook,
569
+ uint256 currentNumberOfTiers,
570
+ uint256 numberOfTiersToRemove,
571
+ JB721TierConfig[] memory tiersToAdd
572
+ )
573
+ internal
574
+ returns (uint256)
575
+ {
576
+ uint256 newNumberOfTiers = currentNumberOfTiers;
577
+ // Cap removals to the number of tiers that actually exist.
578
+ uint256 actualRemovals =
579
+ numberOfTiersToRemove > currentNumberOfTiers ? currentNumberOfTiers : numberOfTiersToRemove;
580
+ uint256[] memory tiersToRemove = new uint256[](actualRemovals);
581
+
582
+ for (uint256 i; i < actualRemovals; i++) {
583
+ tiersToRemove[i] = currentNumberOfTiers - i;
584
+ newNumberOfTiers--;
585
+ }
586
+
587
+ newNumberOfTiers += tiersToAdd.length;
588
+
589
+ vm.startPrank(owner);
590
+ tiersHook.adjustTiers(tiersToAdd, tiersToRemove);
591
+ tiersHook.STORE().cleanTiers(address(tiersHook));
592
+ vm.stopPrank();
593
+
594
+ JB721Tier[] memory storedTiers =
595
+ tiersHook.STORE().tiersOf(address(tiersHook), new uint256[](0), false, 0, newNumberOfTiers);
596
+ assertEq(storedTiers.length, newNumberOfTiers);
597
+
598
+ return newNumberOfTiers;
599
+ }
600
+
601
+ // Initialize a hook with tiers that use the default tier config.
602
+ // Use default pricing context (native token, 18 decimals, and 0 address as oracle).
603
+ // Don't prevent overspending.
604
+ function _initHookDefaultTiers(uint256 initialNumberOfTiers) internal returns (JB721TiersHook) {
605
+ return _initHookDefaultTiers(initialNumberOfTiers, false, uint32(uint160(JBConstants.NATIVE_TOKEN)), 18);
606
+ }
607
+
608
+ // Initialize a hook with tiers that use the default tier config.
609
+ // Use default pricing context (native token, 18 decimals).
610
+ function _initHookDefaultTiers(
611
+ uint256 initialNumberOfTiers,
612
+ bool preventOverspending
613
+ )
614
+ internal
615
+ returns (JB721TiersHook)
616
+ {
617
+ return
618
+ _initHookDefaultTiers(
619
+ initialNumberOfTiers, preventOverspending, uint32(uint160(JBConstants.NATIVE_TOKEN)), 18
620
+ );
621
+ }
622
+
623
+ // Initialize a hook with tiers that use the default tier config.
624
+ function _initHookDefaultTiers(
625
+ uint256 initialNumberOfTiers,
626
+ bool preventOverspending,
627
+ uint32 currency,
628
+ uint8 decimals
629
+ )
630
+ internal
631
+ returns (JB721TiersHook tiersHook)
632
+ {
633
+ // Initialize first tiers to add.
634
+ (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, initialNumberOfTiers);
635
+
636
+ // Deploy the hook.
637
+ vm.etch(hook_i, address(hook).code);
638
+ tiersHook = JB721TiersHook(hook_i);
639
+
640
+ // Initialize the hook's flags and init config in memory (for stack's sake).
641
+ JB721TiersHookFlags memory flags = JB721TiersHookFlags({
642
+ preventOverspending: preventOverspending,
643
+ issueTokensForSplits: false,
644
+ noNewTiersWithReserves: false,
645
+ noNewTiersWithVotes: false,
646
+ noNewTiersWithOwnerMinting: false
647
+ });
648
+
649
+ JB721InitTiersConfig memory initConfig =
650
+ JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: decimals});
651
+
652
+ tiersHook.initialize(
653
+ projectId,
654
+ name,
655
+ symbol,
656
+ baseUri,
657
+ IJB721TokenUriResolver(mockTokenUriResolver),
658
+ contractUri,
659
+ initConfig,
660
+ flags
661
+ );
662
+
663
+ // Transfer ownership to owner.
664
+ tiersHook.transferOwnership(owner);
665
+ }
666
+
667
+ // Initialize a 721 tiers hook specialized for testing purposes.
668
+ function _initializeForTestHook(uint256 initialNumberOfTiers) internal returns (ForTest_JB721TiersHook tiersHook) {
669
+ // Initialize first tiers to add.
670
+ (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, initialNumberOfTiers);
671
+
672
+ // Deploy the ForTest hook store.
673
+ ForTest_JB721TiersHookStore hookStore = new ForTest_JB721TiersHookStore();
674
+
675
+ // Deploy the ForTest hook.
676
+ tiersHook = new ForTest_JB721TiersHook(
677
+ ForTest_JB721TiersHook.ForTestInitConfig({
678
+ projectId: projectId,
679
+ name: name,
680
+ symbol: symbol,
681
+ baseUri: baseUri,
682
+ tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
683
+ contractUri: contractUri,
684
+ tiers: tierConfigs,
685
+ flags: JB721TiersHookFlags({
686
+ preventOverspending: false,
687
+ issueTokensForSplits: false,
688
+ noNewTiersWithReserves: false,
689
+ noNewTiersWithVotes: false,
690
+ noNewTiersWithOwnerMinting: true
691
+ })
692
+ }),
693
+ IJBDirectory(mockJBDirectory),
694
+ IJBPrices(mockJBPrices),
695
+ IJBRulesets(mockJBRulesets),
696
+ IJB721TiersHookStore(address(hookStore)),
697
+ IJBSplits(mockJBSplits)
698
+ );
699
+
700
+ // Transfer the hook's ownership to owner.
701
+ tiersHook.transferOwnership(owner);
702
+ }
703
+
704
+ // Create a default `JBDeploy712TiersHookConfig` and `JBLaunchProjectConfig` to quickly bootstrap a 721 tiers hook
705
+ // and project.
706
+ function createData()
707
+ internal
708
+ view
709
+ returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig)
710
+ {
711
+ string memory projectUri;
712
+ JBPayDataHookRulesetConfig[] memory rulesetConfigurations;
713
+ JBTerminalConfig[] memory terminalConfigurations;
714
+ JBPayDataHookRulesetMetadata memory metadata;
715
+ JBFundAccessLimitGroup[] memory fundAccessLimitGroups;
716
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](10);
717
+ for (uint256 i; i < 10; i++) {
718
+ tierConfigs[i] = JB721TierConfig({
719
+ price: uint104((i + 1) * 10),
720
+ initialSupply: uint32(100),
721
+ votingUnits: uint16(0),
722
+ reserveFrequency: uint16(0),
723
+ reserveBeneficiary: reserveBeneficiary,
724
+ encodedIPFSUri: tokenUris[i],
725
+ category: uint24(100),
726
+ discountPercent: uint8(0),
727
+ flags: JB721TierConfigFlags({
728
+ allowOwnerMint: false,
729
+ useReserveBeneficiaryAsDefault: false,
730
+ transfersPausable: false,
731
+ useVotingUnits: true,
732
+ cantBeRemoved: false,
733
+ cantIncreaseDiscountPercent: false,
734
+ cantBuyWithCredits: false
735
+ }),
736
+ splitPercent: 0,
737
+ splits: new JBSplit[](0)
738
+ });
739
+ }
740
+ tiersHookConfig = JBDeploy721TiersHookConfig({
741
+ name: name,
742
+ symbol: symbol,
743
+ baseUri: baseUri,
744
+ tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
745
+ contractUri: contractUri,
746
+ tiersConfig: JB721InitTiersConfig({
747
+ tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
748
+ }),
749
+ flags: JB721TiersHookFlags({
750
+ preventOverspending: false,
751
+ issueTokensForSplits: false,
752
+ noNewTiersWithReserves: true,
753
+ noNewTiersWithVotes: true,
754
+ noNewTiersWithOwnerMinting: true
755
+ })
756
+ });
757
+
758
+ projectUri = "myIPFSHash";
759
+
760
+ metadata = JBPayDataHookRulesetMetadata({
761
+ reservedPercent: 5000, //50%
762
+ cashOutTaxRate: 5000, //50%
763
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
764
+ pausePay: false,
765
+ pauseCreditTransfers: false,
766
+ allowOwnerMinting: false,
767
+ allowSetCustomToken: false,
768
+ allowTerminalMigration: false,
769
+ allowSetTerminals: false,
770
+ allowSetController: false,
771
+ ownerMustSendPayouts: false,
772
+ allowAddAccountingContext: false,
773
+ allowAddPriceFeed: false,
774
+ holdFees: false,
775
+ useTotalSurplusForCashOuts: false,
776
+ useDataHookForCashOut: false,
777
+ metadata: 0x00
778
+ });
779
+
780
+ rulesetConfigurations = new JBPayDataHookRulesetConfig[](1);
781
+ rulesetConfigurations[0].mustStartAtOrAfter = 0;
782
+ rulesetConfigurations[0].duration = 14;
783
+ rulesetConfigurations[0].weight = 10 ** 18;
784
+ rulesetConfigurations[0].weightCutPercent = 450_000_000;
785
+ rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
786
+ rulesetConfigurations[0].metadata = metadata;
787
+ rulesetConfigurations[0].fundAccessLimitGroups = fundAccessLimitGroups;
788
+
789
+ terminalConfigurations = new JBTerminalConfig[](1);
790
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
791
+ accountingContextsToAccept[0] = JBAccountingContext({
792
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18, token: JBConstants.NATIVE_TOKEN
793
+ });
794
+ terminalConfigurations[0] = JBTerminalConfig({
795
+ terminal: IJBTerminal(mockTerminalAddress), accountingContextsToAccept: accountingContextsToAccept
796
+ });
797
+
798
+ launchProjectConfig = JBLaunchProjectConfig({
799
+ projectUri: projectUri,
800
+ rulesetConfigurations: rulesetConfigurations,
801
+ terminalConfigurations: terminalConfigurations,
802
+ memo: rulesetMemo
803
+ });
804
+ }
805
+ }