@bananapus/721-hook-v6 0.0.21 → 0.0.23

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.
@@ -0,0 +1,214 @@
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
+
14
+ /// @notice Regression test: split metadata is proportionally scaled when credits fund a split-bearing tier mint.
15
+ /// @dev Previously (pre-fix), the per-tier split amounts were left at the uncapped value, trapping forwarded ETH.
16
+ /// After the F-2 fix, split amounts are scaled down to match the actual forwarded amount.
17
+ contract CodexSplitCreditsMismatch is UnitTestSetup {
18
+ address internal splitBeneficiary = makeAddr("splitBeneficiary");
19
+
20
+ function setUp() public override {
21
+ super.setUp();
22
+ vm.etch(mockJBSplits, new bytes(0x69));
23
+ }
24
+
25
+ function _buildPayMetadata(
26
+ address hookAddress,
27
+ bool allowOverspending,
28
+ uint16[] memory tierIdsToMint
29
+ )
30
+ internal
31
+ view
32
+ returns (bytes memory)
33
+ {
34
+ bytes[] memory data = new bytes[](1);
35
+ data[0] = abi.encode(allowOverspending, tierIdsToMint);
36
+ bytes4[] memory ids = new bytes4[](1);
37
+ ids[0] = metadataHelper.getId("pay", hookAddress);
38
+ return metadataHelper.createMetadata(ids, data);
39
+ }
40
+
41
+ function _beforePayContext(
42
+ address hookAddress,
43
+ uint256 amountValue,
44
+ uint16[] memory tierIdsToMint
45
+ )
46
+ internal
47
+ view
48
+ returns (JBBeforePayRecordedContext memory)
49
+ {
50
+ return JBBeforePayRecordedContext({
51
+ terminal: mockTerminalAddress,
52
+ payer: beneficiary,
53
+ amount: JBTokenAmount({
54
+ token: JBConstants.NATIVE_TOKEN,
55
+ value: amountValue,
56
+ decimals: 18,
57
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
58
+ }),
59
+ projectId: projectId,
60
+ rulesetId: 0,
61
+ beneficiary: beneficiary,
62
+ weight: 10e18,
63
+ reservedPercent: 5000,
64
+ metadata: _buildPayMetadata(hookAddress, false, tierIdsToMint)
65
+ });
66
+ }
67
+
68
+ function _afterPayContext(
69
+ address hookAddress,
70
+ uint256 amountValue,
71
+ uint256 forwardedAmountValue,
72
+ bytes memory hookMetadata,
73
+ uint16[] memory tierIdsToMint
74
+ )
75
+ internal
76
+ view
77
+ returns (JBAfterPayRecordedContext memory)
78
+ {
79
+ return JBAfterPayRecordedContext({
80
+ payer: beneficiary,
81
+ projectId: projectId,
82
+ rulesetId: 0,
83
+ amount: JBTokenAmount({
84
+ token: JBConstants.NATIVE_TOKEN,
85
+ value: amountValue,
86
+ decimals: 18,
87
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
88
+ }),
89
+ forwardedAmount: JBTokenAmount({
90
+ token: JBConstants.NATIVE_TOKEN,
91
+ value: forwardedAmountValue,
92
+ decimals: 18,
93
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
94
+ }),
95
+ weight: 10e18,
96
+ newlyIssuedTokenCount: 0,
97
+ beneficiary: beneficiary,
98
+ hookMetadata: hookMetadata,
99
+ payerMetadata: _buildPayMetadata(hookAddress, true, tierIdsToMint)
100
+ });
101
+ }
102
+
103
+ function test_payCreditsScaleSplitMetadata_andForwardEth() public {
104
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
105
+ IJB721TiersHookStore hookStore = testHook.STORE();
106
+
107
+ vm.mockCall(
108
+ mockJBDirectory,
109
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
110
+ abi.encode(true)
111
+ );
112
+
113
+ // Tier costs 1 ether and routes 100% of its effective price to splits.
114
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
115
+ tierConfigs[0] = JB721TierConfig({
116
+ price: 1 ether,
117
+ initialSupply: 100,
118
+ votingUnits: 0,
119
+ reserveFrequency: 0,
120
+ reserveBeneficiary: reserveBeneficiary,
121
+ encodedIPFSUri: bytes32(uint256(0x1234)),
122
+ category: 1,
123
+ discountPercent: 0,
124
+ allowOwnerMint: false,
125
+ useReserveBeneficiaryAsDefault: false,
126
+ transfersPausable: false,
127
+ cannotBeRemoved: false,
128
+ cannotIncreaseDiscountPercent: false,
129
+ useVotingUnits: false,
130
+ splitPercent: 1_000_000_000,
131
+ splits: new JBSplit[](0)
132
+ });
133
+
134
+ vm.prank(address(testHook));
135
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
136
+
137
+ // Seed 1 ether of pay credits with an earlier overpayment.
138
+ uint16[] memory noTiers = new uint16[](0);
139
+ JBAfterPayRecordedContext memory creditSeedContext = JBAfterPayRecordedContext({
140
+ payer: beneficiary,
141
+ projectId: projectId,
142
+ rulesetId: 0,
143
+ amount: JBTokenAmount({
144
+ token: JBConstants.NATIVE_TOKEN,
145
+ value: 1 ether,
146
+ decimals: 18,
147
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
148
+ }),
149
+ forwardedAmount: JBTokenAmount({
150
+ token: JBConstants.NATIVE_TOKEN,
151
+ value: 0,
152
+ decimals: 18,
153
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
154
+ }),
155
+ weight: 10e18,
156
+ newlyIssuedTokenCount: 0,
157
+ beneficiary: beneficiary,
158
+ hookMetadata: "",
159
+ payerMetadata: _buildPayMetadata(address(testHook), true, noTiers)
160
+ });
161
+ vm.prank(mockTerminalAddress);
162
+ testHook.afterPayRecordedWith(creditSeedContext);
163
+ assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: pay credits should be seeded");
164
+
165
+ uint16[] memory mintIds = new uint16[](1);
166
+ mintIds[0] = uint16(tierIds[0]);
167
+
168
+ // beforePay caps the forwarded amount to the actual payment...
169
+ (, JBPayHookSpecification[] memory specs) =
170
+ testHook.beforePayRecordedWith(_beforePayContext(address(testHook), 1, mintIds));
171
+ assertEq(specs[0].amount, 1, "forwarded amount should be capped to actual payment");
172
+
173
+ // ...and proportionally scales the encoded per-tier split amounts to match the capped total.
174
+ (, uint256[] memory encodedAmounts) = abi.decode(specs[0].metadata, (uint16[], uint256[]));
175
+ assertEq(encodedAmounts.length, 1, "expected one encoded split amount");
176
+ assertEq(encodedAmounts[0], 1, "hook metadata should be scaled down to match forwarded amount");
177
+
178
+ // Route the split to a beneficiary and make project-balance fallback unavailable.
179
+ JBSplit[] memory splits = new JBSplit[](1);
180
+ splits[0] = JBSplit({
181
+ percent: 1_000_000_000,
182
+ projectId: 0,
183
+ beneficiary: payable(splitBeneficiary),
184
+ preferAddToBalance: false,
185
+ lockedUntil: 0,
186
+ hook: IJBSplitHook(address(0))
187
+ });
188
+
189
+ uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
190
+ vm.mockCall(
191
+ mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
192
+ );
193
+ vm.mockCall(
194
+ mockJBDirectory,
195
+ abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
196
+ abi.encode(address(0))
197
+ );
198
+
199
+ uint256 splitBeneficiaryBalanceBefore = splitBeneficiary.balance;
200
+
201
+ // The mint succeeds because pay credits cover the price. With the fix, the split IS honored.
202
+ JBAfterPayRecordedContext memory creditMintContext =
203
+ _afterPayContext(address(testHook), 1, 1, specs[0].metadata, mintIds);
204
+ vm.deal(mockTerminalAddress, 1);
205
+ vm.prank(mockTerminalAddress);
206
+ testHook.afterPayRecordedWith{value: 1}(creditMintContext);
207
+
208
+ assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary should still receive the NFT");
209
+ assertEq(testHook.payCreditsOf(beneficiary), 1, "only 1 wei of credits should remain");
210
+ // After fix: split beneficiary receives the forwarded ETH, nothing trapped.
211
+ assertEq(splitBeneficiary.balance - splitBeneficiaryBalanceBefore, 1, "split beneficiary should receive 1 wei");
212
+ assertEq(address(testHook).balance, 0, "no ETH should be trapped in the hook");
213
+ }
214
+ }
@@ -11,7 +11,7 @@ import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
11
11
  import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
