@bananapus/suckers-v6 0.0.12 → 0.0.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@arbitrum/nitro-contracts": "^1.2.1",
22
- "@bananapus/core-v6": "^0.0.17",
22
+ "@bananapus/core-v6": "^0.0.23",
23
23
  "@bananapus/permission-ids-v6": "^0.0.10",
24
24
  "@chainlink/contracts-ccip": "^1.5.0",
25
25
  "@chainlink/local": "github:smartcontractkit/chainlink-local",
package/src/JBSucker.sol CHANGED
@@ -700,7 +700,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
700
700
  minReturnedTokens: 0,
701
701
  memo: "",
702
702
  metadata: ""
703
- }) {}
703
+ }) returns (
704
+ uint256
705
+ ) {}
704
706
  catch {
705
707
  // Fee payment failed — proceed without fee, return it as transport payment.
706
708
  transportPayment = msg.value;
@@ -0,0 +1,559 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
5
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
6
+ import {IJBSucker} from "../src/interfaces/IJBSucker.sol";
7
+ import {IJBSuckerDeployer} from "../src/interfaces/IJBSuckerDeployer.sol";
8
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
10
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
11
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
12
+ import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
13
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
14
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
15
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
16
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
17
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
18
+ import {ICCIPRouter} from "src/interfaces/ICCIPRouter.sol";
19
+ import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
20
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
21
+
22
+ import {JBTokenMapping} from "../src/structs/JBTokenMapping.sol";
23
+ import {JBRemoteToken} from "../src/structs/JBRemoteToken.sol";
24
+ import {JBOutboxTree} from "../src/structs/JBOutboxTree.sol";
25
+ import {JBInboxTreeRoot} from "../src/structs/JBInboxTreeRoot.sol";
26
+ import {JBMessageRoot} from "../src/structs/JBMessageRoot.sol";
27
+ import {JBClaim} from "../src/structs/JBClaim.sol";
28
+ import {JBLeaf} from "../src/structs/JBLeaf.sol";
29
+
30
+ import {MerkleLib} from "../src/utils/MerkleLib.sol";
31
+
32
+ import "forge-std/Test.sol";
33
+ import {JBCCIPSuckerDeployer} from "src/deployers/JBCCIPSuckerDeployer.sol";
34
+ import {JBCCIPSucker} from "../src/JBCCIPSucker.sol";
35
+ import {BurnMintERC677Helper} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol";
36
+ import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol";
37
+
38
+ import {IJBSuckerRegistry} from "../src/interfaces/IJBSuckerRegistry.sol";
39
+ import {CCIPHelper} from "../src/libraries/CCIPHelper.sol";
40
+ import {JBSucker} from "../src/JBSucker.sol";
41
+
42
+ /// @notice Fork test that exercises the full prepare -> toRemote -> CCIP deliver -> claim flow
43
+ /// on Sepolia <-> Arbitrum Sepolia, verifying merkle proof verification and double-claim prevention.
44
+ contract CCIPSuckerForkClaimTests is TestBaseWorkflow {
45
+ // CCIP Local Simulator Contracts
46
+ CCIPLocalSimulatorFork ccipLocalSimulatorFork;
47
+ BurnMintERC677Helper ccipBnM;
48
+ BurnMintERC677Helper ccipBnMArbSepolia;
49
+
50
+ // Re-used parameters for project/ruleset/sucker setups
51
+ JBRulesetMetadata _metadata;
52
+
53
+ // Sucker and token
54
+ JBCCIPSuckerDeployer suckerDeployer;
55
+ JBCCIPSuckerDeployer suckerDeployer2;
56
+ IJBSucker suckerL1;
57
+ IJBToken projectOneToken;
58
+
59
+ // Chain ids and selectors
60
+ uint256 sepoliaFork;
61
+ uint256 arbSepoliaFork;
62
+ uint64 arbSepoliaChainSelector = 3_478_487_238_524_512_106;
63
+ uint64 ethSepoliaChainSelector = 16_015_286_601_757_825_753;
64
+
65
+ // RPCs -- named endpoints from foundry.toml [rpc_endpoints].
66
+ string ETHEREUM_SEPOLIA_RPC_URL = "ethereum_sepolia";
67
+ string ARBITRUM_SEPOLIA_RPC_URL = "arbitrum_sepolia";
68
+
69
+ //*********************************************************************//
70
+ // ----------------------------- Events ------------------------------ //
71
+ //*********************************************************************//
72
+
73
+ /// @dev Mirror of IJBSucker.InsertToOutboxTree so we can capture it with vm.expectEmit / recordLogs.
74
+ event InsertToOutboxTree(
75
+ bytes32 indexed beneficiary,
76
+ address indexed token,
77
+ bytes32 hashed,
78
+ uint256 index,
79
+ bytes32 root,
80
+ uint256 projectTokenCount,
81
+ uint256 terminalTokenAmount,
82
+ address caller
83
+ );
84
+
85
+ //*********************************************************************//
86
+ // ---------------------------- Setup parts -------------------------- //
87
+ //*********************************************************************//
88
+
89
+ function initL1AndUtils() public {
90
+ // Setup starts on sepolia fork
91
+ sepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
92
+
93
+ ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
94
+ vm.makePersistent(address(ccipLocalSimulatorFork));
95
+ Register.NetworkDetails memory sepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);
96
+
97
+ ccipBnM = BurnMintERC677Helper(sepoliaNetworkDetails.ccipBnMAddress);
98
+ vm.label(address(ccipBnM), "bnmEthSep");
99
+ vm.makePersistent(address(ccipBnM));
100
+ }
101
+
102
+ function initMetadata() public {
103
+ _metadata = JBRulesetMetadata({
104
+ reservedPercent: JBConstants.MAX_RESERVED_PERCENT / 2, //50%
105
+ cashOutTaxRate: 0,
106
+ baseCurrency: uint32(uint160(address(JBConstants.NATIVE_TOKEN))),
107
+ pausePay: false,
108
+ pauseCreditTransfers: false,
109
+ allowOwnerMinting: true,
110
+ allowSetCustomToken: false,
111
+ allowTerminalMigration: false,
112
+ allowSetTerminals: false,
113
+ allowSetController: false,
114
+ allowAddAccountingContext: true,
115
+ allowAddPriceFeed: true,
116
+ ownerMustSendPayouts: false,
117
+ holdFees: false,
118
+ useTotalSurplusForCashOuts: true,
119
+ useDataHookForPay: false,
120
+ useDataHookForCashOut: false,
121
+ dataHook: address(0),
122
+ metadata: 0
123
+ });
124
+ }
125
+
126
+ function launchAndConfigureL1Project() public {
127
+ // Package up the limits for the given terminal.
128
+ JBFundAccessLimitGroup[] memory _fundAccessLimitGroup = new JBFundAccessLimitGroup[](1);
129
+ {
130
+ JBCurrencyAmount[] memory _payoutLimits = new JBCurrencyAmount[](0);
131
+
132
+ JBCurrencyAmount[] memory _surplusAllowances = new JBCurrencyAmount[](1);
133
+ _surplusAllowances[0] =
134
+ JBCurrencyAmount({amount: 5 * 10 ** 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
135
+
136
+ _fundAccessLimitGroup[0] = JBFundAccessLimitGroup({
137
+ terminal: address(jbMultiTerminal()),
138
+ token: JBConstants.NATIVE_TOKEN,
139
+ payoutLimits: _payoutLimits,
140
+ surplusAllowances: _surplusAllowances
141
+ });
142
+ }
143
+
144
+ {
145
+ JBRulesetConfig[] memory _rulesetConfigurations = new JBRulesetConfig[](1);
146
+ _rulesetConfigurations[0].mustStartAtOrAfter = 0;
147
+ _rulesetConfigurations[0].duration = 0;
148
+ _rulesetConfigurations[0].weight = 1000 * 10 ** 18;
149
+ _rulesetConfigurations[0].weightCutPercent = 0;
150
+ _rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
151
+ _rulesetConfigurations[0].metadata = _metadata;
152
+ _rulesetConfigurations[0].splitGroups = new JBSplitGroup[](0);
153
+ _rulesetConfigurations[0].fundAccessLimitGroups = _fundAccessLimitGroup;
154
+
155
+ JBTerminalConfig[] memory _terminalConfigurations = new JBTerminalConfig[](1);
156
+ JBAccountingContext[] memory _tokensToAccept = new JBAccountingContext[](2);
157
+
158
+ _tokensToAccept[0] = JBAccountingContext({
159
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
160
+ });
161
+
162
+ _tokensToAccept[1] = JBAccountingContext({
163
+ token: address(ccipBnM), decimals: 18, currency: uint32(uint160(address(ccipBnM)))
164
+ });
165
+
166
+ _terminalConfigurations[0] =
167
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokensToAccept});
168
+
169
+ vm.expectCall(address(ccipBnM), abi.encodeWithSelector(IERC20Metadata.decimals.selector));
170
+
171
+ // Create a first project to collect fees.
172
+ jbController()
173
+ .launchProjectFor({
174
+ owner: multisig(),
175
+ projectUri: "whatever",
176
+ rulesetConfigurations: _rulesetConfigurations,
177
+ terminalConfigurations: _terminalConfigurations,
178
+ memo: ""
179
+ });
180
+
181
+ // Setup an erc20 for the project
182
+ projectOneToken = jbController().deployERC20For(1, "SuckerToken", "SOOK", bytes32(0));
183
+
184
+ // Add a price-feed to reconcile pays and cash outs with our test token
185
+ MockPriceFeed _priceFeedNativeTest = new MockPriceFeed(100 * 10 ** 18, 18);
186
+ vm.label(address(_priceFeedNativeTest), "Mock Price Feed Native-ccipBnM");
187
+
188
+ vm.startPrank(address(jbController()));
189
+ IJBPrices(jbPrices())
190
+ .addPriceFeedFor({
191
+ projectId: 1,
192
+ pricingCurrency: uint32(uint160(address(ccipBnM))),
193
+ unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
194
+ feed: IJBPriceFeed(_priceFeedNativeTest)
195
+ });
196
+ }
197
+ }
198
+
199
+ function initL2AndUtils() public {
200
+ // Create and select our L2 fork
201
+ arbSepoliaFork = vm.createSelectFork(ARBITRUM_SEPOLIA_RPC_URL);
202
+
203
+ Register.NetworkDetails memory arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(421_614);
204
+
205
+ ccipBnMArbSepolia = BurnMintERC677Helper(arbSepoliaNetworkDetails.ccipBnMAddress);
206
+ vm.label(address(ccipBnMArbSepolia), "bnmArbSep");
207
+ }
208
+
209
+ function launchAndConfigureL2Project() public {
210
+ JBFundAccessLimitGroup[] memory _fundAccessLimitGroup = new JBFundAccessLimitGroup[](1);
211
+ {
212
+ JBRulesetConfig[] memory _rulesetConfigurations = new JBRulesetConfig[](1);
213
+ _rulesetConfigurations[0].mustStartAtOrAfter = 0;
214
+ _rulesetConfigurations[0].duration = 0;
215
+ _rulesetConfigurations[0].weight = 1000 * 10 ** 18;
216
+ _rulesetConfigurations[0].weightCutPercent = 0;
217
+ _rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
218
+ _rulesetConfigurations[0].metadata = _metadata;
219
+ _rulesetConfigurations[0].splitGroups = new JBSplitGroup[](0);
220
+ _rulesetConfigurations[0].fundAccessLimitGroups = _fundAccessLimitGroup;
221
+
222
+ JBTerminalConfig[] memory _terminalConfigurations = new JBTerminalConfig[](1);
223
+ JBAccountingContext[] memory _tokensToAccept = new JBAccountingContext[](2);
224
+
225
+ _tokensToAccept[0] = JBAccountingContext({
226
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
227
+ });
228
+
229
+ _tokensToAccept[1] = JBAccountingContext({
230
+ token: address(ccipBnMArbSepolia), decimals: 18, currency: uint32(uint160(address(ccipBnMArbSepolia)))
231
+ });
232
+
233
+ _terminalConfigurations[0] =
234
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokensToAccept});
235
+
236
+ vm.expectCall(address(ccipBnMArbSepolia), abi.encodeWithSelector(IERC20Metadata.decimals.selector));
237
+
238
+ // Create a first project to collect fees.
239
+ jbController()
240
+ .launchProjectFor({
241
+ owner: multisig(),
242
+ projectUri: "whatever",
243
+ rulesetConfigurations: _rulesetConfigurations,
244
+ terminalConfigurations: _terminalConfigurations,
245
+ memo: ""
246
+ });
247
+ }
248
+ }
249
+
250
+ //*********************************************************************//
251
+ // ------------------------------- Setup ----------------------------- //
252
+ //*********************************************************************//
253
+
254
+ function setUp() public override {
255
+ // Create (and select) Sepolia fork and make simulator helper contracts persistent.
256
+ initL1AndUtils();
257
+
258
+ // Set metadata for the test projects to use.
259
+ initMetadata();
260
+
261
+ // Run setup on our first fork (sepolia) so we have a JBV4 setup (deploys v4 contracts).
262
+ super.setUp();
263
+
264
+ vm.stopPrank();
265
+ vm.startPrank(address(0x1112222));
266
+ suckerDeployer = new JBCCIPSuckerDeployer(jbDirectory(), jbPermissions(), jbTokens(), address(this), address(0));
267
+ vm.stopPrank();
268
+
269
+ // Set the remote chain as arb-sep
270
+ suckerDeployer.setChainSpecificConstants(
271
+ 421_614, CCIPHelper.selectorOfChain(421_614), ICCIPRouter(CCIPHelper.routerOfChain(block.chainid))
272
+ );
273
+
274
+ // Deploy the singleton and configure it.
275
+ vm.startPrank(address(0x1112222));
276
+ JBCCIPSucker singleton = new JBCCIPSucker({
277
+ deployer: suckerDeployer,
278
+ directory: jbDirectory(),
279
+ permissions: jbPermissions(),
280
+ tokens: jbTokens(),
281
+ feeProjectId: 1,
282
+ registry: IJBSuckerRegistry(address(0)),
283
+ trustedForwarder: address(0)
284
+ });
285
+ vm.stopPrank();
286
+
287
+ suckerDeployer.configureSingleton(singleton);
288
+
289
+ // Deploy our first sucker (on sepolia, the current fork, or "L1").
290
+ suckerL1 = suckerDeployer.createForSender(1, "salty");
291
+ vm.label(address(suckerL1), "suckerL1");
292
+
293
+ // Allow the sucker to mint
294
+ uint8[] memory ids = new uint8[](1);
295
+ ids[0] = JBPermissionIds.MINT_TOKENS;
296
+
297
+ JBPermissionsData memory perms =
298
+ JBPermissionsData({operator: address(suckerL1), projectId: 1, permissionIds: ids});
299
+
300
+ // Allow our L1 sucker to mint.
301
+ vm.startPrank(multisig());
302
+ jbPermissions().setPermissionsFor(multisig(), perms);
303
+
304
+ // Launch and configure our project on L1.
305
+ launchAndConfigureL1Project();
306
+
307
+ vm.stopPrank();
308
+
309
+ // Init our L2 fork and CCIP Local simulator utils for L2.
310
+ initL2AndUtils();
311
+
312
+ // Setup JBV4 on our forked L2 (arb-sep).
313
+ super.setUp();
314
+
315
+ vm.stopPrank();
316
+
317
+ vm.startPrank(address(0x1112222));
318
+ suckerDeployer2 =
319
+ new JBCCIPSuckerDeployer(jbDirectory(), jbPermissions(), jbTokens(), address(this), address(0));
320
+ vm.stopPrank();
321
+
322
+ suckerDeployer2.setChainSpecificConstants(
323
+ 11_155_111, CCIPHelper.selectorOfChain(11_155_111), ICCIPRouter(CCIPHelper.routerOfChain(block.chainid))
324
+ );
325
+
326
+ // Deploy the singleton and configure it.
327
+ vm.startPrank(address(0x1112222));
328
+ JBCCIPSucker singleton2 = new JBCCIPSucker({
329
+ deployer: suckerDeployer2,
330
+ directory: jbDirectory(),
331
+ permissions: jbPermissions(),
332
+ tokens: jbTokens(),
333
+ feeProjectId: 1,
334
+ registry: IJBSuckerRegistry(address(0)),
335
+ trustedForwarder: address(0)
336
+ });
337
+ vm.stopPrank();
338
+
339
+ suckerDeployer2.configureSingleton(singleton2);
340
+
341
+ // Deploy the sucker on L2.
342
+ suckerDeployer2.createForSender(1, "salty");
343
+
344
+ // Launch our project on L2.
345
+ vm.startPrank(multisig());
346
+ launchAndConfigureL2Project();
347
+
348
+ // Allow the L2 sucker to mint (reuse same perms struct, operator is the same address via CREATE2 salt).
349
+ jbPermissions().setPermissionsFor(multisig(), perms);
350
+
351
+ vm.stopPrank();
352
+
353
+ // Mock the registry's toRemoteFee() on both forks (registry is address(0) in tests).
354
+ vm.selectFork(sepoliaFork);
355
+ vm.mockCall(address(0), abi.encodeCall(IJBSuckerRegistry.toRemoteFee, ()), abi.encode(uint256(0)));
356
+ vm.selectFork(arbSepoliaFork);
357
+ vm.mockCall(address(0), abi.encodeCall(IJBSuckerRegistry.toRemoteFee, ()), abi.encode(uint256(0)));
358
+ }
359
+
360
+ //*********************************************************************//
361
+ // ----------------------- Helper: zero-hash proof ------------------- //
362
+ //*********************************************************************//
363
+
364
+ /// @notice Builds a 32-element proof of all zero hashes for a single-leaf tree (index 0).
365
+ /// @dev For a tree with one leaf at index 0, every sibling on the path to the root is the
366
+ /// zero hash at that level: Z_0, Z_1, ..., Z_31.
367
+ function _zeroProof() internal pure returns (bytes32[32] memory proof) {
368
+ proof[0] = hex"0000000000000000000000000000000000000000000000000000000000000000"; // Z_0
369
+ proof[1] = hex"ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5"; // Z_1
370
+ proof[2] = hex"b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30"; // Z_2
371
+ proof[3] = hex"21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85"; // Z_3
372
+ proof[4] = hex"e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344"; // Z_4
373
+ proof[5] = hex"0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d"; // Z_5
374
+ proof[6] = hex"887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968"; // Z_6
375
+ proof[7] = hex"ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83"; // Z_7
376
+ proof[8] = hex"9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af"; // Z_8
377
+ proof[9] = hex"cefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0"; // Z_9
378
+ proof[10] = hex"f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5"; // Z_10
379
+ proof[11] = hex"f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892"; // Z_11
380
+ proof[12] = hex"3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c"; // Z_12
381
+ proof[13] = hex"c1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb"; // Z_13
382
+ proof[14] = hex"5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc"; // Z_14
383
+ proof[15] = hex"da7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2"; // Z_15
384
+ proof[16] = hex"2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f"; // Z_16
385
+ proof[17] = hex"e1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a"; // Z_17
386
+ proof[18] = hex"5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0"; // Z_18
387
+ proof[19] = hex"b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0"; // Z_19
388
+ proof[20] = hex"c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2"; // Z_20
389
+ proof[21] = hex"f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9"; // Z_21
390
+ proof[22] = hex"5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377"; // Z_22
391
+ proof[23] = hex"4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652"; // Z_23
392
+ proof[24] = hex"cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef"; // Z_24
393
+ proof[25] = hex"0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d"; // Z_25
394
+ proof[26] = hex"b8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0"; // Z_26
395
+ proof[27] = hex"838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e"; // Z_27
396
+ proof[28] = hex"662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e"; // Z_28
397
+ proof[29] = hex"388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322"; // Z_29
398
+ proof[30] = hex"93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735"; // Z_30
399
+ proof[31] = hex"8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9"; // Z_31
400
+ }
401
+
402
+ //*********************************************************************//
403
+ // ------------------------------- Tests ----------------------------- //
404
+ //*********************************************************************//
405
+
406
+ /// @notice Full end-to-end: prepare on L1 -> toRemote via CCIP -> claim on L2 -> double-claim reverts.
407
+ function test_forkClaimNative() external {
408
+ // -- Actors and parameters --
409
+ address user = makeAddr("him");
410
+
411
+ // State captured across scoped blocks
412
+ uint256 capturedProjectTokenCount;
413
+ uint256 capturedTerminalTokenAmount;
414
+ uint256 capturedIndex;
415
+ bytes32 capturedBeneficiary;
416
+
417
+ // ----------------------------------------------------------------
418
+ // Step 1: Prepare on L1 — pay the project, then prepare to bridge
419
+ // ----------------------------------------------------------------
420
+ {
421
+ vm.selectFork(sepoliaFork);
422
+
423
+ uint256 amountToSend = 0.05 ether;
424
+ uint256 maxCashedOut = amountToSend / 2;
425
+
426
+ vm.deal(user, amountToSend);
427
+
428
+ // Map the native token for bridging
429
+ JBTokenMapping memory map = JBTokenMapping({
430
+ localToken: JBConstants.NATIVE_TOKEN,
431
+ minGas: 200_000,
432
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)))
433
+ });
434
+
435
+ vm.prank(multisig());
436
+ suckerL1.mapToken(map);
437
+
438
+ // User pays the project and receives project tokens
439
+ vm.startPrank(user);
440
+ uint256 projectTokenAmount =
441
+ jbMultiTerminal().pay{value: amountToSend}(1, JBConstants.NATIVE_TOKEN, amountToSend, user, 0, "", "");
442
+
443
+ // Approve the sucker to use the project tokens
444
+ IERC20(address(projectOneToken)).approve(address(suckerL1), projectTokenAmount);
445
+
446
+ // Record logs to capture InsertToOutboxTree event
447
+ vm.recordLogs();
448
+
449
+ // Prepare: cashes out project tokens for native tokens, inserts leaf into outbox tree
450
+ suckerL1.prepare(
451
+ projectTokenAmount, bytes32(uint256(uint160(user))), maxCashedOut, JBConstants.NATIVE_TOKEN
452
+ );
453
+ vm.stopPrank();
454
+ }
455
+
456
+ // ----------------------------------------------------------------
457
+ // Step 2: Extract leaf values from the InsertToOutboxTree event
458
+ // ----------------------------------------------------------------
459
+ {
460
+ Vm.Log[] memory logs = vm.getRecordedLogs();
461
+
462
+ bytes32 eventSig =
463
+ keccak256("InsertToOutboxTree(bytes32,address,bytes32,uint256,bytes32,uint256,uint256,address)");
464
+
465
+ bool found;
466
+
467
+ for (uint256 i; i < logs.length; i++) {
468
+ if (logs[i].topics[0] == eventSig) {
469
+ capturedBeneficiary = logs[i].topics[1];
470
+
471
+ (, // hashed
472
+ capturedIndex,, // root
473
+ capturedProjectTokenCount,
474
+ capturedTerminalTokenAmount,
475
+ // caller
476
+ ) = abi.decode(logs[i].data, (bytes32, uint256, bytes32, uint256, uint256, address));
477
+
478
+ found = true;
479
+ break;
480
+ }
481
+ }
482
+ assertTrue(found, "InsertToOutboxTree event not found");
483
+ assertEq(capturedIndex, 0, "First leaf should be at index 0");
484
+ assertEq(capturedBeneficiary, bytes32(uint256(uint160(user))), "Beneficiary mismatch");
485
+ assertGt(capturedProjectTokenCount, 0, "Project token count should be > 0");
486
+ assertGt(capturedTerminalTokenAmount, 0, "Terminal token amount should be > 0");
487
+ }
488
+
489
+ // ----------------------------------------------------------------
490
+ // Step 3: Send over CCIP (toRemote) and deliver to L2
491
+ // ----------------------------------------------------------------
492
+ {
493
+ address rootSender = makeAddr("rootSender");
494
+ vm.deal(rootSender, 1 ether);
495
+
496
+ vm.prank(rootSender);
497
+ suckerL1.toRemote{value: 1 ether}(JBConstants.NATIVE_TOKEN);
498
+
499
+ // Verify outbox is cleared
500
+ assertEq(
501
+ suckerL1.outboxOf(JBConstants.NATIVE_TOKEN).balance, 0, "Outbox balance should be 0 after toRemote"
502
+ );
503
+
504
+ // CCIP local simulator delivers the message to L2
505
+ ccipLocalSimulatorFork.switchChainAndRouteMessage(arbSepoliaFork);
506
+
507
+ // We are now on L2 (arbSepoliaFork). suckerL1 is at the same address due to CREATE2.
508
+ // Verify inbox root was set
509
+ assertNotEq(
510
+ suckerL1.inboxOf(JBConstants.NATIVE_TOKEN).root,
511
+ bytes32(0),
512
+ "Inbox root should be set after CCIP delivery"
513
+ );
514
+
515
+ // Verify native value was delivered
516
+ assertEq(address(suckerL1).balance, capturedTerminalTokenAmount, "Sucker should hold the bridged ETH");
517
+ }
518
+
519
+ // ----------------------------------------------------------------
520
+ // Step 4: Claim on L2 — beneficiary receives minted project tokens
521
+ // ----------------------------------------------------------------
522
+ {
523
+ // Check user's project token balance before claim (should be 0 on L2)
524
+ assertEq(jbTokens().totalBalanceOf(user, 1), 0, "User should have 0 project tokens on L2 before claim");
525
+
526
+ // For a single-leaf tree (index 0), the merkle proof siblings are all zero hashes
527
+ JBClaim memory claimData = JBClaim({
528
+ token: JBConstants.NATIVE_TOKEN,
529
+ leaf: JBLeaf({
530
+ index: capturedIndex,
531
+ beneficiary: capturedBeneficiary,
532
+ projectTokenCount: capturedProjectTokenCount,
533
+ terminalTokenAmount: capturedTerminalTokenAmount
534
+ }),
535
+ proof: _zeroProof()
536
+ });
537
+
538
+ // Execute the claim
539
+ suckerL1.claim(claimData);
540
+
541
+ // Verify: beneficiary received the minted project tokens
542
+ assertEq(
543
+ jbTokens().totalBalanceOf(user, 1),
544
+ capturedProjectTokenCount,
545
+ "User should have received the exact project token count from the claim"
546
+ );
547
+
548
+ // ----------------------------------------------------------------
549
+ // Step 5: Double-claim reverts
550
+ // ----------------------------------------------------------------
551
+ vm.expectRevert(
552
+ abi.encodeWithSelector(
553
+ JBSucker.JBSucker_LeafAlreadyExecuted.selector, JBConstants.NATIVE_TOKEN, capturedIndex
554
+ )
555
+ );
556
+ suckerL1.claim(claimData);
557
+ }
558
+ }
559
+ }