@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
@@ -1,757 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "../utils/UnitTestSetup.sol";
6
- import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
7
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
8
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
9
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
10
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
11
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
12
- import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
13
- import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
14
-
15
- contract MockERC20 is ERC20 {
16
- constructor() ERC20("Mock Token", "MOCK") {}
17
-
18
- function mint(address to, uint256 amount) external {
19
- _mint(to, amount);
20
- }
21
- }
22
-
23
- contract Test_TierSplitRouting is UnitTestSetup {
24
- using stdStorage for StdStorage;
25
-
26
- address alice = makeAddr("alice");
27
- address bob = makeAddr("bob");
28
-
29
- function setUp() public override {
30
- super.setUp();
31
- vm.etch(mockJBSplits, new bytes(0x69));
32
- }
33
-
34
- // Helper: build a tier config with splits.
35
- function _tierConfigWithSplit(
36
- uint104 price,
37
- uint32 splitPercent
38
- )
39
- internal
40
- pure
41
- returns (JB721TierConfig memory config)
42
- {
43
- config.price = price;
44
- config.initialSupply = uint32(100);
45
- config.category = uint24(1);
46
- config.encodedIPFSUri = bytes32(uint256(0x1234));
47
- config.splitPercent = splitPercent;
48
- }
49
-
50
- // Helper: build payer metadata for tier IDs.
51
- function _buildPayerMetadata(
52
- address hookAddress,
53
- uint16[] memory tierIdsToMint
54
- )
55
- internal
56
- view
57
- returns (bytes memory)
58
- {
59
- bytes[] memory data = new bytes[](1);
60
- data[0] = abi.encode(false, tierIdsToMint);
61
- bytes4[] memory ids = new bytes4[](1);
62
- ids[0] = metadataHelper.getId("pay", hookAddress);
63
- return metadataHelper.createMetadata(ids, data);
64
- }
65
-
66
- // ──────────────────────────────────────────────
67
- // Test: beforePayRecordedWith calculates split amount
68
- // ──────────────────────────────────────────────
69
-
70
- function test_beforePayRecorded_calculatesSplitAmount() public {
71
- // Create hook with a default tier (no splits).
72
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
73
- IJB721TiersHookStore hookStore = testHook.STORE();
74
-
75
- // Add a tier with 50% split directly to the hook's store.
76
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
77
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
78
- vm.prank(address(testHook));
79
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
80
-
81
- // Build payer metadata requesting that tier.
82
- uint16[] memory mintIds = new uint16[](1);
83
- mintIds[0] = uint16(tierIds[0]);
84
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
85
-
86
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
87
- terminal: mockTerminalAddress,
88
- payer: beneficiary,
89
- amount: JBTokenAmount({
90
- token: JBConstants.NATIVE_TOKEN,
91
- value: 1 ether,
92
- decimals: 18,
93
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
94
- }),
95
- projectId: projectId,
96
- rulesetId: 0,
97
- beneficiary: beneficiary,
98
- weight: 10e18,
99
- reservedPercent: 5000,
100
- metadata: payerMetadata
101
- });
102
-
103
- (uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
104
-
105
- // Weight adjusted for 50% split: 10e18 * 0.5 = 5e18.
106
- assertEq(weight, 5e18);
107
- // Hook spec should forward 50% of 1 ETH = 0.5 ETH.
108
- assertEq(specs.length, 1);
109
- assertEq(specs[0].amount, 0.5 ether);
110
- }
111
-
112
- // ──────────────────────────────────────────────
113
- // Test: no splitPercent means no forwarded amount
114
- // ──────────────────────────────────────────────
115
-
116
- function test_beforePayRecorded_noSplitPercent_noForwardedAmount() public {
117
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
118
- IJB721TiersHookStore hookStore = testHook.STORE();
119
-
120
- // Add a tier with 0% split.
121
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
122
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
123
- vm.prank(address(testHook));
124
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
125
-
126
- uint16[] memory mintIds = new uint16[](1);
127
- mintIds[0] = uint16(tierIds[0]);
128
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
129
-
130
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
131
- terminal: mockTerminalAddress,
132
- payer: beneficiary,
133
- amount: JBTokenAmount({
134
- token: JBConstants.NATIVE_TOKEN,
135
- value: 1 ether,
136
- decimals: 18,
137
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
138
- }),
139
- projectId: projectId,
140
- rulesetId: 0,
141
- beneficiary: beneficiary,
142
- weight: 10e18,
143
- reservedPercent: 5000,
144
- metadata: payerMetadata
145
- });
146
-
147
- (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
148
-
149
- // No split amount forwarded.
150
- assertEq(specs[0].amount, 0);
151
- }
152
-
153
- // ──────────────────────────────────────────────
154
- // Test: multiple tiers with different split percents
155
- // ──────────────────────────────────────────────
156
-
157
- function test_beforePayRecorded_multipleTiersDifferentSplitPercents() public {
158
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
159
- IJB721TiersHookStore hookStore = testHook.STORE();
160
-
161
- // Tier 1: 1 ETH, 30% split. Tier 2: 2 ETH, 100% split.
162
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
163
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000);
164
- tierConfigs[0].category = 1;
165
- tierConfigs[1] = _tierConfigWithSplit(2 ether, 1_000_000_000);
166
- tierConfigs[1].category = 2;
167
- vm.prank(address(testHook));
168
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
169
-
170
- uint16[] memory mintIds = new uint16[](2);
171
- mintIds[0] = uint16(tierIds[0]);
172
- mintIds[1] = uint16(tierIds[1]);
173
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
174
-
175
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
176
- terminal: mockTerminalAddress,
177
- payer: beneficiary,
178
- amount: JBTokenAmount({
179
- token: JBConstants.NATIVE_TOKEN,
180
- value: 3 ether,
181
- decimals: 18,
182
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
183
- }),
184
- projectId: projectId,
185
- rulesetId: 0,
186
- beneficiary: beneficiary,
187
- weight: 10e18,
188
- reservedPercent: 5000,
189
- metadata: payerMetadata
190
- });
191
-
192
- (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
193
-
194
- // Total split = 1 ETH * 30% + 2 ETH * 100% = 0.3 + 2.0 = 2.3 ETH.
195
- assertEq(specs[0].amount, 2.3 ether);
196
- }
197
-
198
- // ──────────────────────────────────────────────
199
- // Test: weight adjusted for splits
200
- // ──────────────────────────────────────────────
201
-
202
- function test_beforePayRecorded_weightAdjustedForSplits() public {
203
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
204
- IJB721TiersHookStore hookStore = testHook.STORE();
205
-
206
- // Add a tier with 30% split.
207
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
208
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000); // 30%
209
- vm.prank(address(testHook));
210
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
211
-
212
- uint16[] memory mintIds = new uint16[](1);
213
- mintIds[0] = uint16(tierIds[0]);
214
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
215
-
216
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
217
- terminal: mockTerminalAddress,
218
- payer: beneficiary,
219
- amount: JBTokenAmount({
220
- token: JBConstants.NATIVE_TOKEN,
221
- value: 1 ether,
222
- decimals: 18,
223
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
224
- }),
225
- projectId: projectId,
226
- rulesetId: 0,
227
- beneficiary: beneficiary,
228
- weight: 10e18,
229
- reservedPercent: 5000,
230
- metadata: payerMetadata
231
- });
232
-
233
- (uint256 weight,) = testHook.beforePayRecordedWith(context);
234
-
235
- // Weight adjusted for 30% split: 10e18 * 0.7 = 7e18.
236
- assertEq(weight, 7e18);
237
- }
238
-
239
- function test_beforePayRecorded_noSplitPercent_weightUnchanged() public {
240
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
241
- IJB721TiersHookStore hookStore = testHook.STORE();
242
-
243
- // Add a tier with 0% split.
244
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
245
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
246
- vm.prank(address(testHook));
247
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
248
-
249
- uint16[] memory mintIds = new uint16[](1);
250
- mintIds[0] = uint16(tierIds[0]);
251
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
252
-
253
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
254
- terminal: mockTerminalAddress,
255
- payer: beneficiary,
256
- amount: JBTokenAmount({
257
- token: JBConstants.NATIVE_TOKEN,
258
- value: 1 ether,
259
- decimals: 18,
260
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
261
- }),
262
- projectId: projectId,
263
- rulesetId: 0,
264
- beneficiary: beneficiary,
265
- weight: 10e18,
266
- reservedPercent: 5000,
267
- metadata: payerMetadata
268
- });
269
-
270
- (uint256 weight,) = testHook.beforePayRecordedWith(context);
271
-
272
- // No split = weight unchanged.
273
- assertEq(weight, 10e18);
274
- }
275
-
276
- function test_beforePayRecorded_fullSplitPercent_weightZero() public {
277
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
278
- IJB721TiersHookStore hookStore = testHook.STORE();
279
-
280
- // Add a tier with 100% split, priced at full payment amount.
281
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
282
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000); // 100%
283
- vm.prank(address(testHook));
284
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
285
-
286
- uint16[] memory mintIds = new uint16[](1);
287
- mintIds[0] = uint16(tierIds[0]);
288
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
289
-
290
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
291
- terminal: mockTerminalAddress,
292
- payer: beneficiary,
293
- amount: JBTokenAmount({
294
- token: JBConstants.NATIVE_TOKEN,
295
- value: 1 ether,
296
- decimals: 18,
297
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
298
- }),
299
- projectId: projectId,
300
- rulesetId: 0,
301
- beneficiary: beneficiary,
302
- weight: 10e18,
303
- reservedPercent: 5000,
304
- metadata: payerMetadata
305
- });
306
-
307
- (uint256 weight,) = testHook.beforePayRecordedWith(context);
308
-
309
- // 100% split of full amount = weight 0.
310
- assertEq(weight, 0);
311
- }
312
-
313
- // ──────────────────────────────────────────────
314
- // Test: afterPayRecordedWith distributes to split beneficiary
315
- // ──────────────────────────────────────────────
316
-
317
- function test_afterPayRecorded_distributesToBeneficiary() public {
318
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
319
- IJB721TiersHookStore hookStore = testHook.STORE();
320
-
321
- // Add a tier with 50% split.
322
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
323
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000);
324
- vm.prank(address(testHook));
325
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
326
-
327
- // Mock directory checks.
328
- mockAndExpect(
329
- address(mockJBDirectory),
330
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
331
- abi.encode(true)
332
- );
333
-
334
- // Mock splits: alice gets 100%.
335
- JBSplit[] memory splits = new JBSplit[](1);
336
- splits[0] = JBSplit({
337
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
338
- projectId: 0,
339
- beneficiary: payable(alice),
340
- preferAddToBalance: false,
341
- lockedUntil: 0,
342
- hook: IJBSplitHook(address(0))
343
- });
344
-
345
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
346
- mockAndExpect(
347
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
348
- );
349
-
350
- // Build payer metadata.
351
- uint16[] memory mintIds = new uint16[](1);
352
- mintIds[0] = uint16(tierIds[0]);
353
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
354
-
355
- // Build hook metadata (per-tier split breakdown from beforePayRecordedWith).
356
- uint16[] memory splitTierIds = new uint16[](1);
357
- splitTierIds[0] = uint16(tierIds[0]);
358
- uint256[] memory splitAmounts = new uint256[](1);
359
- splitAmounts[0] = 0.5 ether;
360
-
361
- JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
362
- payer: beneficiary,
363
- projectId: projectId,
364
- rulesetId: 0,
365
- amount: JBTokenAmount({
366
- token: JBConstants.NATIVE_TOKEN,
367
- value: 1 ether,
368
- decimals: 18,
369
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
370
- }),
371
- forwardedAmount: JBTokenAmount({
372
- token: JBConstants.NATIVE_TOKEN,
373
- value: 0.5 ether,
374
- decimals: 18,
375
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
376
- }),
377
- weight: 10e18,
378
- newlyIssuedTokenCount: 0,
379
- beneficiary: beneficiary,
380
- hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
381
- payerMetadata: payerMetadata
382
- });
383
-
384
- uint256 aliceBalanceBefore = alice.balance;
385
-
386
- vm.deal(mockTerminalAddress, 1 ether);
387
- vm.prank(mockTerminalAddress);
388
- testHook.afterPayRecordedWith{value: 0.5 ether}(payContext);
389
-
390
- // Alice should have received 0.5 ETH.
391
- assertEq(alice.balance - aliceBalanceBefore, 0.5 ether);
392
- // NFT should have been minted.
393
- assertEq(testHook.balanceOf(beneficiary), 1);
394
- }
395
-
396
- // ──────────────────────────────────────────────
397
- // Test: issueTokensForSplits flag gives full weight with partial splits
398
- // ──────────────────────────────────────────────
399
-
400
- function test_beforePayRecorded_issueTokensForSplits_fullWeight() public {
401
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
402
- IJB721TiersHookStore hookStore = testHook.STORE();
403
-
404
- // Set the issueTokensForSplits flag.
405
- vm.prank(address(testHook));
406
- hookStore.recordFlags(
407
- JB721TiersHookFlags({
408
- noNewTiersWithReserves: false,
409
- noNewTiersWithVotes: false,
410
- noNewTiersWithOwnerMinting: false,
411
- preventOverspending: false,
412
- issueTokensForSplits: true
413
- })
414
- );
415
-
416
- // Add a tier with 50% split.
417
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
418
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
419
- vm.prank(address(testHook));
420
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
421
-
422
- uint16[] memory mintIds = new uint16[](1);
423
- mintIds[0] = uint16(tierIds[0]);
424
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
425
-
426
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
427
- terminal: mockTerminalAddress,
428
- payer: beneficiary,
429
- amount: JBTokenAmount({
430
- token: JBConstants.NATIVE_TOKEN,
431
- value: 1 ether,
432
- decimals: 18,
433
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
434
- }),
435
- projectId: projectId,
436
- rulesetId: 0,
437
- beneficiary: beneficiary,
438
- weight: 10e18,
439
- reservedPercent: 5000,
440
- metadata: payerMetadata
441
- });
442
-
443
- (uint256 weight,) = testHook.beforePayRecordedWith(context);
444
-
445
- // Flag set — weight should be full despite 50% split.
446
- assertEq(weight, 10e18);
447
- }
448
-
449
- // ──────────────────────────────────────────────
450
- // Test: issueTokensForSplits flag gives full weight even when splits consume entire payment
451
- // ──────────────────────────────────────────────
452
-
453
- function test_beforePayRecorded_issueTokensForSplits_fullSplit_stillFullWeight() public {
454
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
455
- IJB721TiersHookStore hookStore = testHook.STORE();
456
-
457
- // Set the issueTokensForSplits flag.
458
- vm.prank(address(testHook));
459
- hookStore.recordFlags(
460
- JB721TiersHookFlags({
461
- noNewTiersWithReserves: false,
462
- noNewTiersWithVotes: false,
463
- noNewTiersWithOwnerMinting: false,
464
- preventOverspending: false,
465
- issueTokensForSplits: true
466
- })
467
- );
468
-
469
- // Add a tier with 100% split, priced at full payment amount.
470
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
471
- tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000); // 100%
472
- vm.prank(address(testHook));
473
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
474
-
475
- uint16[] memory mintIds = new uint16[](1);
476
- mintIds[0] = uint16(tierIds[0]);
477
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
478
-
479
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
480
- terminal: mockTerminalAddress,
481
- payer: beneficiary,
482
- amount: JBTokenAmount({
483
- token: JBConstants.NATIVE_TOKEN,
484
- value: 1 ether,
485
- decimals: 18,
486
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
487
- }),
488
- projectId: projectId,
489
- rulesetId: 0,
490
- beneficiary: beneficiary,
491
- weight: 10e18,
492
- reservedPercent: 5000,
493
- metadata: payerMetadata
494
- });
495
-
496
- (uint256 weight,) = testHook.beforePayRecordedWith(context);
497
-
498
- // Flag set — weight should be full despite 100% split consuming entire payment.
499
- assertEq(weight, 10e18);
500
- }
501
-
502
- // ──────────────────────────────────────────────
503
- // ERC20 Tests: afterPayRecordedWith with ERC20 tokens
504
- // ──────────────────────────────────────────────
505
-
506
- /// @notice Helper: set up an ERC20 tier split test. Returns (hook, tierIds, mockToken).
507
- // forge-lint: disable-next-line(mixed-case-function)
508
- function _setupERC20TierSplit()
509
- internal
510
- returns (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token)
511
- {
512
- // Deploy mock ERC20.
513
- token = new MockERC20();
514
-
515
- // Initialize hook with ERC20 currency (0 default tiers).
516
- testHook = _initHookDefaultTiers(0, false, uint32(uint160(address(token))), 18);
517
- IJB721TiersHookStore hookStore = testHook.STORE();
518
-
519
- // Add a tier with 50% split, priced at 100 tokens.
520
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
521
- tierConfigs[0] = _tierConfigWithSplit(100, 500_000_000); // 50%
522
- vm.prank(address(testHook));
523
- tierIds = hookStore.recordAddTiers(tierConfigs);
524
-
525
- // Mock directory checks.
526
- mockAndExpect(
527
- address(mockJBDirectory),
528
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
529
- abi.encode(true)
530
- );
531
- }
532
-
533
- /// @notice Helper: build afterPayRecordedWith context for ERC20 payments.
534
- // forge-lint: disable-next-line(mixed-case-function)
535
- function _buildERC20PayContext(
536
- JB721TiersHook testHook,
537
- uint256[] memory tierIds,
538
- address token,
539
- uint256 payAmount,
540
- uint256 forwardedAmount
541
- )
542
- internal
543
- view
544
- returns (JBAfterPayRecordedContext memory)
545
- {
546
- uint16[] memory mintIds = new uint16[](1);
547
- mintIds[0] = uint16(tierIds[0]);
548
- // Use METADATA_ID_TARGET (the original hook address) for metadata resolution.
549
- bytes memory payerMetadata = _buildPayerMetadata(testHook.METADATA_ID_TARGET(), mintIds);
550
-
551
- uint16[] memory splitTierIds = new uint16[](1);
552
- splitTierIds[0] = uint16(tierIds[0]);
553
- uint256[] memory splitAmounts = new uint256[](1);
554
- splitAmounts[0] = forwardedAmount;
555
-
556
- return JBAfterPayRecordedContext({
557
- payer: beneficiary,
558
- projectId: projectId,
559
- rulesetId: 0,
560
- // forge-lint: disable-next-line(unsafe-typecast)
561
- amount: JBTokenAmount({token: token, value: payAmount, decimals: 18, currency: uint32(uint160(token))}),
562
- forwardedAmount: JBTokenAmount({
563
- // forge-lint: disable-next-line(unsafe-typecast)
564
- token: token,
565
- value: forwardedAmount,
566
- decimals: 18,
567
- // forge-lint: disable-next-line(unsafe-typecast)
568
- currency: uint32(uint160(token))
569
- }),
570
- weight: 10e18,
571
- newlyIssuedTokenCount: 0,
572
- beneficiary: beneficiary,
573
- hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
574
- payerMetadata: payerMetadata
575
- });
576
- }
577
-
578
- function test_afterPayRecorded_erc20_distributesToBeneficiary() public {
579
- (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
580
-
581
- // Mock splits: alice gets 100%.
582
- JBSplit[] memory splits = new JBSplit[](1);
583
- splits[0] = JBSplit({
584
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
585
- projectId: 0,
586
- beneficiary: payable(alice),
587
- preferAddToBalance: false,
588
- lockedUntil: 0,
589
- hook: IJBSplitHook(address(0))
590
- });
591
-
592
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
593
- mockAndExpect(
594
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
595
- );
596
-
597
- // Give terminal the ERC20 tokens and approve the hook.
598
- token.mint(mockTerminalAddress, 100);
599
- vm.prank(mockTerminalAddress);
600
- token.approve(address(testHook), 50);
601
-
602
- JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
603
-
604
- vm.prank(mockTerminalAddress);
605
- testHook.afterPayRecordedWith(payContext);
606
-
607
- // Alice should have received 50 tokens.
608
- assertEq(token.balanceOf(alice), 50);
609
- // NFT should have been minted.
610
- assertEq(testHook.balanceOf(beneficiary), 1);
611
- }
612
-
613
- function test_afterPayRecorded_erc20_splitToProject_addToBalance() public {
614
- (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
615
-
616
- // Target project for split.
617
- uint256 targetProjectId = 99;
618
- address targetTerminal = makeAddr("targetTerminal");
619
- vm.etch(targetTerminal, new bytes(0x69));
620
-
621
- // Mock splits: 100% to target project with preferAddToBalance.
622
- JBSplit[] memory splits = new JBSplit[](1);
623
- splits[0] = JBSplit({
624
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
625
- // forge-lint: disable-next-line(unsafe-typecast)
626
- projectId: uint56(targetProjectId),
627
- beneficiary: payable(address(0)),
628
- preferAddToBalance: true,
629
- lockedUntil: 0,
630
- hook: IJBSplitHook(address(0))
631
- });
632
-
633
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
634
- mockAndExpect(
635
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
636
- );
637
-
638
- // Mock directory: target project's primary terminal.
639
- mockAndExpect(
640
- address(mockJBDirectory),
641
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
642
- abi.encode(targetTerminal)
643
- );
644
-
645
- // Mock the addToBalanceOf call on the target terminal.
646
- vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
647
-
648
- // Give terminal the ERC20 tokens and approve the hook.
649
- token.mint(mockTerminalAddress, 100);
650
- vm.prank(mockTerminalAddress);
651
- token.approve(address(testHook), 50);
652
-
653
- JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
654
-
655
- vm.prank(mockTerminalAddress);
656
- testHook.afterPayRecordedWith(payContext);
657
-
658
- // Hook approved the target terminal (library calls forceApprove before addToBalanceOf).
659
- // Mock terminal doesn't pull, so hook still holds the tokens. Verify approval was set.
660
- assertGe(token.allowance(address(testHook), targetTerminal), 50);
661
- assertEq(testHook.balanceOf(beneficiary), 1);
662
- }
663
-
664
- function test_afterPayRecorded_erc20_splitToProject_pay() public {
665
- (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
666
-
667
- uint256 targetProjectId = 99;
668
- address targetTerminal = makeAddr("targetTerminal");
669
- vm.etch(targetTerminal, new bytes(0x69));
670
-
671
- // Mock splits: 100% to target project with preferAddToBalance = false (pay).
672
- JBSplit[] memory splits = new JBSplit[](1);
673
- splits[0] = JBSplit({
674
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
675
- // forge-lint: disable-next-line(unsafe-typecast)
676
- projectId: uint56(targetProjectId),
677
- beneficiary: payable(alice),
678
- preferAddToBalance: false,
679
- lockedUntil: 0,
680
- hook: IJBSplitHook(address(0))
681
- });
682
-
683
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
684
- mockAndExpect(
685
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
686
- );
687
-
688
- // Mock directory: target project's primary terminal.
689
- mockAndExpect(
690
- address(mockJBDirectory),
691
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
692
- abi.encode(targetTerminal)
693
- );
694
-
695
- // Mock the pay call on the target terminal.
696
- vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(0));
697
-
698
- // Give terminal the ERC20 tokens and approve the hook.
699
- token.mint(mockTerminalAddress, 100);
700
- vm.prank(mockTerminalAddress);
701
- token.approve(address(testHook), 50);
702
-
703
- JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
704
-
705
- vm.prank(mockTerminalAddress);
706
- testHook.afterPayRecordedWith(payContext);
707
-
708
- // Hook approved the target terminal (library calls forceApprove before pay).
709
- assertGe(token.allowance(address(testHook), targetTerminal), 50);
710
- assertEq(testHook.balanceOf(beneficiary), 1);
711
- }
712
-
713
- function test_afterPayRecorded_erc20_noBeneficiary_routesToProjectBalance() public {
714
- (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
715
-
716
- // Mock splits: no beneficiary and no projectId — leftover goes to project balance.
717
- JBSplit[] memory splits = new JBSplit[](1);
718
- splits[0] = JBSplit({
719
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
720
- projectId: 0,
721
- beneficiary: payable(address(0)),
722
- preferAddToBalance: false,
723
- lockedUntil: 0,
724
- hook: IJBSplitHook(address(0))
725
- });
726
-
727
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
728
- mockAndExpect(
729
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
730
- );
731
-
732
- // Mock the project's primary terminal for the leftover addToBalance.
733
- address projectTerminal = makeAddr("projectTerminal");
734
- vm.etch(projectTerminal, new bytes(0x69));
735
- mockAndExpect(
736
- address(mockJBDirectory),
737
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, address(token)),
738
- abi.encode(projectTerminal)
739
- );
740
-
741
- vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
742
-
743
- // Give terminal the ERC20 tokens and approve the hook.
744
- token.mint(mockTerminalAddress, 100);
745
- vm.prank(mockTerminalAddress);
746
- token.approve(address(testHook), 50);
747
-
748
- JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
749
-
750
- vm.prank(mockTerminalAddress);
751
- testHook.afterPayRecordedWith(payContext);
752
-
753
- // Hook approved the project terminal (library calls forceApprove before addToBalanceOf).
754
- assertGe(token.allowance(address(testHook), projectTerminal), 50);
755
- assertEq(testHook.balanceOf(beneficiary), 1);
756
- }
757
- }