12
12
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
13
13
 
14
- contract CodexNemesis_CrossCurrencySplitNoPrices is UnitTestSetup {
14
+ contract CrossCurrencySplitNoPrices is UnitTestSetup {
15
15
  function test_crossCurrencySplit_withoutPrices_locksForwardedNativeFunds() public {
16
16
  JB721TiersHook noPricesOrigin = new JB721TiersHook(
17
17
  IJBDirectory(mockJBDirectory),
@@ -75,7 +75,7 @@ contract CodexNemesis_CrossCurrencySplitNoPrices is UnitTestSetup {
75
75
  })
76
76
  );
77
77
 
78
- // When PRICES is address(0) and currencies differ, convertSplitAmounts returns 0
78
+ // When PRICES is address(0) and currencies differ, convertAndCapSplitAmounts returns 0
79
79
  // to avoid forwarding an unconverted amount in the wrong currency denomination.
80
80
  // This means weight is NOT reduced (full weight) and no funds are forwarded.
81
81
  assertEq(weight, 10e18, "weight unchanged when split conversion fails due to missing prices");
@@ -0,0 +1,142 @@
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 = abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether));
63
+
64
+ JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
65
+ payer: beneficiary,
66
+ projectId: projectId,
67
+ rulesetId: 0,
68
+ amount: JBTokenAmount({
69
+ token: JBConstants.NATIVE_TOKEN,
70
+ value: 1 ether,
71
+ decimals: 18,
72
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
73
+ }),
74
+ forwardedAmount: JBTokenAmount({
75
+ token: JBConstants.NATIVE_TOKEN,
76
+ value: 1 ether,
77
+ decimals: 18,
78
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
79
+ }),
80
+ weight: 10e18,
81
+ newlyIssuedTokenCount: 0,
82
+ beneficiary: beneficiary,
83
+ hookMetadata: hookMetadata,
84
+ payerMetadata: payerMetadata
85
+ });
86
+
87
+ // Mock primaryTerminalOf so the leftover (failed split amount) can be routed to the project balance.
88
+ address projectTerminal = makeAddr("projectTerminal");
89
+ vm.etch(projectTerminal, new bytes(0x69));
90
+ mockAndExpect(
91
+ address(mockJBDirectory),
92
+ abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
93
+ abi.encode(projectTerminal)
94
+ );
95
+ // Mock addToBalanceOf on the terminal so the leftover deposit succeeds.
96
+ vm.mockCall(
97
+ projectTerminal,
98
+ abi.encodeWithSelector(
99
+ IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 0.5 ether, false, "", ""
100
+ ),
101
+ ""
102
+ );
103
+
104
+ uint256 bobBalanceBefore = bob.balance;
105
+
106
+ vm.deal(mockTerminalAddress, 1 ether);
107
+ vm.prank(mockTerminalAddress);
108
+ testHook.afterPayRecordedWith{value: 1 ether}(payContext);
109
+
110
+ // With the fix, Bob only receives his fair 50% share. The failed split's 0.5 ether
111
+ // is routed to the project's balance via addToBalanceOf on the primary terminal.
112
+ assertEq(
113
+ bob.balance - bobBalanceBefore,
114
+ 0.5 ether,
115
+ "later split should only receive its own allocation, not the failed split's share"
116
+ );
117
+ }
118
+
119
+ function _buildPayMetadata(address hookAddress, uint16 tierId) internal view returns (bytes memory) {
120
+ bytes[] memory data = new bytes[](1);
121
+ data[0] = abi.encode(false, _singleTierId(tierId));
122
+ bytes4[] memory ids = new bytes4[](1);
123
+ ids[0] = metadataHelper.getId("pay", hookAddress);
124
+ return metadataHelper.createMetadata(ids, data);
125
+ }
126
+
127
+ function _singleTierId(uint16 tierId) internal pure returns (uint16[] memory tierIds) {
128
+ tierIds = new uint16[](1);
129
+ tierIds[0] = tierId;
130
+ }
131
+
132
+ function _singleAmount(uint256 amount) internal pure returns (uint256[] memory amounts) {
133
+ amounts = new uint256[](1);
134
+ amounts[0] = amount;
135
+ }
136
+ }
137
+
138
+ contract RevertOnReceive {
139
+ receive() external payable {
140
+ revert("NO_ETH");
141
+ }
142
+ }
@@ -249,7 +249,6 @@ contract ERC20CashOutFork is Test {
249
249
  tokenUriResolver: IJB721TokenUriResolver(address(0)),
250
250
  contractUri: "ipfs://contract",
251
251
  tiersConfig: JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: 6}),
