@bananapus/suckers-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +422 -0
- package/SECURITY.md +55 -0
- package/SKILLS.md +163 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/base/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSucker.json +1385 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSuckerDeployer.json +373 -0
- package/deployments/nana-suckers-v5/optimism/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBSuckerRegistry.json +694 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +33 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +506 -0
- package/script/helpers/SuckerDeploymentLib.sol +97 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JBArbitrumSucker.sol +311 -0
- package/src/JBBaseSucker.sol +41 -0
- package/src/JBCCIPSucker.sol +303 -0
- package/src/JBOptimismSucker.sol +143 -0
- package/src/JBSucker.sol +1159 -0
- package/src/JBSuckerRegistry.sol +262 -0
- package/src/deployers/JBArbitrumSuckerDeployer.sol +86 -0
- package/src/deployers/JBBaseSuckerDeployer.sol +26 -0
- package/src/deployers/JBCCIPSuckerDeployer.sol +88 -0
- package/src/deployers/JBOptimismSuckerDeployer.sol +82 -0
- package/src/deployers/JBSuckerDeployer.sol +147 -0
- package/src/enums/JBAddToBalanceMode.sol +11 -0
- package/src/enums/JBLayer.sol +8 -0
- package/src/enums/JBSuckerState.sol +14 -0
- package/src/interfaces/IArbGatewayRouter.sol +11 -0
- package/src/interfaces/IArbL1GatewayRouter.sol +17 -0
- package/src/interfaces/IArbL2GatewayRouter.sol +14 -0
- package/src/interfaces/ICCIPRouter.sol +11 -0
- package/src/interfaces/IJBArbitrumSucker.sol +13 -0
- package/src/interfaces/IJBArbitrumSuckerDeployer.sol +12 -0
- package/src/interfaces/IJBCCIPSuckerDeployer.sol +15 -0
- package/src/interfaces/IJBOpSuckerDeployer.sol +11 -0
- package/src/interfaces/IJBOptimismSucker.sol +10 -0
- package/src/interfaces/IJBSucker.sol +144 -0
- package/src/interfaces/IJBSuckerDeployer.sol +40 -0
- package/src/interfaces/IJBSuckerExtended.sol +22 -0
- package/src/interfaces/IJBSuckerRegistry.sol +75 -0
- package/src/interfaces/IOPMessenger.sol +18 -0
- package/src/interfaces/IOPStandardBridge.sol +29 -0
- package/src/interfaces/IWrappedNativeToken.sol +13 -0
- package/src/libraries/ARBAddresses.sol +17 -0
- package/src/libraries/ARBChains.sol +11 -0
- package/src/libraries/CCIPHelper.sol +136 -0
- package/src/structs/JBClaim.sol +13 -0
- package/src/structs/JBInboxTreeRoot.sol +12 -0
- package/src/structs/JBLeaf.sol +14 -0
- package/src/structs/JBMessageRoot.sol +16 -0
- package/src/structs/JBOutboxTree.sol +18 -0
- package/src/structs/JBRemoteToken.sol +17 -0
- package/src/structs/JBSuckerDeployerConfig.sol +12 -0
- package/src/structs/JBSuckersPair.sol +11 -0
- package/src/structs/JBTokenMapping.sol +13 -0
- package/src/utils/MerkleLib.sol +1020 -0
- package/test/Fork.t.sol +514 -0
- package/test/InteropCompat.t.sol +676 -0
- package/test/SuckerAttacks.t.sol +509 -0
- package/test/SuckerDeepAttacks.t.sol +1563 -0
- package/test/mocks/ERC20Mock.sol +36 -0
- package/test/mocks/MockMessenger.sol +42 -0
- package/test/unit/arb.t.sol +28 -0
- package/test/unit/ccip_native_interop.t.sol +719 -0
- package/test/unit/ccip_refund.t.sol +234 -0
- package/test/unit/deployer.t.sol +475 -0
- package/test/unit/emergency.t.sol +305 -0
- package/test/unit/merkle.t.sol +212 -0
- package/test/unit/multi_chain_evolution.t.sol +622 -0
- package/test/unit/registry.t.sol +26 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IBridge} from "@arbitrum/nitro-contracts/src/bridge/IBridge.sol";
|
|
5
|
+
import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol";
|
|
6
|
+
import {IOutbox} from "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol";
|
|
7
|
+
import {AddressAliasHelper} from "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol";
|
|
8
|
+
import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
|
|
9
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
10
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
11
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
12
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
13
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
14
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
15
|
+
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
|
|
16
|
+
|
|
17
|
+
import {JBSucker} from "./JBSucker.sol";
|
|
18
|
+
import {JBArbitrumSuckerDeployer} from "./deployers/JBArbitrumSuckerDeployer.sol";
|
|
19
|
+
import {JBAddToBalanceMode} from "./enums/JBAddToBalanceMode.sol";
|
|
20
|
+
import {JBLayer} from "./enums/JBLayer.sol";
|
|
21
|
+
import {IArbGatewayRouter} from "./interfaces/IArbGatewayRouter.sol";
|
|
22
|
+
import {IArbL1GatewayRouter} from "./interfaces/IArbL1GatewayRouter.sol";
|
|
23
|
+
import {IArbL2GatewayRouter} from "./interfaces/IArbL2GatewayRouter.sol";
|
|
24
|
+
import {IJBArbitrumSucker} from "./interfaces/IJBArbitrumSucker.sol";
|
|
25
|
+
import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
|
|
26
|
+
import {ARBAddresses} from "./libraries/ARBAddresses.sol";
|
|
27
|
+
import {ARBChains} from "./libraries/ARBChains.sol";
|
|
28
|
+
import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
|
|
29
|
+
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
30
|
+
import {JBOutboxTree} from "./structs/JBOutboxTree.sol";
|
|
31
|
+
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
32
|
+
import {MerkleLib} from "./utils/MerkleLib.sol";
|
|
33
|
+
|
|
34
|
+
/// @notice A `JBSucker` implementation to suck tokens between two chains connected by an Arbitrum bridge.
|
|
35
|
+
contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
36
|
+
using BitMaps for BitMaps.BitMap;
|
|
37
|
+
using MerkleLib for MerkleLib.Tree;
|
|
38
|
+
|
|
39
|
+
//*********************************************************************//
|
|
40
|
+
// --------------------------- custom errors ------------------------- //
|
|
41
|
+
//*********************************************************************//
|
|
42
|
+
|
|
43
|
+
error JBArbitrumSucker_NotEnoughGas(uint256 payment, uint256 cost);
|
|
44
|
+
|
|
45
|
+
//*********************************************************************//
|
|
46
|
+
// --------------- public immutable stored properties ---------------- //
|
|
47
|
+
//*********************************************************************//
|
|
48
|
+
|
|
49
|
+
/// @notice The inbox used to send messages between the local and remote sucker.
|
|
50
|
+
IInbox public immutable override ARBINBOX;
|
|
51
|
+
|
|
52
|
+
/// @notice The gateway router for the specific chain
|
|
53
|
+
IArbGatewayRouter public immutable override GATEWAYROUTER;
|
|
54
|
+
|
|
55
|
+
/// @notice The layer that this contract is on.
|
|
56
|
+
JBLayer public immutable override LAYER;
|
|
57
|
+
|
|
58
|
+
//*********************************************************************//
|
|
59
|
+
// ---------------------------- constructor -------------------------- //
|
|
60
|
+
//*********************************************************************//
|
|
61
|
+
|
|
62
|
+
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
63
|
+
/// @param permissions A contract storing permissions.
|
|
64
|
+
/// @param tokens A contract that manages token minting and burning.
|
|
65
|
+
/// @param addToBalanceMode The mode of adding tokens to balance.
|
|
66
|
+
constructor(
|
|
67
|
+
JBArbitrumSuckerDeployer deployer,
|
|
68
|
+
IJBDirectory directory,
|
|
69
|
+
IJBPermissions permissions,
|
|
70
|
+
IJBTokens tokens,
|
|
71
|
+
JBAddToBalanceMode addToBalanceMode,
|
|
72
|
+
address trustedForwarder
|
|
73
|
+
)
|
|
74
|
+
JBSucker(directory, permissions, tokens, addToBalanceMode, trustedForwarder)
|
|
75
|
+
{
|
|
76
|
+
GATEWAYROUTER = JBArbitrumSuckerDeployer(deployer).arbGatewayRouter();
|
|
77
|
+
ARBINBOX = JBArbitrumSuckerDeployer(deployer).arbInbox();
|
|
78
|
+
LAYER = JBArbitrumSuckerDeployer(deployer).arbLayer();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//*********************************************************************//
|
|
82
|
+
// ------------------------ external views --------------------------- //
|
|
83
|
+
//*********************************************************************//
|
|
84
|
+
|
|
85
|
+
/// @notice Returns the chain on which the peer is located.
|
|
86
|
+
/// @return chainId of the peer.
|
|
87
|
+
function peerChainId() external view virtual override returns (uint256) {
|
|
88
|
+
uint256 chainId = block.chainid;
|
|
89
|
+
if (chainId == ARBChains.ETH_CHAINID) return ARBChains.ARB_CHAINID;
|
|
90
|
+
if (chainId == ARBChains.ARB_CHAINID) return ARBChains.ETH_CHAINID;
|
|
91
|
+
if (chainId == ARBChains.ETH_SEP_CHAINID) return ARBChains.ARB_SEP_CHAINID;
|
|
92
|
+
if (chainId == ARBChains.ARB_SEP_CHAINID) return ARBChains.ETH_SEP_CHAINID;
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//*********************************************************************//
|
|
97
|
+
// ------------------------ internal views --------------------------- //
|
|
98
|
+
//*********************************************************************//
|
|
99
|
+
|
|
100
|
+
/// @notice Checks if the `sender` (`_msgSender()`) is a valid representative of the remote peer.
|
|
101
|
+
/// @param sender The message's sender.
|
|
102
|
+
/// @return valid A flag if the sender is a valid representative of the remote peer.
|
|
103
|
+
function _isRemotePeer(address sender) internal view override returns (bool) {
|
|
104
|
+
// Convert the bytes32 peer to an address for comparison with EVM bridge contracts.
|
|
105
|
+
address peerAddress = _toAddress(peer());
|
|
106
|
+
|
|
107
|
+
// If we are the L1 peer,
|
|
108
|
+
if (LAYER == JBLayer.L1) {
|
|
109
|
+
IBridge bridge = ARBINBOX.bridge();
|
|
110
|
+
// Check that the sender is the bridge and that the outbox has our peer as the sender.
|
|
111
|
+
return sender == address(bridge) && peerAddress == IOutbox(bridge.activeOutbox()).l2ToL1Sender();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If we are the L2 peer, check using the `AddressAliasHelper`.
|
|
115
|
+
return sender == AddressAliasHelper.applyL1ToL2Alias(peerAddress);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//*********************************************************************//
|
|
119
|
+
// --------------------- internal transactions ----------------------- //
|
|
120
|
+
//*********************************************************************//
|
|
121
|
+
|
|
122
|
+
/// @notice Uses the L1/L2 gateway to send the root and assets over the bridge to the peer.
|
|
123
|
+
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
|
|
124
|
+
/// @param token The token to bridge the outbox tree for.
|
|
125
|
+
/// @param remoteToken Information about the remote token being bridged to.
|
|
126
|
+
function _sendRootOverAMB(
|
|
127
|
+
uint256 transportPayment,
|
|
128
|
+
uint256,
|
|
129
|
+
address token,
|
|
130
|
+
uint256 amount,
|
|
131
|
+
JBRemoteToken memory remoteToken,
|
|
132
|
+
JBMessageRoot memory message
|
|
133
|
+
)
|
|
134
|
+
internal
|
|
135
|
+
override
|
|
136
|
+
{
|
|
137
|
+
// Bridge expects to be paid
|
|
138
|
+
if (transportPayment == 0 && LAYER == JBLayer.L1) revert JBSucker_ExpectedMsgValue();
|
|
139
|
+
|
|
140
|
+
// Build the calldata that will be send to the peer. This will call `JBSucker.fromRemote` on the remote peer.
|
|
141
|
+
bytes memory data = abi.encodeCall(JBSucker.fromRemote, (message));
|
|
142
|
+
|
|
143
|
+
// Depending on which layer we are on, send the call to the other layer.
|
|
144
|
+
// slither-disable-start out-of-order-retryable
|
|
145
|
+
if (LAYER == JBLayer.L1) {
|
|
146
|
+
_toL2({
|
|
147
|
+
token: token, transportPayment: transportPayment, amount: amount, data: data, remoteToken: remoteToken
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
_toL1({token: token, amount: amount, data: data, remoteToken: remoteToken});
|
|
151
|
+
}
|
|
152
|
+
// slither-disable-end out-of-order-retryable
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// @notice Bridge the `token` and data to the remote L1 chain.
|
|
156
|
+
/// @param token The token to bridge.
|
|
157
|
+
/// @param amount The amount of tokens to bridge.
|
|
158
|
+
/// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
|
|
159
|
+
/// @param remoteToken Information about the remote token to bridged to.
|
|
160
|
+
function _toL1(address token, uint256 amount, bytes memory data, JBRemoteToken memory remoteToken) internal {
|
|
161
|
+
uint256 nativeValue;
|
|
162
|
+
|
|
163
|
+
// Revert if there's a `msg.value`. Sending a message to L1 does not require any payment.
|
|
164
|
+
if (msg.value != 0) {
|
|
165
|
+
// slither-disable-next-line msg-value-loop
|
|
166
|
+
revert JBSucker_UnexpectedMsgValue(msg.value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If the token is an ERC-20, bridge it to the peer.
|
|
170
|
+
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
171
|
+
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
172
|
+
// slither-disable-next-line calls-loop
|
|
173
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: GATEWAYROUTER.getGateway(token), value: amount});
|
|
174
|
+
|
|
175
|
+
// Convert bytes32 types to address at the Arbitrum bridge API boundary.
|
|
176
|
+
// slither-disable-next-line calls-loop,unused-return
|
|
177
|
+
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
178
|
+
.outboundTransfer({
|
|
179
|
+
l1Token: _toAddress(remoteToken.addr), to: _toAddress(peer()), amount: amount, data: bytes("")
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
|
|
183
|
+
nativeValue = amount;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Send the message to the peer with the reclaimed ETH.
|
|
187
|
+
// Address `100` is the ArbSys precompile address.
|
|
188
|
+
// Convert bytes32 peer to address at the Arbitrum API boundary.
|
|
189
|
+
// slither-disable-next-line calls-loop,unused-return
|
|
190
|
+
ArbSys(address(100)).sendTxToL1{value: nativeValue}({destination: _toAddress(peer()), data: data});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// @notice Bridge the `token` and data to the remote L2 chain.
|
|
194
|
+
/// @param token The token to bridge.
|
|
195
|
+
/// @param amount The amount of tokens to bridge.
|
|
196
|
+
/// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
|
|
197
|
+
function _toL2(
|
|
198
|
+
address token,
|
|
199
|
+
uint256 transportPayment,
|
|
200
|
+
uint256 amount,
|
|
201
|
+
bytes memory data,
|
|
202
|
+
JBRemoteToken memory remoteToken
|
|
203
|
+
)
|
|
204
|
+
internal
|
|
205
|
+
{
|
|
206
|
+
uint256 nativeValue;
|
|
207
|
+
uint256 maxFeePerGas = block.basefee;
|
|
208
|
+
uint256 callTransportCost;
|
|
209
|
+
uint256 maxSubmissionCost;
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
// slither-disable-next-line calls-loop
|
|
213
|
+
maxSubmissionCost =
|
|
214
|
+
ARBINBOX.calculateRetryableSubmissionFee({dataLength: data.length, baseFee: maxFeePerGas});
|
|
215
|
+
|
|
216
|
+
// Tracks the cost for the call to the remote peer.
|
|
217
|
+
callTransportCost = maxSubmissionCost + (MESSENGER_BASE_GAS_LIMIT * maxFeePerGas);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If the token is an ERC-20, bridge it to the peer.
|
|
221
|
+
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
222
|
+
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
223
|
+
// Calculate the cost of the ERC-20 transfer. (96 is the length of the abi encoded `data`)
|
|
224
|
+
// slither-disable-next-line calls-loop
|
|
225
|
+
uint256 maxSubmissionCostERC20 =
|
|
226
|
+
ARBINBOX.calculateRetryableSubmissionFee({dataLength: 96, baseFee: maxFeePerGas});
|
|
227
|
+
|
|
228
|
+
uint256 tokenTransportCost = maxSubmissionCostERC20 + (remoteToken.minGas * maxFeePerGas);
|
|
229
|
+
|
|
230
|
+
// Ensure we bridge enough for gas costs on L2 side
|
|
231
|
+
if (transportPayment < callTransportCost + tokenTransportCost) {
|
|
232
|
+
revert JBArbitrumSucker_NotEnoughGas(transportPayment, callTransportCost + tokenTransportCost);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
// The amount of left over transportPayment will be split over the two calls.
|
|
237
|
+
uint256 transportPaymentRemainder = (transportPayment - callTransportCost - tokenTransportCost) / 2;
|
|
238
|
+
tokenTransportCost += transportPaymentRemainder;
|
|
239
|
+
callTransportCost += transportPaymentRemainder;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Approve the tokens to be bridged.
|
|
243
|
+
// slither-disable-next-line calls-loop
|
|
244
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: GATEWAYROUTER.getGateway(token), value: amount});
|
|
245
|
+
|
|
246
|
+
// Perform the ERC-20 bridge transfer. Convert bytes32 peer to address at the Arbitrum bridge API boundary.
|
|
247
|
+
// slither-disable-start out-of-order-retryable
|
|
248
|
+
// slither-disable-next-line calls-loop,unused-return
|
|
249
|
+
IArbL1GatewayRouter(address(GATEWAYROUTER)).outboundTransferCustomRefund{value: tokenTransportCost}({
|
|
250
|
+
token: token,
|
|
251
|
+
refundTo: _msgSender(),
|
|
252
|
+
to: _toAddress(peer()),
|
|
253
|
+
amount: amount,
|
|
254
|
+
maxGas: remoteToken.minGas,
|
|
255
|
+
gasPriceBid: maxFeePerGas,
|
|
256
|
+
data: bytes(abi.encode(maxSubmissionCostERC20, bytes("")))
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
// Ensure we bridge enough for gas costs on L2 side
|
|
260
|
+
if (transportPayment < callTransportCost) {
|
|
261
|
+
revert JBArbitrumSucker_NotEnoughGas(transportPayment, callTransportCost);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If the token is the native token then we only need to do a single call.
|
|
265
|
+
// So it should use all of the transportPayment.
|
|
266
|
+
callTransportCost = transportPayment;
|
|
267
|
+
|
|
268
|
+
// Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
|
|
269
|
+
nativeValue = amount;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Create the retryable ticket containing the merkleRoot.
|
|
273
|
+
// We call unsafe as we do not want the refund address to be aliased to L2.
|
|
274
|
+
// The above check is the same check that makes it `safeCreateRetryableTicket`.
|
|
275
|
+
|
|
276
|
+
// Convert bytes32 peer to address at the Arbitrum inbox API boundary.
|
|
277
|
+
// slither-disable-next-line calls-loop,unused-return
|
|
278
|
+
_createRetryableTicket({
|
|
279
|
+
callTransportCost: callTransportCost,
|
|
280
|
+
nativeValue: nativeValue,
|
|
281
|
+
maxSubmissionCost: maxSubmissionCost,
|
|
282
|
+
maxFeePerGas: maxFeePerGas,
|
|
283
|
+
data: data
|
|
284
|
+
});
|
|
285
|
+
// slither-disable-end out-of-order-retryable
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// @notice Helper to create the retryable ticket, avoiding stack-too-deep.
|
|
289
|
+
function _createRetryableTicket(
|
|
290
|
+
uint256 callTransportCost,
|
|
291
|
+
uint256 nativeValue,
|
|
292
|
+
uint256 maxSubmissionCost,
|
|
293
|
+
uint256 maxFeePerGas,
|
|
294
|
+
bytes memory data
|
|
295
|
+
)
|
|
296
|
+
internal
|
|
297
|
+
{
|
|
298
|
+
address peerAddress = _toAddress(peer());
|
|
299
|
+
// slither-disable-next-line unused-return,calls-loop
|
|
300
|
+
ARBINBOX.unsafeCreateRetryableTicket{value: callTransportCost + nativeValue}({
|
|
301
|
+
to: peerAddress,
|
|
302
|
+
l2CallValue: nativeValue,
|
|
303
|
+
maxSubmissionCost: maxSubmissionCost,
|
|
304
|
+
excessFeeRefundAddress: _msgSender(),
|
|
305
|
+
callValueRefundAddress: peerAddress,
|
|
306
|
+
gasLimit: MESSENGER_BASE_GAS_LIMIT,
|
|
307
|
+
maxFeePerGas: maxFeePerGas,
|
|
308
|
+
data: data
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "./JBOptimismSucker.sol";
|
|
5
|
+
|
|
6
|
+
contract JBBaseSucker is JBOptimismSucker {
|
|
7
|
+
//*********************************************************************//
|
|
8
|
+
// ---------------------------- constructor -------------------------- //
|
|
9
|
+
//*********************************************************************//
|
|
10
|
+
|
|
11
|
+
/// @param deployer A contract that deploys the clones for this contracts.
|
|
12
|
+
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
13
|
+
/// @param permissions A contract storing permissions.
|
|
14
|
+
/// @param tokens A contract that manages token minting and burning.
|
|
15
|
+
/// @param addToBalanceMode The mode of adding tokens to balance.
|
|
16
|
+
constructor(
|
|
17
|
+
JBOptimismSuckerDeployer deployer,
|
|
18
|
+
IJBDirectory directory,
|
|
19
|
+
IJBPermissions permissions,
|
|
20
|
+
IJBTokens tokens,
|
|
21
|
+
JBAddToBalanceMode addToBalanceMode,
|
|
22
|
+
address trustedForwarder
|
|
23
|
+
)
|
|
24
|
+
JBOptimismSucker(deployer, directory, permissions, tokens, addToBalanceMode, trustedForwarder)
|
|
25
|
+
{}
|
|
26
|
+
|
|
27
|
+
//*********************************************************************//
|
|
28
|
+
// ------------------------ external views --------------------------- //
|
|
29
|
+
//*********************************************************************//
|
|
30
|
+
|
|
31
|
+
/// @notice Returns the chain on which the peer is located.
|
|
32
|
+
/// @return chainId of the peer.
|
|
33
|
+
function peerChainId() external view virtual override returns (uint256) {
|
|
34
|
+
uint256 chainId = block.chainid;
|
|
35
|
+
if (chainId == 1) return 8453;
|
|
36
|
+
if (chainId == 8453) return 1;
|
|
37
|
+
if (chainId == 11_155_111) return 84_532;
|
|
38
|
+
if (chainId == 84_532) return 11_155_111;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
|
|
5
|
+
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.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 {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
11
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
12
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
13
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
14
|
+
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
|
|
15
|
+
|
|
16
|
+
import {JBSucker} from "./JBSucker.sol";
|
|
17
|
+
import {JBCCIPSuckerDeployer} from "./deployers/JBCCIPSuckerDeployer.sol";
|
|
18
|
+
import {JBAddToBalanceMode} from "./enums/JBAddToBalanceMode.sol";
|
|
19
|
+
import {ICCIPRouter, IWrappedNativeToken} from "./interfaces/ICCIPRouter.sol";
|
|
20
|
+
import {IJBCCIPSuckerDeployer} from "./interfaces/IJBCCIPSuckerDeployer.sol";
|
|
21
|
+
import {CCIPHelper} from "./libraries/CCIPHelper.sol";
|
|
22
|
+
import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
|
|
23
|
+
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
24
|
+
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
25
|
+
import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
|
|
26
|
+
import {MerkleLib} from "./utils/MerkleLib.sol";
|
|
27
|
+
|
|
28
|
+
/// @notice A `JBSucker` implementation to suck tokens between chains with Chainlink CCIP
|
|
29
|
+
contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
30
|
+
using MerkleLib for MerkleLib.Tree;
|
|
31
|
+
using BitMaps for BitMaps.BitMap;
|
|
32
|
+
|
|
33
|
+
//*********************************************************************//
|
|
34
|
+
// --------------------------- custom errors ------------------------- //
|
|
35
|
+
//*********************************************************************//
|
|
36
|
+
|
|
37
|
+
error JBCCIPSucker_InvalidRouter(address router);
|
|
38
|
+
|
|
39
|
+
//*********************************************************************//
|
|
40
|
+
// ------------------------------ events ----------------------------- //
|
|
41
|
+
//*********************************************************************//
|
|
42
|
+
|
|
43
|
+
/// @notice Emitted when a transport payment refund fails after a successful CCIP send.
|
|
44
|
+
/// @dev The refunded ETH is permanently stuck in this contract — there is no recovery function.
|
|
45
|
+
/// This is an accepted tradeoff to avoid reverting after CCIP has committed the bridge message.
|
|
46
|
+
/// @param recipient The address that was supposed to receive the refund.
|
|
47
|
+
/// @param amount The amount of the failed refund (permanently stuck in this contract).
|
|
48
|
+
event TransportPaymentRefundFailed(address indexed recipient, uint256 amount);
|
|
49
|
+
|
|
50
|
+
//*********************************************************************//
|
|
51
|
+
// --------------- public immutable stored properties ---------------- //
|
|
52
|
+
//*********************************************************************//
|
|
53
|
+
|
|
54
|
+
/// @notice The CCIP router used to bridge tokens between the local and remote chain.
|
|
55
|
+
ICCIPRouter public immutable CCIP_ROUTER;
|
|
56
|
+
|
|
57
|
+
/// @notice The chain id of the remote chain.
|
|
58
|
+
uint256 public immutable REMOTE_CHAIN_ID;
|
|
59
|
+
|
|
60
|
+
/// @notice The CCIP chain selector of the remote chain.
|
|
61
|
+
uint64 public immutable REMOTE_CHAIN_SELECTOR;
|
|
62
|
+
|
|
63
|
+
//*********************************************************************//
|
|
64
|
+
// ---------------------------- constructor -------------------------- //
|
|
65
|
+
//*********************************************************************//
|
|
66
|
+
|
|
67
|
+
/// @param deployer A contract that deploys the clones for this contracts.
|
|
68
|
+
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
69
|
+
/// @param tokens A contract that manages token minting and burning.
|
|
70
|
+
/// @param permissions A contract storing permissions.
|
|
71
|
+
/// @param addToBalanceMode The mode of adding tokens to balance.
|
|
72
|
+
constructor(
|
|
73
|
+
JBCCIPSuckerDeployer deployer,
|
|
74
|
+
IJBDirectory directory,
|
|
75
|
+
IJBTokens tokens,
|
|
76
|
+
IJBPermissions permissions,
|
|
77
|
+
JBAddToBalanceMode addToBalanceMode,
|
|
78
|
+
address trustedForwarder
|
|
79
|
+
)
|
|
80
|
+
JBSucker(directory, permissions, tokens, addToBalanceMode, trustedForwarder)
|
|
81
|
+
{
|
|
82
|
+
REMOTE_CHAIN_ID = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainId();
|
|
83
|
+
REMOTE_CHAIN_SELECTOR = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainSelector();
|
|
84
|
+
CCIP_ROUTER = IJBCCIPSuckerDeployer(deployer).ccipRouter();
|
|
85
|
+
|
|
86
|
+
if (address(CCIP_ROUTER) == address(0)) revert JBCCIPSucker_InvalidRouter(address(CCIP_ROUTER));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//*********************************************************************//
|
|
90
|
+
// ------------------------ external views --------------------------- //
|
|
91
|
+
//*********************************************************************//
|
|
92
|
+
|
|
93
|
+
/// @notice Returns the chain on which the peer is located.
|
|
94
|
+
/// @return chainId of the peer.
|
|
95
|
+
function peerChainId() external view virtual override returns (uint256 chainId) {
|
|
96
|
+
// Return the remote chain id
|
|
97
|
+
return REMOTE_CHAIN_ID;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//*********************************************************************//
|
|
101
|
+
// ------------------------- public views ---------------------------- //
|
|
102
|
+
//*********************************************************************//
|
|
103
|
+
|
|
104
|
+
/// @notice Return the current router
|
|
105
|
+
/// @return CCIP router address
|
|
106
|
+
function getRouter() public view returns (address) {
|
|
107
|
+
return address(CCIP_ROUTER);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// @notice IERC165 supports an interfaceId
|
|
111
|
+
/// @param interfaceId The interfaceId to check
|
|
112
|
+
/// @return true if the interfaceId is supported
|
|
113
|
+
/// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver
|
|
114
|
+
/// e.g. return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId
|
|
115
|
+
/// This allows CCIP to check if ccipReceive is available before calling it.
|
|
116
|
+
/// If this returns false or reverts, only tokens are transferred to the receiver.
|
|
117
|
+
/// If this returns true, tokens are transferred and ccipReceive is called atomically.
|
|
118
|
+
/// Additionally, if the receiver address does not have code associated with
|
|
119
|
+
/// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred.
|
|
120
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
121
|
+
return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//*********************************************************************//
|
|
125
|
+
// --------------------- external transactions ----------------------- //
|
|
126
|
+
//*********************************************************************//
|
|
127
|
+
|
|
128
|
+
/// @notice The entrypoint for the CCIP router to call. This function should
|
|
129
|
+
/// never revert, all errors should be handled internally in this contract.
|
|
130
|
+
/// @param any2EvmMessage The message to process.
|
|
131
|
+
/// @dev Extremely important to ensure only router calls this.
|
|
132
|
+
function ccipReceive(Client.Any2EVMMessage calldata any2EvmMessage) external override {
|
|
133
|
+
// only calls from the set router are accepted.
|
|
134
|
+
if (_msgSender() != address(CCIP_ROUTER)) revert JBSucker_NotPeer(_toBytes32(_msgSender()));
|
|
135
|
+
|
|
136
|
+
// Decode the message root from the peer
|
|
137
|
+
JBMessageRoot memory root = abi.decode(any2EvmMessage.data, (JBMessageRoot));
|
|
138
|
+
address origin = abi.decode(any2EvmMessage.sender, (address));
|
|
139
|
+
|
|
140
|
+
// Make sure that the message came from our peer.
|
|
141
|
+
if (origin != _toAddress(peer()) || any2EvmMessage.sourceChainSelector != REMOTE_CHAIN_SELECTOR) {
|
|
142
|
+
revert JBSucker_NotPeer(_toBytes32(origin));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Note (M-28): We intentionally do NOT validate root.amount against destTokenAmounts[0].amount here.
|
|
146
|
+
// CCIP fees are paid separately (via feeToken), so delivered amounts should always match what was sent.
|
|
147
|
+
// If we reverted on a mismatch, the tokens already transferred by CCIP would be locked in the router
|
|
148
|
+
// with no recovery path — a concrete fund-loss risk that outweighs the theoretical defense-in-depth
|
|
149
|
+
// benefit against a CCIP-level failure or peer compromise. See AUDIT_FINDINGS.md M-28.
|
|
150
|
+
|
|
151
|
+
// We either send no tokens or a single token.
|
|
152
|
+
if (any2EvmMessage.destTokenAmounts.length == 1) {
|
|
153
|
+
// The sucker only handles ERC-20s or native. CCIP delivers wrapped native (WETH).
|
|
154
|
+
Client.EVMTokenAmount memory tokenAmount = any2EvmMessage.destTokenAmounts[0];
|
|
155
|
+
// Unwrap WETH -> ETH only when the root says the token is NATIVE_TOKEN.
|
|
156
|
+
// When root.token is an ERC-20 address (e.g., bridging to a chain where ETH is an ERC-20), no unwrap.
|
|
157
|
+
if (root.token == _toBytes32(JBConstants.NATIVE_TOKEN)) {
|
|
158
|
+
// We can (safely) assume that the token that is set in the `destTokenAmounts` is a valid wrapped
|
|
159
|
+
// native.
|
|
160
|
+
// If this ends up not being the case then our sanity check to see if we unwrapped the native asset will
|
|
161
|
+
// fail.
|
|
162
|
+
IWrappedNativeToken wrapped_native = IWrappedNativeToken(tokenAmount.token);
|
|
163
|
+
uint256 balanceBefore = _balanceOf({token: JBConstants.NATIVE_TOKEN, addr: address(this)});
|
|
164
|
+
|
|
165
|
+
// Withdraw the wrapped native asset.
|
|
166
|
+
wrapped_native.withdraw(tokenAmount.amount);
|
|
167
|
+
|
|
168
|
+
// Sanity check the unwrapping of the native asset.
|
|
169
|
+
// slither-disable-next-line incorrect-equality
|
|
170
|
+
assert(
|
|
171
|
+
balanceBefore + tokenAmount.amount
|
|
172
|
+
== _balanceOf({token: JBConstants.NATIVE_TOKEN, addr: address(this)})
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Call ourselves to process the root.
|
|
178
|
+
this.fromRemote(root);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//*********************************************************************//
|
|
182
|
+
// --------------------- internal transactions ----------------------- //
|
|
183
|
+
//*********************************************************************//
|
|
184
|
+
|
|
185
|
+
/// @notice Unused in this context.
|
|
186
|
+
function _isRemotePeer(address sender) internal view override returns (bool _valid) {
|
|
187
|
+
// NOTICE: We do not check if its the `peer` here, as this contract is supposed to be the caller *NOT* the peer.
|
|
188
|
+
return sender == address(this);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @notice Uses CCIP to send the root and assets over the bridge to the peer.
|
|
192
|
+
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
|
|
193
|
+
/// @param token The token to bridge the outbox tree for.
|
|
194
|
+
/// @param remoteToken Information about the remote token being bridged to.
|
|
195
|
+
function _sendRootOverAMB(
|
|
196
|
+
uint256 transportPayment,
|
|
197
|
+
uint256,
|
|
198
|
+
address token,
|
|
199
|
+
uint256 amount,
|
|
200
|
+
JBRemoteToken memory remoteToken,
|
|
201
|
+
JBMessageRoot memory sucker_message
|
|
202
|
+
)
|
|
203
|
+
internal
|
|
204
|
+
override
|
|
205
|
+
{
|
|
206
|
+
// Make sure we are attempting to pay the bridge
|
|
207
|
+
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue();
|
|
208
|
+
|
|
209
|
+
uint256 gasLimit = MESSENGER_BASE_GAS_LIMIT;
|
|
210
|
+
Client.EVMTokenAmount[] memory tokenAmounts;
|
|
211
|
+
if (amount != 0) {
|
|
212
|
+
// If we also do an asset transfer then we increase the min required gas amount.
|
|
213
|
+
gasLimit += remoteToken.minGas;
|
|
214
|
+
|
|
215
|
+
// Wrap native ETH -> WETH for CCIP bridging. CCIP only transports ERC-20s.
|
|
216
|
+
// This is why `_validateTokenMapping` enforces minGas for native tokens too.
|
|
217
|
+
if (token == JBConstants.NATIVE_TOKEN) {
|
|
218
|
+
// Get the wrapped native token.
|
|
219
|
+
// slither-disable-next-line calls-loop
|
|
220
|
+
IWrappedNativeToken wrapped_native = CCIP_ROUTER.getWrappedNative();
|
|
221
|
+
// Deposit the wrapped native asset.
|
|
222
|
+
// slither-disable-next-line calls-loop,arbitrary-send-eth
|
|
223
|
+
wrapped_native.deposit{value: amount}();
|
|
224
|
+
// Update the token to be the wrapped native asset.
|
|
225
|
+
token = address(wrapped_native);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Set the token amounts
|
|
229
|
+
tokenAmounts = new Client.EVMTokenAmount[](1);
|
|
230
|
+
tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount});
|
|
231
|
+
|
|
232
|
+
// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
|
|
233
|
+
SafeERC20.forceApprove(IERC20(token), address(CCIP_ROUTER), amount);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
|
|
237
|
+
// CCIP requires EVM addresses, so convert the bytes32 peer to an address for the receiver field.
|
|
238
|
+
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
|
|
239
|
+
receiver: abi.encode(_toAddress(peer())),
|
|
240
|
+
data: abi.encode(sucker_message),
|
|
241
|
+
tokenAmounts: tokenAmounts,
|
|
242
|
+
extraArgs: Client._argsToBytes(
|
|
243
|
+
// Additional arguments, setting gas limit
|
|
244
|
+
Client.EVMExtraArgsV1({gasLimit: gasLimit})
|
|
245
|
+
),
|
|
246
|
+
// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees,
|
|
247
|
+
// We pay in the native asset.
|
|
248
|
+
feeToken: address(0)
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Get the fee required to send the CCIP message
|
|
252
|
+
// slither-disable-next-line calls-loop
|
|
253
|
+
uint256 fees = CCIP_ROUTER.getFee({destinationChainSelector: REMOTE_CHAIN_SELECTOR, message: message});
|
|
254
|
+
|
|
255
|
+
if (fees > transportPayment) {
|
|
256
|
+
revert JBSucker_InsufficientMsgValue(transportPayment, fees);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// slither-disable-next-line calls-loop,unused-return
|
|
260
|
+
CCIP_ROUTER.ccipSend{value: fees}({destinationChainSelector: REMOTE_CHAIN_SELECTOR, message: message});
|
|
261
|
+
|
|
262
|
+
// Refund remaining balance. We use a low-level call that does not revert on failure because
|
|
263
|
+
// `ccipSend` above has already committed the bridge message and transferred the tokens. If we
|
|
264
|
+
// reverted here (e.g. because the caller is a non-payable contract), the entire transaction
|
|
265
|
+
// would roll back — but the CCIP message is already in-flight. The tokens would be gone, the
|
|
266
|
+
// merkle root never gets processed, and the outbox state is inconsistent.
|
|
267
|
+
//
|
|
268
|
+
// If the refund fails, the ETH (transportPayment - fees) will be permanently stuck in this
|
|
269
|
+
// contract. There is no sweep or recovery function — `addOutstandingAmountToBalance` only
|
|
270
|
+
// moves funds tracked via `fromRemote`, not arbitrary ETH. This is an accepted tradeoff:
|
|
271
|
+
// stuck dust from a fee overpayment is far less harmful than bricking the entire bridge
|
|
272
|
+
// operation. The event provides observability so it doesn't go unnoticed.
|
|
273
|
+
//
|
|
274
|
+
// See AUDIT_FINDINGS.md M-2 for the full analysis.
|
|
275
|
+
uint256 refundAmount = transportPayment - fees;
|
|
276
|
+
if (refundAmount != 0) {
|
|
277
|
+
// slither-disable-next-line calls-loop,msg-value-loop,reentrancy-events
|
|
278
|
+
(bool sent,) = _msgSender().call{value: refundAmount}("");
|
|
279
|
+
if (!sent) emit TransportPaymentRefundFailed(_msgSender(), refundAmount);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// @notice Allow sucker implementations to add/override mapping rules to suite their specific needs.
|
|
284
|
+
/// @dev Unlike OP/Arbitrum suckers (which share ETH as native on both chains), this CCIP sucker can connect
|
|
285
|
+
/// chains with different native tokens. This means `NATIVE_TOKEN` may map to an ERC-20 on the remote chain.
|
|
286
|
+
///
|
|
287
|
+
/// Example: ETH mainnet (native = ETH) <-> Celo (native = CELO, ETH is an ERC-20).
|
|
288
|
+
/// - On mainnet: `mapToken({localToken: NATIVE_TOKEN, remoteToken: celoETH_address})`
|
|
289
|
+
/// - Sending: `_sendRootOverAMB` wraps native ETH -> WETH, bridges WETH via CCIP.
|
|
290
|
+
/// - Receiving: `ccipReceive` checks `root.token == NATIVE_TOKEN` to decide whether to unwrap WETH -> ETH.
|
|
291
|
+
/// If `root.token` is an ERC-20 address (like celoETH), no unwrap occurs — tokens stay as ERC-20.
|
|
292
|
+
///
|
|
293
|
+
/// The base class restriction (`NATIVE_TOKEN` can only map to `NATIVE_TOKEN` or `address(0)`) is intentionally
|
|
294
|
+
/// removed here. The base class retains that restriction for OP/Arbitrum where both chains share ETH as native.
|
|
295
|
+
function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual override {
|
|
296
|
+
// Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
|
|
297
|
+
// funds. CCIP wraps native tokens to WETH before bridging (see `_sendRootOverAMB`), so ALL tokens —
|
|
298
|
+
// including native — need sufficient gas for an ERC-20 transfer on the remote chain.
|
|
299
|
+
if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
|
|
300
|
+
revert JBSucker_BelowMinGas(map.minGas, MESSENGER_ERC20_MIN_GAS_LIMIT);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|