@bananapus/721-hook-v6 0.0.32 → 0.0.34

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 (35) hide show
  1. package/USER_JOURNEYS.md +11 -0
  2. package/package.json +3 -3
  3. package/script/Deploy.s.sol +53 -19
  4. package/src/JB721Checkpoints.sol +92 -0
  5. package/src/JB721CheckpointsDeployer.sol +45 -0
  6. package/src/JB721TiersHook.sol +90 -116
  7. package/src/abstract/JB721Hook.sol +5 -0
  8. package/src/interfaces/IJB721Checkpoints.sol +34 -0
  9. package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
  10. package/src/interfaces/IJB721TiersHook.sol +8 -0
  11. package/src/libraries/JB721Constants.sol +6 -0
  12. package/src/libraries/JB721TiersHookLib.sol +353 -146
  13. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
  14. package/test/Fork.t.sol +11 -2
  15. package/test/TestAuditGaps.sol +1 -1
  16. package/test/TestCheckpoints.t.sol +329 -0
  17. package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
  18. package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
  19. package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
  20. package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
  21. package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
  22. package/test/audit/SplitFailureRedistribution.t.sol +2 -1
  23. package/test/fork/ERC20CashOutFork.t.sol +11 -2
  24. package/test/fork/ERC20TierSplitFork.t.sol +11 -2
  25. package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
  26. package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
  27. package/test/regression/SplitDistributionBugs.t.sol +5 -5
  28. package/test/regression/SplitNoBeneficiary.t.sol +1 -1
  29. package/test/unit/AuditFixes_Unit.t.sol +5 -5
  30. package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
  31. package/test/unit/pay_Unit.t.sol +1 -0
  32. package/test/unit/redeem_Unit.t.sol +3 -3
  33. package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
  34. package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
  35. package/test/unit/tierSplitRouting_Unit.t.sol +2 -2
