@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,249 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
5
- import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
6
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
9
- import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
10
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
11
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
12
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
13
- import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
14
- import {JB721TiersHook} from "../../src/JB721TiersHook.sol";
15
- import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
16
- import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
17
- import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
18
- import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
19
- import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
20
-
21
- /// @notice Regression test for: same-currency decimal mismatch in split forwarding.
22
- /// @dev When pricing decimals differ from payment decimals but the currency is the same,
23
- /// `convertAndCapSplitAmounts` must rescale split amounts before comparing to `amountValue`.
24
- /// Without the fix, split amounts stay in pricing decimals (e.g. 18), the cap comparison uses
25
- /// payment decimals (e.g. 6), and the cap clips the split to 100% of the payment.
26
- contract SameCurrencyDecimalMismatch is UnitTestSetup {
27
- // Shared constants.
28
- address constant MOCK_TOKEN = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
29
- // forge-lint: disable-next-line(unsafe-typecast)
30
- uint32 constant CURRENCY = uint32(uint160(MOCK_TOKEN));
31
-
32
- /// @notice Prove that a 50% split with same currency but different decimals (pricing=18, payment=6)
33
- /// correctly forwards ~50% of the payment, not 100%.
34
- function test_sameCurrency_differentDecimals_splitAmountScaledCorrectly() public {
35
- // Deploy hook with PRICES=address(0), tier priced at 1e18 (18-decimal), 50% split.
36
- JB721TiersHook testHook;
37
- {
38
- JB721TiersHook origin = new JB721TiersHook(
39
- IJBDirectory(mockJBDirectory),
40
- IJBPermissions(mockJBPermissions),
41
- IJBPrices(address(0)),
42
- IJBRulesets(mockJBRulesets),
43
- store,
44
- IJBSplits(mockJBSplits),
45
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
46
- trustedForwarder
47
- );
48
- address hookAddr = makeAddr("hook18to6");
49
- vm.etch(hookAddr, address(origin).code);
50
- testHook = JB721TiersHook(hookAddr);
51
- }
52
-
53
- {
54
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
55
- tierConfigs[0].price = 1e18;
56
- tierConfigs[0].splitPercent = 500_000_000; // 50%.
57
- testHook.initialize(
58
- projectId,
59
- name,
60
- symbol,
61
- baseUri,
62
- IJB721TokenUriResolver(mockTokenUriResolver),
63
- contractUri,
64
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
65
- JB721TiersHookFlags({
66
- preventOverspending: false,
67
- issueTokensForSplits: false,
68
- noNewTiersWithReserves: false,
69
- noNewTiersWithVotes: false,
70
- noNewTiersWithOwnerMinting: false
71
- })
72
- );
73
- }
74
-
75
- // Build payer metadata requesting tier 1.
76
- bytes memory payerMetadata;
77
- {
78
- uint16[] memory tierIdsToMint = new uint16[](1);
79
- tierIdsToMint[0] = 1;
80
- bytes[] memory data = new bytes[](1);
81
- data[0] = abi.encode(true, tierIdsToMint);
82
- bytes4[] memory ids = new bytes4[](1);
83
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
84
- payerMetadata = metadataHelper.createMetadata(ids, data);
85
- }
86
-
87
- // Pay 1.0 token reported as 6 decimals (value = 1e6). Same currency, different decimals.
88
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
89
- JBBeforePayRecordedContext({
90
- terminal: mockTerminalAddress,
91
- payer: beneficiary,
92
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e6, decimals: 6, currency: CURRENCY}),
93
- projectId: projectId,
94
- rulesetId: 0,
95
- beneficiary: beneficiary,
96
- weight: 10e18,
97
- reservedPercent: 0,
98
- metadata: payerMetadata
99
- })
100
- );
101
-
102
- // Without the fix: split amount (5e17 in 18-decimal pricing) is compared to amountValue (1e6),
103
- // causing the cap to clip it to 1e6 (100% of payment) and weight becomes 0.
104
- // With the fix: split is rescaled to 5e5 (50% of 1e6) and weight is 5e18.
105
- assertEq(hookSpecs[0].amount, 5e5, "split should be 50% of payment (5e5), not capped to 100%");
106
- assertEq(weight, 5e18, "weight should be 50% (half goes to splits)");
107
- }
108
-
109
- /// @notice Sanity check: same currency AND same decimals — no rescaling needed.
110
- function test_sameCurrency_sameDecimals_splitAmountUnchanged() public {
111
- JB721TiersHook testHook;
112
- {
113
- JB721TiersHook origin = new JB721TiersHook(
114
- IJBDirectory(mockJBDirectory),
115
- IJBPermissions(mockJBPermissions),
116
- IJBPrices(address(0)),
117
- IJBRulesets(mockJBRulesets),
118
- store,
119
- IJBSplits(mockJBSplits),
120
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
121
- trustedForwarder
122
- );
123
- address hookAddr = makeAddr("hook18to18");
124
- vm.etch(hookAddr, address(origin).code);
125
- testHook = JB721TiersHook(hookAddr);
126
- }
127
-
128
- {
129
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
130
- tierConfigs[0].price = 1e18;
131
- tierConfigs[0].splitPercent = 500_000_000;
132
- testHook.initialize(
133
- projectId,
134
- name,
135
- symbol,
136
- baseUri,
137
- IJB721TokenUriResolver(mockTokenUriResolver),
138
- contractUri,
139
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
140
- JB721TiersHookFlags({
141
- preventOverspending: false,
142
- issueTokensForSplits: false,
143
- noNewTiersWithReserves: false,
144
- noNewTiersWithVotes: false,
145
- noNewTiersWithOwnerMinting: false
146
- })
147
- );
148
- }
149
-
150
- bytes memory payerMetadata;
151
- {
152
- uint16[] memory tierIdsToMint = new uint16[](1);
153
- tierIdsToMint[0] = 1;
154
- bytes[] memory data = new bytes[](1);
155
- data[0] = abi.encode(true, tierIdsToMint);
156
- bytes4[] memory ids = new bytes4[](1);
157
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
158
- payerMetadata = metadataHelper.createMetadata(ids, data);
159
- }
160
-
161
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
162
- JBBeforePayRecordedContext({
163
- terminal: mockTerminalAddress,
164
- payer: beneficiary,
165
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
166
- projectId: projectId,
167
- rulesetId: 0,
168
- beneficiary: beneficiary,
169
- weight: 10e18,
170
- reservedPercent: 0,
171
- metadata: payerMetadata
172
- })
173
- );
174
-
175
- assertEq(hookSpecs[0].amount, 5e17, "split should be 50% of payment");
176
- assertEq(weight, 5e18, "weight should be 50%");
177
- }
178
-
179
- /// @notice Same currency, payment has MORE decimals than pricing (pricing=6, payment=18).
180
- function test_sameCurrency_paymentMoreDecimals_splitScaledUp() public {
181
- JB721TiersHook testHook;
182
- {
183
- JB721TiersHook origin = new JB721TiersHook(
184
- IJBDirectory(mockJBDirectory),
185
- IJBPermissions(mockJBPermissions),
186
- IJBPrices(address(0)),
187
- IJBRulesets(mockJBRulesets),
188
- store,
189
- IJBSplits(mockJBSplits),
190
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
191
- trustedForwarder
192
- );
193
- address hookAddr = makeAddr("hook6to18");
194
- vm.etch(hookAddr, address(origin).code);
195
- testHook = JB721TiersHook(hookAddr);
196
- }
197
-
198
- {
199
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
200
- tierConfigs[0].price = 1e6; // 1.0 token in 6-decimal pricing.
201
- tierConfigs[0].splitPercent = 500_000_000;
202
- testHook.initialize(
203
- projectId,
204
- name,
205
- symbol,
206
- baseUri,
207
- IJB721TokenUriResolver(mockTokenUriResolver),
208
- contractUri,
209
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 6}),
210
- JB721TiersHookFlags({
211
- preventOverspending: false,
212
- issueTokensForSplits: false,
213
- noNewTiersWithReserves: false,
214
- noNewTiersWithVotes: false,
215
- noNewTiersWithOwnerMinting: false
216
- })
217
- );
218
- }
219
-
220
- bytes memory payerMetadata;
221
- {
222
- uint16[] memory tierIdsToMint = new uint16[](1);
223
- tierIdsToMint[0] = 1;
224
- bytes[] memory data = new bytes[](1);
225
- data[0] = abi.encode(true, tierIdsToMint);
226
- bytes4[] memory ids = new bytes4[](1);
227
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
228
- payerMetadata = metadataHelper.createMetadata(ids, data);
229
- }
230
-
231
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
232
- JBBeforePayRecordedContext({
233
- terminal: mockTerminalAddress,
234
- payer: beneficiary,
235
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
236
- projectId: projectId,
237
- rulesetId: 0,
238
- beneficiary: beneficiary,
239
- weight: 10e18,
240
- reservedPercent: 0,
241
- metadata: payerMetadata
242
- })
243
- );
244
-
245
- // 50% of 1.0 token in 18-decimal payment = 5e17.
246
- assertEq(hookSpecs[0].amount, 5e17, "split scaled up to 18-decimal payment");
247
- assertEq(weight, 5e18, "weight should be 50%");
248
- }
249
- }
@@ -1,219 +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 {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
9
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
10
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
11
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
12
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
13
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
14
-
15
- /// @notice Regression test: split metadata is proportionally scaled when credits fund a split-bearing tier mint.
16
- /// @dev Previously (pre-fix), the per-tier split amounts were left at the uncapped value, trapping forwarded ETH.
17
- /// After the F-2 fix, split amounts are scaled down to match the actual forwarded amount.
18
- contract SplitCreditsMismatch is UnitTestSetup {
19
- address internal splitBeneficiary = makeAddr("splitBeneficiary");
20
-
21
- function setUp() public override {
22
- super.setUp();
23
- vm.etch(mockJBSplits, new bytes(0x69));
24
- }
25
-
26
- function _buildPayMetadata(
27
- address hookAddress,
28
- bool allowOverspending,
29
- uint16[] memory tierIdsToMint
30
- )
31
- internal
32
- view
33
- returns (bytes memory)
34
- {
35
- bytes[] memory data = new bytes[](1);
36
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
37
- bytes4[] memory ids = new bytes4[](1);
38
- ids[0] = metadataHelper.getId("pay", hookAddress);
39
- return metadataHelper.createMetadata(ids, data);
40
- }
41
-
42
- function _beforePayContext(
43
- address hookAddress,
44
- uint256 amountValue,
45
- uint16[] memory tierIdsToMint
46
- )
47
- internal
48
- view
49
- returns (JBBeforePayRecordedContext memory)
50
- {
51
- return JBBeforePayRecordedContext({
52
- terminal: mockTerminalAddress,
53
- payer: beneficiary,
54
- amount: JBTokenAmount({
55
- token: JBConstants.NATIVE_TOKEN,
56
- value: amountValue,
57
- decimals: 18,
58
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
59
- }),
60
- projectId: projectId,
61
- rulesetId: 0,
62
- beneficiary: beneficiary,
63
- weight: 10e18,
64
- reservedPercent: 5000,
65
- metadata: _buildPayMetadata(hookAddress, false, tierIdsToMint)
66
- });
67
- }
68
-
69
- function _afterPayContext(
70
- address hookAddress,
71
- uint256 amountValue,
72
- uint256 forwardedAmountValue,
73
- bytes memory hookMetadata,
74
- uint16[] memory tierIdsToMint
75
- )
76
- internal
77
- view
78
- returns (JBAfterPayRecordedContext memory)
79
- {
80
- return JBAfterPayRecordedContext({
81
- payer: beneficiary,
82
- projectId: projectId,
83
- rulesetId: 0,
84
- amount: JBTokenAmount({
85
- token: JBConstants.NATIVE_TOKEN,
86
- value: amountValue,
87
- decimals: 18,
88
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
89
- }),
90
- forwardedAmount: JBTokenAmount({
91
- token: JBConstants.NATIVE_TOKEN,
92
- value: forwardedAmountValue,
93
- decimals: 18,
94
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
95
- }),
96
- weight: 10e18,
97
- newlyIssuedTokenCount: 0,
98
- beneficiary: beneficiary,
99
- hookMetadata: hookMetadata,
100
- payerMetadata: _buildPayMetadata(hookAddress, true, tierIdsToMint)
101
- });
102
- }
103
-
104
- function test_payCreditsScaleSplitMetadata_andForwardEth() public {
105
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
106
- IJB721TiersHookStore hookStore = testHook.STORE();
107
-
108
- vm.mockCall(
109
- mockJBDirectory,
110
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
111
- abi.encode(true)
112
- );
113
-
114
- // Tier costs 1 ether and routes 100% of its effective price to splits.
115
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
116
- tierConfigs[0] = JB721TierConfig({
117
- price: 1 ether,
118
- initialSupply: 100,
119
- votingUnits: 0,
120
- reserveFrequency: 0,
121
- reserveBeneficiary: reserveBeneficiary,
122
- encodedIPFSUri: bytes32(uint256(0x1234)),
123
- category: 1,
124
- discountPercent: 0,
125
- flags: JB721TierConfigFlags({
126
- allowOwnerMint: false,
127
- useReserveBeneficiaryAsDefault: false,
128
- transfersPausable: false,
129
- useVotingUnits: false,
130
- cantBeRemoved: false,
131
- cantIncreaseDiscountPercent: false,
132
- cantBuyWithCredits: false
133
- }),
134
- splitPercent: 1_000_000_000,
135
- splits: new JBSplit[](0)
136
- });
137
-
138
- vm.prank(address(testHook));
139
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
140
-
141
- // Seed 1 ether of pay credits with an earlier overpayment.
142
- uint16[] memory noTiers = new uint16[](0);
143
- JBAfterPayRecordedContext memory creditSeedContext = JBAfterPayRecordedContext({
144
- payer: beneficiary,
145
- projectId: projectId,
146
- rulesetId: 0,
147
- amount: JBTokenAmount({
148
- token: JBConstants.NATIVE_TOKEN,
149
- value: 1 ether,
150
- decimals: 18,
151
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
152
- }),
153
- forwardedAmount: JBTokenAmount({
154
- token: JBConstants.NATIVE_TOKEN,
155
- value: 0,
156
- decimals: 18,
157
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
158
- }),
159
- weight: 10e18,
160
- newlyIssuedTokenCount: 0,
161
- beneficiary: beneficiary,
162
- hookMetadata: "",
163
- payerMetadata: _buildPayMetadata(address(testHook), true, noTiers)
164
- });
165
- vm.prank(mockTerminalAddress);
166
- testHook.afterPayRecordedWith(creditSeedContext);
167
- assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: pay credits should be seeded");
168
-
169
- uint16[] memory mintIds = new uint16[](1);
170
- mintIds[0] = uint16(tierIds[0]);
171
-
172
- // beforePay caps the forwarded amount to the actual payment...
173
- (, JBPayHookSpecification[] memory specs) =
174
- testHook.beforePayRecordedWith(_beforePayContext(address(testHook), 1, mintIds));
175
- assertEq(specs[0].amount, 1, "forwarded amount should be capped to actual payment");
176
-
177
- // ...and proportionally scales the encoded per-tier split amounts to match the capped total.
178
- (,, bytes memory splitData) = abi.decode(specs[0].metadata, (address, address, bytes));
179
- (, uint256[] memory encodedAmounts) = abi.decode(splitData, (uint16[], uint256[]));
180
- assertEq(encodedAmounts.length, 1, "expected one encoded split amount");
181
- assertEq(encodedAmounts[0], 1, "hook metadata should be scaled down to match forwarded amount");
182
-
183
- // Route the split to a beneficiary and make project-balance fallback unavailable.
184
- JBSplit[] memory splits = new JBSplit[](1);
185
- splits[0] = JBSplit({
186
- percent: 1_000_000_000,
187
- projectId: 0,
188
- beneficiary: payable(splitBeneficiary),
189
- preferAddToBalance: false,
190
- lockedUntil: 0,
191
- hook: IJBSplitHook(address(0))
192
- });
193
-
194
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
195
- vm.mockCall(
196
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
197
- );
198
- vm.mockCall(
199
- mockJBDirectory,
200
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
201
- abi.encode(address(0))
202
- );
203
-
204
- uint256 splitBeneficiaryBalanceBefore = splitBeneficiary.balance;
205
-
206
- // The mint succeeds because pay credits cover the price. With the fix, the split IS honored.
207
- JBAfterPayRecordedContext memory creditMintContext =
208
- _afterPayContext(address(testHook), 1, 1, specs[0].metadata, mintIds);
209
- vm.deal(mockTerminalAddress, 1);
210
- vm.prank(mockTerminalAddress);
211
- testHook.afterPayRecordedWith{value: 1}(creditMintContext);
212
-
213
- assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary should still receive the NFT");
214
- assertEq(testHook.payCreditsOf(beneficiary), 1, "only 1 wei of credits should remain");
215
- // After fix: split beneficiary receives the forwarded ETH, nothing trapped.
216
- assertEq(splitBeneficiary.balance - splitBeneficiaryBalanceBefore, 1, "split beneficiary should receive 1 wei");
217
- assertEq(address(testHook).balance, 0, "no ETH should be trapped in the hook");
218
- }
219
- }
@@ -1,143 +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 {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
8
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
9
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
11
-
12
- contract SplitFailureRedistribution is UnitTestSetup {
13
- address internal alice = makeAddr("alice");
14
- address internal bob = makeAddr("bob");
15
-
16
- function test_failedEarlierSplit_doesNotOverpayLaterSplit() public {
17
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
18
- IJB721TiersHookStore hookStore = testHook.STORE();
19
-
20
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
21
- tierConfigs[0].price = 1 ether;
22
- tierConfigs[0].initialSupply = uint32(100);
23
- tierConfigs[0].category = uint24(1);
24
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
25
- tierConfigs[0].splitPercent = 1_000_000_000;
26
-
27
- vm.prank(address(testHook));
28
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
29
-
30
- mockAndExpect(
31
- address(mockJBDirectory),
32
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
33
- abi.encode(true)
34
- );
35
-
36
- RevertOnReceive revertingBeneficiary = new RevertOnReceive();
37
-
38
- JBSplit[] memory splits = new JBSplit[](2);
39
- splits[0] = JBSplit({
40
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
41
- projectId: 0,
42
- beneficiary: payable(address(revertingBeneficiary)),
43
- preferAddToBalance: false,
44
- lockedUntil: 0,
45
- hook: IJBSplitHook(address(0))
46
- });
47
- splits[1] = JBSplit({
48
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
49
- projectId: 0,
50
- beneficiary: payable(bob),
51
- preferAddToBalance: false,
52
- lockedUntil: 0,
53
- hook: IJBSplitHook(address(0))
54
- });
55
-
56
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
57
- mockAndExpect(
58
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
59
- );
60
-
61
- bytes memory payerMetadata = _buildPayMetadata(address(testHook), uint16(tierIds[0]));
62
- bytes memory hookMetadata =
63
- abi.encode(beneficiary, beneficiary, abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether)));
64
-
65
- JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
66
- payer: beneficiary,
67
- projectId: projectId,
68
- rulesetId: 0,
69
- amount: JBTokenAmount({
70
- token: JBConstants.NATIVE_TOKEN,
71
- value: 1 ether,
72
- decimals: 18,
73
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
74
- }),
75
- forwardedAmount: JBTokenAmount({
76
- token: JBConstants.NATIVE_TOKEN,
77
- value: 1 ether,
78
- decimals: 18,
79
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
80
- }),
81
- weight: 10e18,
82
- newlyIssuedTokenCount: 0,
83
- beneficiary: beneficiary,
84
- hookMetadata: hookMetadata,
85
- payerMetadata: payerMetadata
86
- });
87
-
88
- // Mock primaryTerminalOf so the leftover (failed split amount) can be routed to the project balance.
89
- address projectTerminal = makeAddr("projectTerminal");
90
- vm.etch(projectTerminal, new bytes(0x69));
91
- mockAndExpect(
92
- address(mockJBDirectory),
93
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
94
- abi.encode(projectTerminal)
95
- );
96
- // Mock addToBalanceOf on the terminal so the leftover deposit succeeds.
97
- vm.mockCall(
98
- projectTerminal,
99
- abi.encodeWithSelector(
100
- IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 0.5 ether, false, "", ""
101
- ),
102
- ""
103
- );
104
-
105
- uint256 bobBalanceBefore = bob.balance;
106
-
107
- vm.deal(mockTerminalAddress, 1 ether);
108
- vm.prank(mockTerminalAddress);
109
- testHook.afterPayRecordedWith{value: 1 ether}(payContext);
110
-
111
- // With the fix, Bob only receives his fair 50% share. The failed split's 0.5 ether
112
- // is routed to the project's balance via addToBalanceOf on the primary terminal.
113
- assertEq(
114
- bob.balance - bobBalanceBefore,
115
- 0.5 ether,
116
- "later split should only receive its own allocation, not the failed split's share"
117
- );
118
- }
119
-
120
- function _buildPayMetadata(address hookAddress, uint16 tierId) internal view returns (bytes memory) {
121
- bytes[] memory data = new bytes[](1);
122
- data[0] = abi.encode(false, _singleTierId(tierId));
123
- bytes4[] memory ids = new bytes4[](1);
124
- ids[0] = metadataHelper.getId("pay", hookAddress);
125
- return metadataHelper.createMetadata(ids, data);
126
- }
127
-
128
- function _singleTierId(uint16 tierId) internal pure returns (uint16[] memory tierIds) {
129
- tierIds = new uint16[](1);
130
- tierIds[0] = tierId;
131
- }
132
-
133
- function _singleAmount(uint256 amount) internal pure returns (uint256[] memory amounts) {
134
- amounts = new uint256[](1);
135
- amounts[0] = amount;
136
- }
137
- }
138
-
139
- contract RevertOnReceive {
140
- receive() external payable {
141
- revert("NO_ETH");
142
- }
143
- }