@bananapus/721-hook-v6 0.0.42 → 0.0.45

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 (86) 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 +61 -19
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +66 -53
  8. package/src/JB721TiersHookDeployer.sol +8 -5
  9. package/src/JB721TiersHookProjectDeployer.sol +87 -46
  10. package/src/JB721TiersHookStore.sol +137 -107
  11. package/src/abstract/JB721Hook.sol +8 -6
  12. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  13. package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
  14. package/src/interfaces/IJB721TiersHook.sol +3 -3
  15. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
  16. package/src/interfaces/IJB721TiersHookStore.sol +11 -11
  17. package/src/libraries/JB721TiersHookLib.sol +1 -1
  18. package/src/structs/JB721TiersHookFlags.sol +1 -1
  19. package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
  20. package/test/utils/AccessJBLib.sol +49 -0
  21. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  22. package/test/utils/TestBaseWorkflow.sol +213 -0
  23. package/test/utils/UnitTestSetup.sol +805 -0
  24. package/.gas-snapshot +0 -152
  25. package/ADMINISTRATION.md +0 -87
  26. package/ARCHITECTURE.md +0 -98
  27. package/AUDIT_INSTRUCTIONS.md +0 -77
  28. package/RISKS.md +0 -118
  29. package/SKILLS.md +0 -43
  30. package/STYLE_GUIDE.md +0 -610
  31. package/USER_JOURNEYS.md +0 -121
  32. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  33. package/slither-ci.config.json +0 -10
  34. package/test/721HookAttacks.t.sol +0 -408
  35. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  36. package/test/Fork.t.sol +0 -2346
  37. package/test/TestAuditGaps.sol +0 -1075
  38. package/test/TestCheckpoints.t.sol +0 -341
  39. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  40. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  41. package/test/audit/AuditRegressions.t.sol +0 -83
  42. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  43. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  44. package/test/audit/FreshAudit.t.sol +0 -197
  45. package/test/audit/FutureTierPoC.t.sol +0 -39
  46. package/test/audit/FutureTierRemoval.t.sol +0 -47
  47. package/test/audit/Pass12L18.t.sol +0 -80
  48. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  49. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  50. package/test/audit/RepoFindings.t.sol +0 -195
  51. package/test/audit/ReserveActivation.t.sol +0 -87
  52. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  53. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  54. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  55. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  56. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  57. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  58. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  59. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  60. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  61. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  62. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  63. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  64. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  65. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  66. package/test/regression/CacheTierLookup.t.sol +0 -190
  67. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  68. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  69. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  70. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  71. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  72. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  73. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  74. package/test/unit/JBBitmap.t.sol +0 -170
  75. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  76. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  77. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  78. package/test/unit/deployer_Unit.t.sol +0 -114
  79. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  80. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  81. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  82. package/test/unit/pay_Unit.t.sol +0 -1661
  83. package/test/unit/redeem_Unit.t.sol +0 -473
  84. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  85. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  86. 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
- }