@@ -0,0 +1,161 @@
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 {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
+ import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
8
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
+
10
+ contract CodexRetroactiveReserveBeneficiaryDilution is UnitTestSetup {
11
+ function _buildPayMetadata(address hookAddress, uint16[] memory tierIdsToMint)
12
+ internal
13
+ view
14
+ returns (bytes memory)
15
+ {
16
+ bytes[] memory data = new bytes[](1);
17
+ data[0] = abi.encode(true, tierIdsToMint);
18
+ bytes4[] memory ids = new bytes4[](1);
19
+ ids[0] = metadataHelper.getId("pay", hookAddress);
20
+ return metadataHelper.createMetadata(ids, data);
21
+ }
22
+
23
+ function _afterPayContext(
24
+ address hookAddress,
25
+ uint256 amountValue,
26
+ uint16[] memory tierIdsToMint
27
+ )
28
+ internal
29
+ view
30
+ returns (JBAfterPayRecordedContext memory)
31
+ {
32
+ return JBAfterPayRecordedContext({
33
+ payer: beneficiary,
34
+ projectId: projectId,
35
+ rulesetId: 0,
36
+ amount: JBTokenAmount({
37
+ token: JBConstants.NATIVE_TOKEN,
38
+ value: amountValue,
39
+ decimals: 18,
40
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
41
+ }),
42
+ forwardedAmount: JBTokenAmount({
43
+ token: JBConstants.NATIVE_TOKEN,
44
+ value: 0,
45
+ decimals: 18,
46
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
47
+ }),
48
+ weight: 10e18,
49
+ newlyIssuedTokenCount: 0,
50
+ beneficiary: beneficiary,
51
+ hookMetadata: "",
52
+ payerMetadata: _buildPayMetadata(hookAddress, tierIdsToMint)
53
+ });
54
+ }
55
+
56
+ function test_adjustTier_can_retroactively_create_reserves_for_existing_supply() public {
57
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
58
+
59
+ JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
60
+ tier1[0] = JB721TierConfig({
61
+ price: 1 ether,
62
+ initialSupply: 100,
63
+ votingUnits: 0,
64
+ reserveFrequency: 2,
65
+ reserveBeneficiary: address(0),
66
+ encodedIPFSUri: bytes32(uint256(0x1234)),
67
+ category: 1,
68
+ discountPercent: 0,
69
+ flags: JB721TierConfigFlags({
70
+ allowOwnerMint: false,
71
+ useReserveBeneficiaryAsDefault: false,
72
+ transfersPausable: false,
73
+ useVotingUnits: false,
74
+ cantBeRemoved: false,
75
+ cantIncreaseDiscountPercent: false,
76
+ cantBuyWithCredits: false
77
+ }),
78
+ splitPercent: 0,
79
+ splits: new JBSplit[](0)
80
+ });
81
+
82
+ vm.prank(owner);
83
+ testHook.adjustTiers(tier1, new uint256[](0));
84
+
85
+ assertEq(
86
+ testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
87
+ 0,
88
+ "no beneficiary means no reserves are pending"
89
+ );
90
+
91
+ vm.mockCall(
92
+ mockJBDirectory,
93
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
94
+ abi.encode(true)
95
+ );
96
+
97
+ uint16[] memory mintIds = new uint16[](3);
98
+ mintIds[0] = 1;
99
+ mintIds[1] = 1;
100
+ mintIds[2] = 1;
101
+
102
+ JBAfterPayRecordedContext memory payContext = _afterPayContext(address(testHook), 3 ether, mintIds);
103
+ vm.prank(mockTerminalAddress);
104
+ testHook.afterPayRecordedWith(payContext);
105
+
106
+ assertEq(testHook.balanceOf(beneficiary), 3, "beneficiary should own the three paid NFTs");
107
+ assertEq(testHook.totalCashOutWeight(), 3 ether, "denominator initially reflects only sold NFTs");
108
+
109
+ JB721TierConfig[] memory tier2 = new JB721TierConfig[](1);
110
+ tier2[0] = JB721TierConfig({
111
+ price: 2 ether,
112
+ initialSupply: 100,
113
+ votingUnits: 0,
114
+ reserveFrequency: 1,
115
+ reserveBeneficiary: owner,
116
+ encodedIPFSUri: bytes32(uint256(0x5678)),
117
+ category: 2,
118
+ discountPercent: 0,
119
+ flags: JB721TierConfigFlags({
120
+ allowOwnerMint: false,
121
+ useReserveBeneficiaryAsDefault: true,
122
+ transfersPausable: false,
123
+ useVotingUnits: false,
124
+ cantBeRemoved: false,
125
+ cantIncreaseDiscountPercent: false,
126
+ cantBuyWithCredits: false
127
+ }),
128
+ splitPercent: 0,
129
+ splits: new JBSplit[](0)
130
+ });
131
+
132
+ vm.prank(owner);
133
+ testHook.adjustTiers(tier2, new uint256[](0));
134
+
135
+ assertEq(
136
+ testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
137
+ owner,
138
+ "new default beneficiary retroactively applies to the old tier"
139
+ );
140
+ assertEq(
141
+ testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
142
+ 2,
143
+ "historical sales now generate retroactive pending reserves"
144
+ );
145
+ assertEq(
146
+ testHook.totalCashOutWeight(),
147
+ 5 ether,
148
+ "existing holders are diluted before any new payment enters the system"
149
+ );
150
+
151
+ vm.prank(address(0xBEEF));
152
+ testHook.mintPendingReservesFor(1, 2);
153
+
154
+ assertEq(testHook.balanceOf(owner), 2, "retroactive reserves mint directly to the new default beneficiary");
155
+ assertEq(
156
+ testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
157
+ 0,
158
+ "all retroactive reserve entitlement can be extracted"
159
+ );
160
+ }
161
+ }
@@ -175,7 +175,8 @@ contract CodexSplitCreditsMismatch is UnitTestSetup {
175
175
  assertEq(specs[0].amount, 1, "forwarded amount should be capped to actual payment");
176
176
 
177
177
  // ...and proportionally scales the encoded per-tier split amounts to match the capped total.
178
- (, uint256[] memory encodedAmounts) = abi.decode(specs[0].metadata, (uint16[], uint256[]));
178
+ (,, bytes memory splitData) = abi.decode(specs[0].metadata, (address, address, bytes));
179
+ (, uint256[] memory encodedAmounts) = abi.decode(splitData, (uint16[], uint256[]));
179
180
  assertEq(encodedAmounts.length, 1, "expected one encoded split amount");
180
181
  assertEq(encodedAmounts[0], 1, "hook metadata should be scaled down to match forwarded amount");
181
182
 
@@ -20,6 +20,7 @@ contract CrossCurrencySplitNoPrices is UnitTestSetup {
20
20
  IJBRulesets(mockJBRulesets),
21
21
  store,
22
22
  IJBSplits(mockJBSplits),
23
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
23
24
  trustedForwarder
24
25
  );
25
26
 
@@ -0,0 +1,249 @@
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
+ }
@@ -59,7 +59,8 @@ contract SplitFailureRedistribution is UnitTestSetup {
59
59
  );
60
60
 
61
61
  bytes memory payerMetadata = _buildPayMetadata(address(testHook), uint16(tierIds[0]));
62
- bytes memory hookMetadata = abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether));
62
+ bytes memory hookMetadata =
63
+ abi.encode(beneficiary, beneficiary, abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether)));
63
64
 
