@bananapus/suckers-v6 0.0.13 → 0.0.15
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/ADMINISTRATION.md +14 -3
- package/ARCHITECTURE.md +67 -17
- package/AUDIT_INSTRUCTIONS.md +97 -1
- package/CHANGE_LOG.md +14 -2
- package/README.md +17 -26
- package/RISKS.md +23 -6
- package/SKILLS.md +46 -9
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +245 -156
- package/foundry.toml +1 -1
- package/package.json +3 -3
- package/script/Deploy.s.sol +31 -2
- package/script/helpers/SuckerDeploymentLib.sol +6 -6
- package/src/JBArbitrumSucker.sol +15 -12
- package/src/JBBaseSucker.sol +1 -1
- package/src/JBCCIPSucker.sol +1 -1
- package/src/JBCeloSucker.sol +1 -1
- package/src/JBOptimismSucker.sol +1 -1
- package/src/JBSucker.sol +24 -7
- package/src/JBSuckerRegistry.sol +1 -1
- package/src/deployers/JBArbitrumSuckerDeployer.sol +1 -1
- package/src/deployers/JBBaseSuckerDeployer.sol +1 -1
- package/src/deployers/JBCCIPSuckerDeployer.sol +1 -1
- package/src/deployers/JBCeloSuckerDeployer.sol +1 -1
- package/src/deployers/JBOptimismSuckerDeployer.sol +1 -1
- package/src/deployers/JBSuckerDeployer.sol +1 -1
- package/src/libraries/CCIPHelper.sol +1 -1
- package/src/utils/MerkleLib.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/ForkArbitrum.t.sol +1 -1
- package/test/ForkCelo.t.sol +1 -1
- package/test/ForkClaim.t.sol +1 -1
- package/test/ForkMainnet.t.sol +1 -1
- package/test/ForkOPStack.t.sol +1 -1
- package/test/SuckerDeepAttacks.t.sol +5 -4
- package/test/audit/ArbitrumL2ToRemoteFeeDoS.t.sol +120 -0
- package/test/audit/CodexNemesisPoC.t.sol +169 -0
- package/test/fork/OptimismSuckerFork.t.sol +457 -0
- package/test/unit/ccip_refund.t.sol +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {stdJson} from "forge-std/Script.sol";
|
|
5
5
|
import {Vm} from "forge-std/Vm.sol";
|
|
@@ -50,12 +50,12 @@ library SuckerDeploymentLib {
|
|
|
50
50
|
// Is deployed on all (supported) chains.
|
|
51
51
|
deployment.registry = IJBSuckerRegistry(
|
|
52
52
|
_getDeploymentAddress({
|
|
53
|
-
path: path, projectName: "nana-suckers-
|
|
53
|
+
path: path, projectName: "nana-suckers-v6", networkName: networkName, contractName: "JBSuckerRegistry"
|
|
54
54
|
})
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
bytes32 _network = keccak256(abi.encodePacked(networkName));
|
|
58
|
-
bool _isMainnet = _network == keccak256("ethereum") || _network == keccak256("
|
|
58
|
+
bool _isMainnet = _network == keccak256("ethereum") || _network == keccak256("ethereum_sepolia");
|
|
59
59
|
bool _isOP = _network == keccak256("optimism") || _network == keccak256("optimism_sepolia");
|
|
60
60
|
bool _isBase = _network == keccak256("base") || _network == keccak256("base_sepolia");
|
|
61
61
|
bool _isArb = _network == keccak256("arbitrum") || _network == keccak256("arbitrum_sepolia");
|
|
@@ -64,7 +64,7 @@ library SuckerDeploymentLib {
|
|
|
64
64
|
deployment.optimismDeployer = IJBSuckerDeployer(
|
|
65
65
|
_getDeploymentAddress({
|
|
66
66
|
path: path,
|
|
67
|
-
projectName: "nana-suckers-
|
|
67
|
+
projectName: "nana-suckers-v6",
|
|
68
68
|
networkName: networkName,
|
|
69
69
|
contractName: "JBOptimismSuckerDeployer"
|
|
70
70
|
})
|
|
@@ -75,7 +75,7 @@ library SuckerDeploymentLib {
|
|
|
75
75
|
deployment.baseDeployer = IJBSuckerDeployer(
|
|
76
76
|
_getDeploymentAddress({
|
|
77
77
|
path: path,
|
|
78
|
-
projectName: "nana-suckers-
|
|
78
|
+
projectName: "nana-suckers-v6",
|
|
79
79
|
networkName: networkName,
|
|
80
80
|
contractName: "JBBaseSuckerDeployer"
|
|
81
81
|
})
|
|
@@ -86,7 +86,7 @@ library SuckerDeploymentLib {
|
|
|
86
86
|
deployment.arbitrumDeployer = IJBSuckerDeployer(
|
|
87
87
|
_getDeploymentAddress({
|
|
88
88
|
path: path,
|
|
89
|
-
projectName: "nana-suckers-
|
|
89
|
+
projectName: "nana-suckers-v6",
|
|
90
90
|
networkName: networkName,
|
|
91
91
|
contractName: "JBArbitrumSuckerDeployer"
|
|
92
92
|
})
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {IBridge} from "@arbitrum/nitro-contracts/src/bridge/IBridge.sol";
|
|
5
5
|
import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol";
|
|
@@ -154,25 +154,34 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
154
154
|
internal
|
|
155
155
|
override
|
|
156
156
|
{
|
|
157
|
-
// Bridge expects to be paid
|
|
158
|
-
if (transportPayment == 0 && LAYER == JBLayer.L1) revert JBSucker_ExpectedMsgValue();
|
|
159
|
-
|
|
160
157
|
// Build the calldata that will be send to the peer. This will call `JBSucker.fromRemote` on the remote peer.
|
|
161
158
|
bytes memory data = abi.encodeCall(JBSucker.fromRemote, (message));
|
|
162
159
|
|
|
163
160
|
// Depending on which layer we are on, send the call to the other layer.
|
|
164
161
|
// slither-disable-start out-of-order-retryable
|
|
165
162
|
if (LAYER == JBLayer.L1) {
|
|
163
|
+
// L1→L2 requires transport payment for retryable tickets.
|
|
164
|
+
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue();
|
|
166
165
|
_toL2({
|
|
167
166
|
token: token, transportPayment: transportPayment, amount: amount, data: data, remoteToken: remoteToken
|
|
168
167
|
});
|
|
169
168
|
} else {
|
|
169
|
+
// L2→L1 via ArbSys is free — reject any transport payment.
|
|
170
|
+
if (transportPayment != 0) revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
170
171
|
_toL1({token: token, amount: amount, data: data, remoteToken: remoteToken});
|
|
171
172
|
}
|
|
172
173
|
// slither-disable-end out-of-order-retryable
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
/// @notice Bridge the `token` and data to the remote L1 chain.
|
|
177
|
+
/// @dev IMPORTANT — Arbitrum non-atomic bridging limitation:
|
|
178
|
+
/// For ERC-20 transfers, this function performs two independent operations: one for the token bridge
|
|
179
|
+
/// (via the L2 gateway router) and one for the `fromRemote` merkle root message (via `ArbSys.sendTxToL1`).
|
|
180
|
+
/// These are processed independently on L1, with no guaranteed ordering.
|
|
181
|
+
///
|
|
182
|
+
/// `_handleClaim` calls `_addToBalance` which checks `amountToAddToBalanceOf` (derived from the contract's
|
|
183
|
+
/// actual token balance minus outbox balance). If the tokens have not arrived yet, this check will revert
|
|
184
|
+
/// with `JBSucker_InsufficientBalance`, preventing unbacked token minting.
|
|
176
185
|
/// @param token The token to bridge.
|
|
177
186
|
/// @param amount The amount of tokens to bridge.
|
|
178
187
|
/// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
|
|
@@ -180,12 +189,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
180
189
|
function _toL1(address token, uint256 amount, bytes memory data, JBRemoteToken memory remoteToken) internal {
|
|
181
190
|
uint256 nativeValue;
|
|
182
191
|
|
|
183
|
-
// Revert if there's a `msg.value`. Sending a message to L1 does not require any payment.
|
|
184
|
-
if (msg.value != 0) {
|
|
185
|
-
// slither-disable-next-line msg-value-loop
|
|
186
|
-
revert JBSucker_UnexpectedMsgValue(msg.value);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
192
|
// If the token is an ERC-20, bridge it to the peer.
|
|
190
193
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
191
194
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
@@ -213,8 +216,8 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
213
216
|
/// @notice Bridge the `token` and data to the remote L2 chain.
|
|
214
217
|
/// @dev IMPORTANT — Arbitrum non-atomic bridging limitation:
|
|
215
218
|
/// For ERC-20 transfers, this function creates two independent retryable tickets: one for the token bridge
|
|
216
|
-
/// (via the gateway router
|
|
217
|
-
///
|
|
219
|
+
/// (via the gateway router) and one for the `fromRemote` merkle root message (via the inbox).
|
|
220
|
+
/// These tickets are redeemed independently on L2, with no guaranteed ordering.
|
|
218
221
|
///
|
|
219
222
|
/// `_handleClaim` calls `_addToBalance` which checks `amountToAddToBalanceOf` (derived from the contract's
|
|
220
223
|
/// actual token balance minus outbox balance). If the tokens have not arrived yet, this check will revert
|
package/src/JBBaseSucker.sol
CHANGED
package/src/JBCCIPSucker.sol
CHANGED
package/src/JBCeloSucker.sol
CHANGED
package/src/JBOptimismSucker.sol
CHANGED
package/src/JBSucker.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
5
5
|
import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
|
|
@@ -466,9 +466,16 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
466
466
|
/// tree is append-only), but users will need regenerated proofs computed against the current root. This trade-off
|
|
467
467
|
/// is accepted because enforcing sequential nonces could permanently block a token's inbox if a single message is
|
|
468
468
|
/// delayed or lost by the bridge.
|
|
469
|
+
/// @dev Post-deprecation root acceptance: Roots are accepted in DEPRECATED state to prevent stranding tokens that
|
|
470
|
+
/// were sent before deprecation. Even though the mandatory `_maxMessagingDelay()` (14-day) buffer gives in-flight
|
|
471
|
+
/// messages time to arrive, accepting roots after deprecation provides a stronger guarantee that users can always
|
|
472
|
+
/// claim their bridged tokens. Double-spend is not a concern because `toRemote` is already disabled in
|
|
473
|
+
/// `SENDING_DISABLED` and `DEPRECATED` states, so no new outbound transfers can occur.
|
|
469
474
|
/// @param root The merkle root, token, and amount being received.
|
|
470
475
|
function fromRemote(JBMessageRoot calldata root) external payable {
|
|
471
476
|
// Make sure that the message came from our peer.
|
|
477
|
+
// Safe to use _msgSender() here: bridge messengers never use ERC2771 meta-transactions,
|
|
478
|
+
// so this always resolves to msg.sender.
|
|
472
479
|
if (!_isRemotePeer(_msgSender())) {
|
|
473
480
|
revert JBSucker_NotPeer(_toBytes32(_msgSender()));
|
|
474
481
|
}
|
|
@@ -495,11 +502,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
495
502
|
// If the received tree's nonce is greater than the current inbox tree's nonce, update the inbox tree.
|
|
496
503
|
// We can't revert because this could be a native token transfer. If we reverted, we would lose the native
|
|
497
504
|
// tokens.
|
|
498
|
-
|
|
499
|
-
// Deprecated suckers reject new roots to prevent double-spend: once deprecated, the project owner may have
|
|
500
|
-
// enabled the emergency hatch for local withdrawals. Accepting a new root after that could allow claiming
|
|
501
|
-
// on both chains. The emergency hatch provides recovery for any tokens stuck in this state.
|
|
502
|
-
if (root.remoteRoot.nonce > inbox.nonce && state() != JBSuckerState.DEPRECATED) {
|
|
505
|
+
if (root.remoteRoot.nonce > inbox.nonce) {
|
|
503
506
|
inbox.nonce = root.remoteRoot.nonce;
|
|
504
507
|
inbox.root = root.remoteRoot.root;
|
|
505
508
|
emit NewInboxTreeRoot({
|
|
@@ -567,6 +570,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
567
570
|
/// @notice Prepare project tokens and the cash out amount backing them to be bridged to the remote chain.
|
|
568
571
|
/// @dev This adds the tokens and funds to the outbox tree for the `token`. They will be bridged by the next call to
|
|
569
572
|
/// `toRemote` for the same `token`.
|
|
573
|
+
/// @dev Reentrancy protection: This function has implicit reentrancy protection through `_pullBackingAssets`.
|
|
574
|
+
/// The `assert` in `_pullBackingAssets` verifies that the contract's token balance increased by exactly the
|
|
575
|
+
/// amount reported by the terminal's `cashOutTokensOf`. A reentrant `prepare()` call would trigger a nested
|
|
576
|
+
/// `cashOutTokensOf`, changing the contract's balance before the outer call's `assert` executes. The outer
|
|
577
|
+
/// `assert` would then fail because the balance delta no longer matches the reported `reclaimedAmount`.
|
|
578
|
+
/// Note: because `assert` is used (not `revert`), a failed reentrancy attempt will consume all remaining gas.
|
|
570
579
|
/// @param projectTokenCount The number of project tokens to prepare for bridging.
|
|
571
580
|
/// @param beneficiary The recipient on the remote chain (bytes32 for cross-VM compatibility).
|
|
572
581
|
/// For EVM peers: the EVM address left-padded to 32 bytes via `_toBytes32`.
|
|
@@ -664,6 +673,11 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
664
673
|
/// remote
|
|
665
674
|
/// chain.
|
|
666
675
|
/// @dev This sends the outbox root for the specified `token` to the remote chain.
|
|
676
|
+
/// @dev Fee payment failure handling: The registry fee payment uses a best-effort pattern (try/catch). If the
|
|
677
|
+
/// fee project's terminal doesn't exist or the `pay` call reverts, the fee is silently absorbed back into the
|
|
678
|
+
/// `transportPayment` instead of reverting the entire transaction. This is a deliberate design choice that favors
|
|
679
|
+
/// bridge availability over fee collection — a failed fee payment should never prevent users from bridging their
|
|
680
|
+
/// tokens. The fee amount is typically small relative to the bridged value, making the tradeoff acceptable.
|
|
667
681
|
/// @param token The terminal token being bridged.
|
|
668
682
|
function toRemote(address token) external payable override {
|
|
669
683
|
JBRemoteToken memory remoteToken = _remoteTokenFor[token];
|
|
@@ -692,6 +706,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
692
706
|
// Best-effort: if the terminal doesn't exist or the pay call reverts, proceed without fee.
|
|
693
707
|
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
|
|
694
708
|
if (address(terminal) != address(0)) {
|
|
709
|
+
// slither-disable-next-line unused-return
|
|
695
710
|
try terminal.pay{value: _toRemoteFee}({
|
|
696
711
|
projectId: FEE_PROJECT_ID,
|
|
697
712
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -700,7 +715,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
700
715
|
minReturnedTokens: 0,
|
|
701
716
|
memo: "",
|
|
702
717
|
metadata: ""
|
|
703
|
-
})
|
|
718
|
+
}) returns (
|
|
719
|
+
uint256
|
|
720
|
+
) {}
|
|
704
721
|
catch {
|
|
705
722
|
// Fee payment failed — proceed without fee, return it as transport payment.
|
|
706
723
|
transportPayment = msg.value;
|
package/src/JBSuckerRegistry.sol
CHANGED
package/src/utils/MerkleLib.sol
CHANGED
package/test/Fork.t.sol
CHANGED
package/test/ForkArbitrum.t.sol
CHANGED
package/test/ForkCelo.t.sol
CHANGED
package/test/ForkClaim.t.sol
CHANGED
package/test/ForkMainnet.t.sol
CHANGED
package/test/ForkOPStack.t.sol
CHANGED
|
@@ -330,7 +330,7 @@ contract SuckerDeepAttacks is Test {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
/// @notice fromRemote when DEPRECATED: should silently ignore even with valid higher nonce.
|
|
333
|
-
function
|
|
333
|
+
function test_fromRemote_deprecated_stillAccepts() public {
|
|
334
334
|
// Set deprecation in the past so state=DEPRECATED.
|
|
335
335
|
sucker.test_setDeprecatedAfter(block.timestamp - 1);
|
|
336
336
|
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED), "Should be DEPRECATED");
|
|
@@ -344,12 +344,13 @@ contract SuckerDeepAttacks is Test {
|
|
|
344
344
|
remoteRoot: JBInboxTreeRoot({nonce: 2, root: bytes32(uint256(0xbbbb))})
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
//
|
|
347
|
+
// Roots are accepted in DEPRECATED state to prevent stranding tokens that were sent
|
|
348
|
+
// before deprecation. Double-spend is not a concern because toRemote is already disabled.
|
|
348
349
|
vm.prank(address(sucker));
|
|
349
350
|
sucker.fromRemote(root);
|
|
350
351
|
|
|
351
|
-
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(
|
|
352
|
-
assertEq(sucker.test_getInboxNonce(TOKEN),
|
|
352
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xbbbb)), "Root SHOULD update even when DEPRECATED");
|
|
353
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 2, "Nonce should update to 2 even when DEPRECATED");
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
/// @notice fromRemote when SENDING_DISABLED: should still accept roots (only sending is disabled).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
9
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
10
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
11
|
+
import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol";
|
|
12
|
+
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
13
|
+
|
|
14
|
+
import "../../src/JBArbitrumSucker.sol";
|
|
15
|
+
import "../../src/deployers/JBArbitrumSuckerDeployer.sol";
|
|
16
|
+
import "../../src/enums/JBLayer.sol";
|
|
17
|
+
import "../../src/interfaces/IArbGatewayRouter.sol";
|
|
18
|
+
import "../../src/interfaces/IJBSuckerRegistry.sol";
|
|
19
|
+
import "../../src/structs/JBRemoteToken.sol";
|
|
20
|
+
|
|
21
|
+
contract ArbitrumL2FeeHarness is JBArbitrumSucker {
|
|
22
|
+
constructor(
|
|
23
|
+
JBArbitrumSuckerDeployer deployer,
|
|
24
|
+
IJBDirectory directory,
|
|
25
|
+
IJBPermissions permissions,
|
|
26
|
+
IJBTokens tokens,
|
|
27
|
+
IJBSuckerRegistry registry
|
|
28
|
+
)
|
|
29
|
+
JBArbitrumSucker(deployer, directory, permissions, tokens, 1, registry, address(0))
|
|
30
|
+
{}
|
|
31
|
+
|
|
32
|
+
function seedOutbox(address token, bytes32 remoteToken) external {
|
|
33
|
+
_remoteTokenFor[token] =
|
|
34
|
+
JBRemoteToken({addr: remoteToken, enabled: true, emergencyHatch: false, minGas: 200_000});
|
|
35
|
+
_insertIntoTree({
|
|
36
|
+
projectTokenCount: 0,
|
|
37
|
+
token: token,
|
|
38
|
+
terminalTokenAmount: 0,
|
|
39
|
+
beneficiary: bytes32(uint256(uint160(address(0xBEEF))))
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// @title ArbitrumL2ToRemoteFeeDoSTest
|
|
45
|
+
/// @notice Regression tests for the Arbitrum L2→L1 transportPayment fix.
|
|
46
|
+
/// Before the fix, `_toL1` checked `msg.value != 0` instead of `transportPayment != 0`.
|
|
47
|
+
/// When a non-zero registry fee existed, `msg.value` was non-zero even though all of it was
|
|
48
|
+
/// consumed by the fee — causing `_toL1` to revert and making L2→L1 bridging impossible.
|
|
49
|
+
/// The fix passes `transportPayment` (msg.value minus fee) into `_toL1`, so the check
|
|
50
|
+
/// correctly passes when all value goes to the fee.
|
|
51
|
+
contract ArbitrumL2ToRemoteFeeDoSTest is Test {
|
|
52
|
+
address internal constant DIRECTORY = address(0x1000);
|
|
53
|
+
address internal constant PERMISSIONS = address(0x2000);
|
|
54
|
+
address internal constant TOKENS = address(0x3000);
|
|
55
|
+
address internal constant REGISTRY = address(0x4000);
|
|
56
|
+
address internal constant TERMINAL = address(0x5000);
|
|
57
|
+
|
|
58
|
+
ArbitrumL2FeeHarness internal sucker;
|
|
59
|
+
|
|
60
|
+
function setUp() public {
|
|
61
|
+
JBArbitrumSuckerDeployer deployer = new JBArbitrumSuckerDeployer({
|
|
62
|
+
directory: IJBDirectory(DIRECTORY),
|
|
63
|
+
permissions: IJBPermissions(PERMISSIONS),
|
|
64
|
+
tokens: IJBTokens(TOKENS),
|
|
65
|
+
configurator: address(this),
|
|
66
|
+
trustedForwarder: address(0)
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
deployer.setChainSpecificConstants({
|
|
70
|
+
layer: JBLayer.L2, inbox: IInbox(address(0)), gatewayRouter: IArbGatewayRouter(address(0xB0B))
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ArbitrumL2FeeHarness singleton = new ArbitrumL2FeeHarness({
|
|
74
|
+
deployer: deployer,
|
|
75
|
+
directory: IJBDirectory(DIRECTORY),
|
|
76
|
+
permissions: IJBPermissions(PERMISSIONS),
|
|
77
|
+
tokens: IJBTokens(TOKENS),
|
|
78
|
+
registry: IJBSuckerRegistry(REGISTRY)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
sucker = ArbitrumL2FeeHarness(payable(LibClone.cloneDeterministic(address(singleton), bytes32("arb_fee_dos"))));
|
|
82
|
+
sucker.initialize(1);
|
|
83
|
+
sucker.seedOutbox(JBConstants.NATIVE_TOKEN, bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))));
|
|
84
|
+
|
|
85
|
+
// Mock the registry fee and fee terminal.
|
|
86
|
+
vm.mockCall(REGISTRY, abi.encodeCall(IJBSuckerRegistry.toRemoteFee, ()), abi.encode(uint256(1)));
|
|
87
|
+
vm.mockCall(
|
|
88
|
+
DIRECTORY,
|
|
89
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (1, JBConstants.NATIVE_TOKEN)),
|
|
90
|
+
abi.encode(IJBTerminal(TERMINAL))
|
|
91
|
+
);
|
|
92
|
+
vm.mockCall(TERMINAL, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(uint256(0)));
|
|
93
|
+
|
|
94
|
+
// Mock ArbSys precompile at address(100) so sendTxToL1 succeeds.
|
|
95
|
+
vm.etch(address(100), hex"00");
|
|
96
|
+
vm.mockCall(address(100), abi.encodeWithSignature("sendTxToL1(address,bytes)"), abi.encode(uint256(0)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// @notice L2→L1 bridging succeeds when msg.value exactly covers the registry fee.
|
|
100
|
+
/// Before the fix, this reverted because _toL1 checked `msg.value != 0`.
|
|
101
|
+
/// After the fix, transportPayment = msg.value - fee = 0, so _toL1 passes.
|
|
102
|
+
function test_toRemoteSucceedsWhenMsgValueCoversFeeExactly() external {
|
|
103
|
+
// msg.value = 1, fee = 1 → transportPayment = 0 → _toL1 accepts
|
|
104
|
+
sucker.toRemote{value: 1}(JBConstants.NATIVE_TOKEN);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// @notice L2→L1 bridging reverts when msg.value exceeds the fee (excess transportPayment).
|
|
108
|
+
/// Sending a message from L2→L1 via ArbSys.sendTxToL1 is free — any leftover transportPayment is invalid.
|
|
109
|
+
function test_toRemoteRevertsWhenExcessTransportPayment() external {
|
|
110
|
+
// msg.value = 2, fee = 1 → transportPayment = 1 → _toL1 reverts
|
|
111
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_UnexpectedMsgValue.selector, 1));
|
|
112
|
+
sucker.toRemote{value: 2}(JBConstants.NATIVE_TOKEN);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// @notice L2→L1 bridging reverts when msg.value is insufficient for the fee.
|
|
116
|
+
function test_toRemoteRevertsWhenMsgValueBelowFee() external {
|
|
117
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_InsufficientMsgValue.selector, 0, 1));
|
|
118
|
+
sucker.toRemote{value: 0}(JBConstants.NATIVE_TOKEN);
|
|
119
|
+
}
|
|
120
|
+
}
|