@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 +2 -2
- package/src/JBSucker.sol +3 -1
- package/test/ForkClaim.t.sol +559 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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
|
+
}
|