64
65
  JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
65
66
  payer: beneficiary,
@@ -70,6 +70,8 @@ import "../../src/JB721TiersHookDeployer.sol";
70
70
  import "../../src/JB721TiersHookProjectDeployer.sol";
71
71
  // forge-lint: disable-next-line(unaliased-plain-import)
72
72
  import "../../src/JB721TiersHookStore.sol";
73
+ import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
74
+ import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
73
75
  // forge-lint: disable-next-line(unaliased-plain-import)
74
76
  import "../../src/interfaces/IJB721TiersHook.sol";
75
77
  // forge-lint: disable-next-line(unaliased-plain-import)
@@ -176,7 +178,7 @@ contract ERC20CashOutFork is Test {
176
178
  jbPermissions = new JBPermissions(address(0));
177
179
  jbProjects = new JBProjects(multisig, address(0), address(0));
178
180
  jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
179
- JBERC20 jbErc20 = new JBERC20();
181
+ JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
180
182
  jbTokens = new JBTokens(jbDirectory, jbErc20);
181
183
  jbRulesets = new JBRulesets(jbDirectory);
182
184
  jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
@@ -217,7 +219,14 @@ contract ERC20CashOutFork is Test {
217
219
  function _deploy721Hook() internal {
218
220
  store = new JB721TiersHookStore();
219
221
  hookImpl = new JB721TiersHook(
220
- jbDirectory, jbPermissions, jbPrices, jbRulesets, store, IJBSplits(address(jbSplits)), address(0)
222
+ jbDirectory,
223
+ jbPermissions,
224
+ jbPrices,
225
+ jbRulesets,
226
+ store,
227
+ IJBSplits(address(jbSplits)),
228
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
229
+ address(0)
221
230
  );
222
231
  addressRegistry = new JBAddressRegistry();
223
232
  hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
@@ -70,6 +70,8 @@ import "../../src/JB721TiersHookDeployer.sol";
70
70
  import "../../src/JB721TiersHookProjectDeployer.sol";
71
71
  // forge-lint: disable-next-line(unaliased-plain-import)
72
72
  import "../../src/JB721TiersHookStore.sol";
73
+ import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
74
+ import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
73
75
  // forge-lint: disable-next-line(unaliased-plain-import)
74
76
  import "../../src/interfaces/IJB721TiersHook.sol";
75
77
  // forge-lint: disable-next-line(unaliased-plain-import)
@@ -153,7 +155,7 @@ contract ERC20TierSplitFork is Test {
153
155
  jbPermissions = new JBPermissions(address(0));
154
156
  jbProjects = new JBProjects(multisig, address(0), address(0));
155
157
  jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
156
- JBERC20 jbErc20 = new JBERC20();
158
+ JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
157
159
  jbTokens = new JBTokens(jbDirectory, jbErc20);
158
160
  jbRulesets = new JBRulesets(jbDirectory);
159
161
  jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
@@ -194,7 +196,14 @@ contract ERC20TierSplitFork is Test {
194
196
  function _deploy721Hook() internal {
195
197
  store = new JB721TiersHookStore();
196
198
  hookImpl = new JB721TiersHook(
197
- jbDirectory, jbPermissions, jbPrices, jbRulesets, store, IJBSplits(address(jbSplits)), address(0)
199
+ jbDirectory,
200
+ jbPermissions,
201
+ jbPrices,
202
+ jbRulesets,
203
+ store,
204
+ IJBSplits(address(jbSplits)),
205
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
206
+ address(0)
198
207
  );
199
208
  addressRegistry = new JBAddressRegistry();
200
209
  hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
@@ -68,6 +68,8 @@ import "../../src/JB721TiersHookDeployer.sol";
68
68
  import "../../src/JB721TiersHookProjectDeployer.sol";
69
69
  // forge-lint: disable-next-line(unaliased-plain-import)
70
70
  import "../../src/JB721TiersHookStore.sol";
71
+ import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
72
+ import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
71
73
  // forge-lint: disable-next-line(unaliased-plain-import)
72
74
  import "../../src/interfaces/IJB721TiersHook.sol";
73
75
  // forge-lint: disable-next-line(unaliased-plain-import)
@@ -160,7 +162,7 @@ contract IssueTokensForSplitsFork is Test {
160
162
  jbPermissions = new JBPermissions(address(0));
161
163
  jbProjects = new JBProjects(multisig, address(0), address(0));
162
164
  jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
163
- JBERC20 jbErc20 = new JBERC20();
165
+ JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
164
166
  jbTokens = new JBTokens(jbDirectory, jbErc20);
165
167
  jbRulesets = new JBRulesets(jbDirectory);
166
168
  jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
@@ -208,7 +210,14 @@ contract IssueTokensForSplitsFork is Test {
208
210
  function _deploy721Hook() internal {
209
211
  store = new JB721TiersHookStore();
210
212
  hookImpl = new JB721TiersHook(
211
- jbDirectory, jbPermissions, jbPrices, jbRulesets, store, IJBSplits(address(jbSplits)), address(0)
213
+ jbDirectory,
214
+ jbPermissions,
215
+ jbPrices,
216
+ jbRulesets,
217
+ store,
218
+ IJBSplits(address(jbSplits)),
219
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
220
+ address(0)
212
221
  );
213
222
  addressRegistry = new JBAddressRegistry();
214
223
  hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
@@ -69,7 +69,7 @@ contract Test_BrokenTerminalDoesNotDos is UnitTestSetup {
69
69
  weight: 10e18,
70
70
  newlyIssuedTokenCount: 0,
71
71
  beneficiary: beneficiary,
72
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
72
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
73
73
  payerMetadata: payerMetadata
74
74
  });
75
75
  }
@@ -163,7 +163,7 @@ contract Test_BrokenTerminalDoesNotDos is UnitTestSetup {
163
163
  weight: 10e18,
164
164
  newlyIssuedTokenCount: 0,
165
165
  beneficiary: beneficiary,
166
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
166
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
167
167
  payerMetadata: payerMetadata
168
168
  });
169
169
 
@@ -133,7 +133,7 @@ contract Test_SplitDistributionBugs is UnitTestSetup {
133
133
  weight: 10e18,
134
134
  newlyIssuedTokenCount: 0,
135
135
  beneficiary: beneficiary,
136
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
136
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
137
137
  payerMetadata: payerMetadata
138
138
  });
139
139
 
@@ -350,7 +350,7 @@ contract Test_SplitDistributionBugs is UnitTestSetup {
350
350
  weight: 10e18,
351
351
  newlyIssuedTokenCount: 0,
352
352
  beneficiary: beneficiary,
353
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
353
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
354
354
  payerMetadata: payerMetadata
355
355
  });
356
356
 
@@ -455,7 +455,7 @@ contract Test_SplitDistributionBugs is UnitTestSetup {
455
455
  weight: 10e18,
456
456
  newlyIssuedTokenCount: 0,
457
457
  beneficiary: beneficiary,
458
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
458
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
459
459
  payerMetadata: payerMetadata
460
460
  });
461
461
 
@@ -566,7 +566,7 @@ contract Test_SplitDistributionBugs is UnitTestSetup {
566
566
  weight: 10e18,
567
567
  newlyIssuedTokenCount: 0,
568
568
  beneficiary: beneficiary,
569
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
569
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
570
570
  payerMetadata: payerMetadata
571
571
  });
572
572
  }
@@ -673,7 +673,7 @@ contract Test_SplitDistributionBugs is UnitTestSetup {
673
673
  weight: 10e18,
674
674
  newlyIssuedTokenCount: 0,
675
675
  beneficiary: beneficiary,
676
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
676
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
677
677
  payerMetadata: payerMetadata
678
678
  });