252
- reserveBeneficiary: reserveBeneficiary,
253
252
  flags: JB721TiersHookFlags({
254
253
  preventOverspending: false,
255
254
  issueTokensForSplits: false,
@@ -226,7 +226,6 @@ contract ERC20TierSplitFork is Test {
226
226
  tokenUriResolver: IJB721TokenUriResolver(address(0)),
227
227
  contractUri: "ipfs://contract",
228
228
  tiersConfig: JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: tokenDecimals}),
229
- reserveBeneficiary: reserveBeneficiary,
230
229
  flags: JB721TiersHookFlags({
231
230
  preventOverspending: false,
232
231
  issueTokensForSplits: false,
@@ -300,7 +299,6 @@ contract ERC20TierSplitFork is Test {
300
299
  tiersConfig: JB721InitTiersConfig({
301
300
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
302
301
  }),
303
- reserveBeneficiary: reserveBeneficiary,
304
302
  flags: JB721TiersHookFlags({
305
303
  preventOverspending: false,
306
304
  issueTokensForSplits: false,
@@ -246,7 +246,6 @@ contract IssueTokensForSplitsFork is Test {
246
246
  currency: uint32(uint160(NATIVE_TOKEN)),
247
247
  decimals: 18
248
248
  }),
249
- reserveBeneficiary: reserveBeneficiary,
250
249
  flags: JB721TiersHookFlags({
251
250
  preventOverspending: false,
252
251
  issueTokensForSplits: issueTokensForSplits,
@@ -138,7 +138,6 @@ contract Test_ProjectDeployerRulesets is UnitTestSetup {
138
138
  tiersConfig: JB721InitTiersConfig({
139
139
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
140
140
  }),
141
- reserveBeneficiary: reserveBeneficiary,
142
141
  flags: JB721TiersHookFlags({
143
142
  preventOverspending: false,
144
143
  issueTokensForSplits: false,