679
679
 
@@ -112,7 +112,7 @@ contract Test_L36_SplitNoBeneficiary is UnitTestSetup {
112
112
  weight: 10e18,
113
113
  newlyIssuedTokenCount: 0,
114
114
  beneficiary: beneficiary,
115
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
115
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
116
116
  payerMetadata: payerMetadata
117
117
  });
118
118
 
@@ -115,9 +115,9 @@ contract Test_AuditFixes_Unit is UnitTestSetup {
115
115
  // The total forwarded amount must be capped at the payment value (may be up to 1 wei less due to rounding).
116
116
  assertApproxEqAbs(specs[0].amount, 2 ether, 1, "Total split should be capped at payment value");
117
117
 
118
- // Decode the per-tier breakdown from hookMetadata.
119
- (uint16[] memory resultTierIds, uint256[] memory resultAmounts) =
120
- abi.decode(specs[0].metadata, (uint16[], uint256[]));
118
+ // Decode the per-tier breakdown from hookMetadata (unwrap beneficiary/payer wrapper first).
119
+ (,, bytes memory splitData) = abi.decode(specs[0].metadata, (address, address, bytes));
120
+ (uint16[] memory resultTierIds, uint256[] memory resultAmounts) = abi.decode(splitData, (uint16[], uint256[]));
121
121
 
122
122
  // Both tiers should be present in the metadata.
123
123
  assertEq(resultTierIds.length, 2, "Should have 2 tier entries");
@@ -260,7 +260,7 @@ contract Test_AuditFixes_Unit is UnitTestSetup {
260
260
  weight: 10e18,
261
261
  newlyIssuedTokenCount: 0,
262
262
  beneficiary: beneficiary,
263
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
263
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
264
264
  payerMetadata: payerMetadata
265
265
  });
266
266
 
@@ -341,7 +341,7 @@ contract Test_AuditFixes_Unit is UnitTestSetup {
341
341
  weight: 10e18,
342
342
  newlyIssuedTokenCount: 0,
343
343
  beneficiary: beneficiary,
344
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
344
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
345
345
  payerMetadata: payerMetadata
346
346
  });
347
347
 
@@ -221,6 +221,7 @@ contract Test_crossCurrencyPay_Unit is UnitTestSetup {
221
221
  IJBRulesets(mockJBRulesets),
222
222
  IJB721TiersHookStore(store),
223
223
  IJBSplits(mockJBSplits),
224
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
224
225
  trustedForwarder
225
226
  );
226
227
 
@@ -1006,6 +1006,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup {
1006
1006
  IJBRulesets(mockJBRulesets),
1007
1007
  IJB721TiersHookStore(store),
1008
1008
  IJBSplits(mockJBSplits),
1009
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
1009
1010
  trustedForwarder
1010
1011
  );
1011
1012
  address noPricesProxy = makeAddr("noPricesProxy2");