From 15e815b21a11c5d30eb57abcc6223c8c56b9e7d1 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 16 Sep 2024 12:46:53 -0500 Subject: [PATCH 01/45] feat: add L3 adapter to send messages to an L2 forwarder Signed-off-by: bennett --- .../chain-adapters/Arbitrum_L3_Adapter.sol | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 contracts/chain-adapters/Arbitrum_L3_Adapter.sol diff --git a/contracts/chain-adapters/Arbitrum_L3_Adapter.sol b/contracts/chain-adapters/Arbitrum_L3_Adapter.sol new file mode 100644 index 00000000..51bb0a8f --- /dev/null +++ b/contracts/chain-adapters/Arbitrum_L3_Adapter.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; + +/** + * @notice Contract containing logic to send messages from L1 to Arbitrum-like L3s using an intermediate L2 message forwarder. + * @notice This contract requires an L2 forwarder contract to be deployed, since we overwrite the target field to this new target. + * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be + * called via delegatecall, which will execute this contract's logic within the context of the originating contract. + * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods + * that call this contract's logic guard against reentrancy. + */ + +// solhint-disable-next-line contract-name-camelcase +contract Arbitrum_L3_Adapter is AdapterInterface { + address public immutable l2Adapter; + address public immutable l2Forwarder; + + error RelayMessageFailed(); + error RelayTokensFailed(address l1Token); + + /** + * @notice Constructs new Adapter for sending tokens/messages to Arbitrum-like L3s. + * @param _l2Adapter Address of the adapter contract on mainnet which implements message transfers + * and token relays. + */ + constructor(address _l2Adapter, address _l2Forwarder) { + l2Adapter = _l2Adapter; + l2Forwarder = _l2Forwarder; + } + + /** + * @notice Send cross-chain message to target on L2, which is forwarded to the Arbitrum-like L3. + * @dev there is a bijective mapping of L3 adapters (on L1) to L2 forwarders to L3 spoke pools. The + * spoke pool address is stored by the L2 forwarder and the L2 forwarder address is stored in this contract. + * @param message Data to send to target. + */ + function relayMessage(address, bytes memory message) external payable override { + (bool success, ) = l2Adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (l2Forwarder, message)) + ); + if (!success) revert RelayMessageFailed(); + } + + /** + * @notice Bridge tokens to an Arbitrum-like L3, using an L2 forwarder. + * @param l1Token L1 token to deposit. + * @param l2Token L2 token to receive. + * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. + * @dev we discard the "to" field since tokens are always sent to the l2Forwarder. + */ + function relayTokens( + address l1Token, + address l2Token, // l2Token is unused for Arbitrum. + uint256 amount, + address + ) external payable override { + (bool success, ) = l2Adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Forwarder)) + ); + if (!success) revert RelayTokensFailed(l1Token); + } +} From f9c0d62772d4eb866b9f94ba92efa4a8b77dbb35 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 17 Sep 2024 14:37:44 -0500 Subject: [PATCH 02/45] feat: add L2 forwarder interface Signed-off-by: bennett --- contracts/chain-adapters/Blast_Adapter.sol | 2 +- contracts/chain-adapters/Lisk_Adapter.sol | 2 +- contracts/chain-adapters/Mode_Adapter.sol | 2 +- contracts/chain-adapters/Redstone_Adapter.sol | 2 +- contracts/chain-adapters/Zora_Adapter.sol | 2 +- .../interfaces/ArbitrumForwarderInterface.sol | 254 ++++++++++++++++++ contracts/libraries/CircleCCTPAdapter.sol | 2 +- 7 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol diff --git a/contracts/chain-adapters/Blast_Adapter.sol b/contracts/chain-adapters/Blast_Adapter.sol index 83fa8053..e3cdc514 100644 --- a/contracts/chain-adapters/Blast_Adapter.sol +++ b/contracts/chain-adapters/Blast_Adapter.sol @@ -76,7 +76,7 @@ contract Blast_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapte ) CrossDomainEnabled(_crossDomainMessenger) // Hardcode cctp messenger to 0x0 to disable CCTP bridging. - CircleCCTPAdapter(_l1Usdc, ITokenMessenger(address(0)), CircleDomainIds.UNINTIALIZED) + CircleCCTPAdapter(_l1Usdc, ITokenMessenger(address(0)), CircleDomainIds.UNINITIALIZED) { L1_WETH = _l1Weth; L1_STANDARD_BRIDGE = _l1StandardBridge; diff --git a/contracts/chain-adapters/Lisk_Adapter.sol b/contracts/chain-adapters/Lisk_Adapter.sol index 7b0e749e..bea4f65f 100644 --- a/contracts/chain-adapters/Lisk_Adapter.sol +++ b/contracts/chain-adapters/Lisk_Adapter.sol @@ -51,7 +51,7 @@ contract Lisk_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter _l1Usdc, // Hardcode cctp messenger to 0x0 to disable CCTP bridging. ITokenMessenger(address(0)), - CircleDomainIds.UNINTIALIZED + CircleDomainIds.UNINITIALIZED ) { L1_WETH = _l1Weth; diff --git a/contracts/chain-adapters/Mode_Adapter.sol b/contracts/chain-adapters/Mode_Adapter.sol index 389dd684..29e39877 100644 --- a/contracts/chain-adapters/Mode_Adapter.sol +++ b/contracts/chain-adapters/Mode_Adapter.sol @@ -51,7 +51,7 @@ contract Mode_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter _l1Usdc, // Hardcode cctp messenger to 0x0 to disable CCTP bridging. ITokenMessenger(address(0)), - CircleDomainIds.UNINTIALIZED + CircleDomainIds.UNINITIALIZED ) { L1_WETH = _l1Weth; diff --git a/contracts/chain-adapters/Redstone_Adapter.sol b/contracts/chain-adapters/Redstone_Adapter.sol index 401c33be..65fc07a4 100644 --- a/contracts/chain-adapters/Redstone_Adapter.sol +++ b/contracts/chain-adapters/Redstone_Adapter.sol @@ -51,7 +51,7 @@ contract Redstone_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAda _l1Usdc, // Hardcode cctp messenger to 0x0 to disable CCTP bridging. ITokenMessenger(address(0)), - CircleDomainIds.UNINTIALIZED + CircleDomainIds.UNINITIALIZED ) { L1_WETH = _l1Weth; diff --git a/contracts/chain-adapters/Zora_Adapter.sol b/contracts/chain-adapters/Zora_Adapter.sol index c36288fd..d0de83ce 100644 --- a/contracts/chain-adapters/Zora_Adapter.sol +++ b/contracts/chain-adapters/Zora_Adapter.sol @@ -51,7 +51,7 @@ contract Zora_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter _l1Usdc, // Hardcode cctp messenger to 0x0 to disable CCTP bridging. ITokenMessenger(address(0)), - CircleDomainIds.UNINTIALIZED + CircleDomainIds.UNINITIALIZED ) { L1_WETH = _l1Weth; diff --git a/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol b/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol new file mode 100644 index 00000000..8ed8c7d7 --- /dev/null +++ b/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Staging ground for incoming and outgoing messages + * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is + * used as native currency on L3. + * @dev Fees are paid in this token. There are certain restrictions on the native token: + * - The token can't be rebasing or have a transfer fee + * - The token must only be transferrable via a call to the token address itself + * - The token must only be able to set allowance via a call to the token address itself + * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert + * - The token must have a max of 2^256 - 1 wei total supply unscaled + * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals + */ +interface ArbitrumERC20Bridge { + /** + * @notice Returns token that is escrowed in bridge on L2 side and minted on L3 as native currency. + * @dev This function doesn't exist on the generic Bridge interface. + * @return address of the native token. + */ + function nativeToken() external view returns (address); +} + +/** + * @title Inbox for user and contract originated messages + * @notice Messages created via this inbox are enqueued in the delayed accumulator + * to await inclusion in the SequencerInbox + */ +interface ArbitrumInboxLike { + /** + * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface + * to return an ArbitrumERC20Bridge instead of a more generic Bridge interface. + * @return address of the bridge. + */ + function bridge() external view returns (ArbitrumERC20Bridge); + + /** + * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts + * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying + * for L2 to L3 message using a custom gas token. + * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on L3 + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - l3CallValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. + * @param to destination L3 contract address + * @param l3CallValue call value for retryable L3 message + * @param maxSubmissionCost Max gas deducted from user's L3 balance to cover base submission fee + * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on L3. + * @param callValueRefundAddress l3Callvalue gets credited here on L3 if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on L3. + * @param gasLimit Max gas deducted from user's L3 balance to cover L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost + * @param data ABI encoded data of L3 message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 l3CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256); +} + +/** + * @notice Generic gateway contract for bridging standard ERC20s to Arbitrum-like networks. + */ +interface ArbitrumERC20GatewayLike { + /** + * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. + * @dev L3 address alias will not be applied to the following types of addresses on L2: + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * @param _l2Token L2 address of ERC20 + * @param _refundTo Account, or its L3 alias if it have code in L2, to be credited with excess gas refund in L3 + * @param _to Account to be credited with the tokens in the L3 (can be the user's L3 account or a contract), + * not subject to L3 aliasing. This account, or its L3 alias if it have code in L2, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's L3 balance to cover L3 execution + * @param _gasPriceBid Gas price for L3 execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransferCustomRefund( + address _l2Token, + address _refundTo, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + /** + * @notice get ERC20 gateway for token. + * @param _token ERC20 address. + * @return address of ERC20 gateway. + */ + function getGateway(address _token) external view returns (address); +} + +/** + * @notice Contract containing logic to send messages from L2 to Arbitrum-like L3s. + * @dev This contract is meant to share code for Arbitrum L2 forwarder contracts deployed to various + * different L2 architectures (e.g. Base, Arbitrum, ZkSync, etc.). It assumes that the L3 conforms + * to an Arbitrum-like interface. + */ + +// solhint-disable-next-line contract-name-camelcase +abstract contract ArbitrumForwarderInterface { + // Amount of gas token allocated to pay for the base submission fee. The base submission fee is a parameter unique to + // retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their + // ticket’s calldata in the retry buffer. (current base submission fee is queryable via + // ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address + // 0x000000000000000000000000000000000000006E. + // @dev This is immutable because we don't know what precision the custom gas token has. + uint256 public immutable L3_MAX_SUBMISSION_COST; + + // L3 Gas price bid for immediate L3 execution attempt (queryable via standard eth*gasPrice RPC) + uint256 public constant L3_GAS_PRICE = 5e9; // 5 gWei + + // Native token expected to be sent in L3 message. Should be 0 for all use cases of this constant, which + // includes sending messages from L2 to L3 and sending Custom gas token ERC20's, which won't be the native token + // on the L3 by definition. + uint256 public constant L3_CALL_VALUE = 0; + + // Gas limit for L3 execution of a cross chain token transfer sent via the inbox. + uint32 public constant RELAY_TOKENS_L3_GAS_LIMIT = 300_000; + // Gas limit for L3 execution of a message sent via the inbox. + uint32 public constant RELAY_MESSAGE_L3_GAS_LIMIT = 2_000_000; + + // This address on L3 receives extra gas token that is left over after relaying a message via the inbox. + address public immutable L3_REFUND_L3_ADDRESS; + + // This is the address which receives messages and tokens on L3, assumed to be the spoke pool. + address public immutable L3_SPOKE_POOL; + + // This is the address which has permission to relay root bundles/messages to the L3 spoke pool. + address public immutable CROSS_DOMAIN_ADMIN; + + // Inbox system contract to send messages to Arbitrum-like L3s. Token bridges use this to send tokens to L3. + // https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol + ArbitrumInboxLike public immutable L2_INBOX; + + // Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this + // contract calls the Inbox. + // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol + // Gateway used for communicating with chains that use custom gas tokens: + // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol + ArbitrumERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; + + error RescueFailed(); + + /* + * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have + * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. + */ + modifier onlyAdmin() { + _requireAdminSender(); + _; + } + + /** + * @notice Constructs new Adapter. + * @param _l2ArbitrumInbox Inbox helper contract to send messages to Arbitrum-like L3s. + * @param _l2ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum-like L3s. + * @param _l3RefundL3Address L3 address to receive gas refunds on after a message is relayed. + * @param _l3MaxSubmissionCost Amount of gas token allocated to pay for the base submission fee. The base + * submission fee is a parameter unique to Arbitrum retryable transactions. This value is hardcoded + * and used for all messages sent by this adapter. + * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily + * stored in this contract on L2. + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. + * In practice, this is the hub pool. + */ + constructor( + ArbitrumInboxLike _l2ArbitrumInbox, + ArbitrumERC20GatewayLike _l2ERC20GatewayRouter, + address _l3RefundL3Address, + uint256 _l3MaxSubmissionCost, + address _l3SpokePool, + address _crossDomainAdmin + ) { + L2_INBOX = _l2ArbitrumInbox; + L2_ERC20_GATEWAY_ROUTER = _l2ERC20GatewayRouter; + L3_REFUND_L3_ADDRESS = _l3RefundL3Address; + L3_MAX_SUBMISSION_COST = _l3MaxSubmissionCost; + L3_SPOKE_POOL = _l3SpokePool; + CROSS_DOMAIN_ADMIN = _crossDomainAdmin; + } + + /** + * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function + * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward + * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder + */ + fallback() external onlyAdmin { + _relayMessage(L3_SPOKE_POOL, msg.data); + } + + /** + * @notice This function can only be called via a rescue adapter, and is used to recover potentially stuck + * funds on this contract. + */ + function rescue( + address target, + uint256 value, + bytes memory message + ) external onlyAdmin { + (bool success, ) = target.call{ value: value }(message); + if (!success) revert RescueFailed(); + } + + /** + * @notice Bridge tokens to an Arbitrum-like L3. + * @notice This contract must hold at least getL2CallValue() amount of ETH or custom gas token + * to send a message via the Inbox successfully, or the message will get stuck. + * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. + * @param l2Token L2 token to deposit. + * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. + */ + function relayTokens(address l2Token, uint256 amount) external virtual; + + /** + * @notice Relay a message to a contract on L2. Implementation changes on whether the + * target bridge supports a custom gas token or not. + * @notice This contract must hold at least getL2CallValue() amount of the custom gas token + * to send a message via the Inbox successfully, or the message will get stuck. + * @notice This function should be implmented differently based on whether the L2-L3 bridge + * requires custom gas tokens to fund cross-chain transactions. + */ + function _relayMessage(address target, bytes memory message) internal virtual; + + // Function to be overridden to accomodate for each L2's unique method of address aliasing. + function _requireAdminSender() internal virtual; + + /** + * @notice Returns required amount of gas token to send a message via the Inbox. + * @param l3GasLimit L3 gas limit for the message. + * @return amount of gas token that this contract needs to hold in order for relayMessage to succeed. + */ + function getL2CallValue(uint32 l3GasLimit) public view returns (uint256) { + return L3_MAX_SUBMISSION_COST + L3_GAS_PRICE * l3GasLimit; + } + + function _contractHasSufficientGasToken(uint32 l3GasLimit) internal view virtual returns (uint256); +} diff --git a/contracts/libraries/CircleCCTPAdapter.sol b/contracts/libraries/CircleCCTPAdapter.sol index 043c1a82..6403ed4c 100644 --- a/contracts/libraries/CircleCCTPAdapter.sol +++ b/contracts/libraries/CircleCCTPAdapter.sol @@ -13,7 +13,7 @@ library CircleDomainIds { uint32 public constant Polygon = 7; // Use this value for placeholder purposes only for adapters that extend this adapter but haven't yet been // assigned a domain ID by Circle. - uint32 public constant UNINTIALIZED = type(uint32).max; + uint32 public constant UNINITIALIZED = type(uint32).max; } /** From 2daf57b25a2cff090b1feecbb4eadab1a707a81f Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 17 Sep 2024 15:13:01 -0500 Subject: [PATCH 03/45] change name of l2Adapter to adapter Signed-off-by: bennett --- .../chain-adapters/Arbitrum_L3_Adapter.sol | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/chain-adapters/Arbitrum_L3_Adapter.sol b/contracts/chain-adapters/Arbitrum_L3_Adapter.sol index 51bb0a8f..d390aa8d 100644 --- a/contracts/chain-adapters/Arbitrum_L3_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_L3_Adapter.sol @@ -14,7 +14,7 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; // solhint-disable-next-line contract-name-camelcase contract Arbitrum_L3_Adapter is AdapterInterface { - address public immutable l2Adapter; + address public immutable adapter; address public immutable l2Forwarder; error RelayMessageFailed(); @@ -22,11 +22,12 @@ contract Arbitrum_L3_Adapter is AdapterInterface { /** * @notice Constructs new Adapter for sending tokens/messages to Arbitrum-like L3s. - * @param _l2Adapter Address of the adapter contract on mainnet which implements message transfers + * @param _adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. + * @param _l2Forwarder Address of the l2 forwarder contract which relays messages up to the L3 spoke pool. */ - constructor(address _l2Adapter, address _l2Forwarder) { - l2Adapter = _l2Adapter; + constructor(address _adapter, address _l2Forwarder) { + adapter = _adapter; l2Forwarder = _l2Forwarder; } @@ -37,9 +38,7 @@ contract Arbitrum_L3_Adapter is AdapterInterface { * @param message Data to send to target. */ function relayMessage(address, bytes memory message) external payable override { - (bool success, ) = l2Adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (l2Forwarder, message)) - ); + (bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Forwarder, message))); if (!success) revert RelayMessageFailed(); } @@ -52,11 +51,11 @@ contract Arbitrum_L3_Adapter is AdapterInterface { */ function relayTokens( address l1Token, - address l2Token, // l2Token is unused for Arbitrum. + address l2Token, uint256 amount, address ) external payable override { - (bool success, ) = l2Adapter.delegatecall( + (bool success, ) = adapter.delegatecall( abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Forwarder)) ); if (!success) revert RelayTokensFailed(l1Token); From cea033783c93f534ca3f9d767b63bd04a02e810b Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 17 Sep 2024 19:00:37 -0500 Subject: [PATCH 04/45] sync with upstream changes Signed-off-by: bennett --- .../interfaces/ArbitrumForwarderInterface.sol | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol b/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol index 8ed8c7d7..cf6040c7 100644 --- a/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol +++ b/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol @@ -35,6 +35,33 @@ interface ArbitrumInboxLike { */ function bridge() external view returns (ArbitrumERC20Bridge); + /** + * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. + * all msg.value will deposited to callValueRefundAddress on L3 + * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging + * @param to destination L3 contract address + * @param l3CallValue call value for retryable L3 message + * @param maxSubmissionCost Max gas deducted from user's L3 balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L3 balance + * @param callValueRefundAddress l3Callvalue gets credited here on L3 if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's L3 balance to cover L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of L3 message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 l3CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + /** * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying @@ -156,6 +183,9 @@ abstract contract ArbitrumForwarderInterface { // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol ArbitrumERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; + event TokensForwarded(address indexed l2Token, uint256 amount); + event MessageForwarded(address indexed target, bytes message); + error RescueFailed(); /* @@ -196,12 +226,15 @@ abstract contract ArbitrumForwarderInterface { CROSS_DOMAIN_ADMIN = _crossDomainAdmin; } + // Added so that this function may receive ETH in the event of stuck transactions. + receive() external payable {} + /** * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder */ - fallback() external onlyAdmin { + fallback() external payable onlyAdmin { _relayMessage(L3_SPOKE_POOL, msg.data); } @@ -226,7 +259,7 @@ abstract contract ArbitrumForwarderInterface { * @param l2Token L2 token to deposit. * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. */ - function relayTokens(address l2Token, uint256 amount) external virtual; + function relayTokens(address l2Token, uint256 amount) external payable virtual; /** * @notice Relay a message to a contract on L2. Implementation changes on whether the @@ -249,6 +282,4 @@ abstract contract ArbitrumForwarderInterface { function getL2CallValue(uint32 l3GasLimit) public view returns (uint256) { return L3_MAX_SUBMISSION_COST + L3_GAS_PRICE * l3GasLimit; } - - function _contractHasSufficientGasToken(uint32 l3GasLimit) internal view virtual returns (uint256); } From 0de93033f590bd7886fae198bf05dc4b36e9732e Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 19 Sep 2024 12:24:49 -0500 Subject: [PATCH 05/45] refactor and rename Signed-off-by: bennett --- .../chain-adapters/ArbitrumForwarderBase.sol | 150 +++++++++ contracts/chain-adapters/Arbitrum_Adapter.sol | 125 +------- .../Arbitrum_CustomGasToken_Adapter.sol | 106 +------ .../interfaces/ArbitrumForwarderInterface.sol | 285 ------------------ .../interfaces/ArbitrumBridgeInterfaces.sol | 185 ++++++++++++ 5 files changed, 337 insertions(+), 514 deletions(-) create mode 100644 contracts/chain-adapters/ArbitrumForwarderBase.sol delete mode 100644 contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol create mode 100644 contracts/interfaces/ArbitrumBridgeInterfaces.sol diff --git a/contracts/chain-adapters/ArbitrumForwarderBase.sol b/contracts/chain-adapters/ArbitrumForwarderBase.sol new file mode 100644 index 00000000..1ecc4388 --- /dev/null +++ b/contracts/chain-adapters/ArbitrumForwarderBase.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { ArbitrumERC20Bridge, ArbitrumInboxLike, ArbitrumERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; + +// solhint-disable-next-line contract-name-camelcase +abstract contract ArbitrumForwarderBase { + // Amount of gas token allocated to pay for the base submission fee. The base submission fee is a parameter unique to + // retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their + // ticket’s calldata in the retry buffer. (current base submission fee is queryable via + // ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address + // 0x000000000000000000000000000000000000006E. + // @dev This is immutable because we don't know what precision the custom gas token has. + uint256 public immutable L3_MAX_SUBMISSION_COST; + + // L3 Gas price bid for immediate L3 execution attempt (queryable via standard eth*gasPrice RPC) + uint256 public immutable L3_GAS_PRICE; // The standard is 5 gWei + + // Native token expected to be sent in L3 message. Should be 0 for all use cases of this constant, which + // includes sending messages from L2 to L3 and sending Custom gas token ERC20's, which won't be the native token + // on the L3 by definition. + uint256 public constant L3_CALL_VALUE = 0; + + // Gas limit for L3 execution of a cross chain token transfer sent via the inbox. + uint32 public constant RELAY_TOKENS_L3_GAS_LIMIT = 300_000; + // Gas limit for L3 execution of a message sent via the inbox. + uint32 public constant RELAY_MESSAGE_L3_GAS_LIMIT = 2_000_000; + + // This address on L3 receives extra gas token that is left over after relaying a message via the inbox. + address public immutable L3_REFUND_L3_ADDRESS; + + // This is the address which receives messages and tokens on L3, assumed to be the spoke pool. + address public immutable L3_SPOKE_POOL; + + // This is the address which has permission to relay root bundles/messages to the L3 spoke pool. + address public immutable CROSS_DOMAIN_ADMIN; + + // Inbox system contract to send messages to Arbitrum-like L3s. Token bridges use this to send tokens to L3. + // https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol + ArbitrumInboxLike public immutable L2_INBOX; + + // Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this + // contract calls the Inbox. + // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol + // Gateway used for communicating with chains that use custom gas tokens: + // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol + ArbitrumERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; + + event TokensForwarded(address indexed l2Token, uint256 amount); + event MessageForwarded(address indexed target, bytes message); + + error RescueFailed(); + + /* + * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have + * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. + */ + modifier onlyAdmin() { + _requireAdminSender(); + _; + } + + /** + * @notice Constructs new Adapter. + * @param _l2ArbitrumInbox Inbox helper contract to send messages to Arbitrum-like L3s. + * @param _l2ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum-like L3s. + * @param _l3RefundL3Address L3 address to receive gas refunds on after a message is relayed. + * @param _l3MaxSubmissionCost Amount of gas token allocated to pay for the base submission fee. The base + * submission fee is a parameter unique to Arbitrum retryable transactions. This value is hardcoded + * and used for all messages sent by this adapter. + * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily + * stored in this contract on L2. + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. + * In practice, this is the hub pool. + */ + constructor( + ArbitrumInboxLike _l2ArbitrumInbox, + ArbitrumERC20GatewayLike _l2ERC20GatewayRouter, + address _l3RefundL3Address, + uint256 _l3MaxSubmissionCost, + uint256 _l3GasPrice, + address _l3SpokePool, + address _crossDomainAdmin + ) { + L2_INBOX = _l2ArbitrumInbox; + L2_ERC20_GATEWAY_ROUTER = _l2ERC20GatewayRouter; + L3_REFUND_L3_ADDRESS = _l3RefundL3Address; + L3_MAX_SUBMISSION_COST = _l3MaxSubmissionCost; + L3_GAS_PRICE = _l3GasPrice; + L3_SPOKE_POOL = _l3SpokePool; + CROSS_DOMAIN_ADMIN = _crossDomainAdmin; + } + + // Added so that this function may receive ETH in the event of stuck transactions. + receive() external payable {} + + /** + * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function + * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward + * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder + */ + fallback() external payable onlyAdmin { + _relayMessage(L3_SPOKE_POOL, msg.data); + } + + /** + * @notice This function can only be called via a rescue adapter. It is used to recover potentially stuck + * funds on this contract. + */ + function adminCall( + address target, + uint256 value, + bytes memory message + ) external onlyAdmin { + (bool success, ) = target.call{ value: value }(message); + if (!success) revert RescueFailed(); + } + + /** + * @notice Bridge tokens to an Arbitrum-like L3. + * @notice This contract must hold at least getL2CallValue() amount of ETH or custom gas token + * to send a message via the Inbox successfully, or the message will get stuck. + * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. + * @param l2Token L2 token to deposit. + * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. + */ + function relayTokens(address l2Token, uint256 amount) external payable virtual; + + /** + * @notice Relay a message to a contract on L2. Implementation changes on whether the + * target bridge supports a custom gas token or not. + * @notice This contract must hold at least getL2CallValue() amount of the custom gas token + * to send a message via the Inbox successfully, or the message will get stuck. + * @notice This function should be implmented differently based on whether the L2-L3 bridge + * requires custom gas tokens to fund cross-chain transactions. + */ + function _relayMessage(address target, bytes memory message) internal virtual; + + // Function to be overridden to accomodate for each L2's unique method of address aliasing. + function _requireAdminSender() internal virtual; + + /** + * @notice Returns required amount of gas token to send a message via the Inbox. + * @param l3GasLimit L3 gas limit for the message. + * @return amount of gas token that this contract needs to hold in order for relayMessage to succeed. + */ + function getL2CallValue(uint32 l3GasLimit) public view returns (uint256) { + return L3_MAX_SUBMISSION_COST + L3_GAS_PRICE * l3GasLimit; + } +} diff --git a/contracts/chain-adapters/Arbitrum_Adapter.sol b/contracts/chain-adapters/Arbitrum_Adapter.sol index 3cfd0f32..8c58b911 100644 --- a/contracts/chain-adapters/Arbitrum_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_Adapter.sol @@ -7,130 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../external/interfaces/CCTPInterfaces.sol"; import "../libraries/CircleCCTPAdapter.sol"; - -/** - * @notice Interface for Arbitrum's L1 Inbox contract used to send messages to Arbitrum. - * @custom:security-contact bugs@across.to - */ -interface ArbitrumL1InboxLike { - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. - * all msg.value will deposited to callValueRefundAddress on L2 - * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed - * funds come from the deposit alone, rather than falling back on the user's L2 balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function unsafeCreateRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); -} - -/** - * @notice Layer 1 Gateway contract for bridging standard ERC20s to Arbitrum. - */ -interface ArbitrumL1ERC20GatewayLike { - /** - * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges - * like the DAI bridge. - * @dev Refunded to aliased L2 address of sender if sender has code on L1, otherwise to to sender's EOA on L2. - * @param _l1Token L1 address of ERC20 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum. - * @dev L2 address alias will not be applied to the following types of addresses on L1: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _l1Token L1 address of ERC20 - * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _l1Token, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); -} +import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumERC20GatewayLike as ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Contract containing logic to send messages from L1 to Arbitrum. diff --git a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol index b4c292b6..f55c1f66 100644 --- a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol @@ -7,6 +7,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ITokenMessenger as ICCTPTokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; +import { ArbitrumERC20Bridge as ArbitrumL1ERC20Bridge, ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumERC20GatewayLike as ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Interface for funder contract that this contract pulls from to pay for relayMessage()/relayTokens() @@ -23,111 +24,6 @@ interface FunderInterface { function withdraw(IERC20 token, uint256 amount) external; } -/** - * @title Staging ground for incoming and outgoing messages - * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is - * used as native currency on L2. - * @dev Fees are paid in this token. There are certain restrictions on the native token: - * - The token can't be rebasing or have a transfer fee - * - The token must only be transferrable via a call to the token address itself - * - The token must only be able to set allowance via a call to the token address itself - * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert - * - The token must have a max of 2^256 - 1 wei total supply unscaled - * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals - */ -interface ArbitrumL1ERC20Bridge { - /** - * @notice Returns token that is escrowed in bridge on L1 side and minted on L2 as native currency. - * @dev This function doesn't exist on the generic Bridge interface. - * @return address of the native token. - */ - function nativeToken() external view returns (address); -} - -/** - * @title Inbox for user and contract originated messages - * @notice Messages created via this inbox are enqueued in the delayed accumulator - * to await inclusion in the SequencerInbox - */ -interface ArbitrumL1InboxLike { - /** - * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface - * to return an ArbitrumL1ERC20Bridge instead of a more generic Bridge interface. - * @return address of the bridge. - */ - function bridge() external view returns (ArbitrumL1ERC20Bridge); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying - * for L1 to L2 message using a custom gas token. - * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on L2 - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - l2CallValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on L2. - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on L2. - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 tokenTotalFeeAmount, - bytes calldata data - ) external returns (uint256); -} - -/** - * @notice Layer 1 Gateway contract for bridging standard ERC20s to Arbitrum. - */ -interface ArbitrumL1ERC20GatewayLike { - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum. - * @dev L2 address alias will not be applied to the following types of addresses on L1: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _l1Token L1 address of ERC20 - * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _l1Token, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); -} - /** * @notice Contract containing logic to send messages from L1 to Arbitrum. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be diff --git a/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol b/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol deleted file mode 100644 index cf6040c7..00000000 --- a/contracts/chain-adapters/interfaces/ArbitrumForwarderInterface.sol +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title Staging ground for incoming and outgoing messages - * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is - * used as native currency on L3. - * @dev Fees are paid in this token. There are certain restrictions on the native token: - * - The token can't be rebasing or have a transfer fee - * - The token must only be transferrable via a call to the token address itself - * - The token must only be able to set allowance via a call to the token address itself - * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert - * - The token must have a max of 2^256 - 1 wei total supply unscaled - * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals - */ -interface ArbitrumERC20Bridge { - /** - * @notice Returns token that is escrowed in bridge on L2 side and minted on L3 as native currency. - * @dev This function doesn't exist on the generic Bridge interface. - * @return address of the native token. - */ - function nativeToken() external view returns (address); -} - -/** - * @title Inbox for user and contract originated messages - * @notice Messages created via this inbox are enqueued in the delayed accumulator - * to await inclusion in the SequencerInbox - */ -interface ArbitrumInboxLike { - /** - * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface - * to return an ArbitrumERC20Bridge instead of a more generic Bridge interface. - * @return address of the bridge. - */ - function bridge() external view returns (ArbitrumERC20Bridge); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. - * all msg.value will deposited to callValueRefundAddress on L3 - * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging - * @param to destination L3 contract address - * @param l3CallValue call value for retryable L3 message - * @param maxSubmissionCost Max gas deducted from user's L3 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L3 balance - * @param callValueRefundAddress l3Callvalue gets credited here on L3 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L3 balance to cover L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L3 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l3CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying - * for L2 to L3 message using a custom gas token. - * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on L3 - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - l3CallValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. - * @param to destination L3 contract address - * @param l3CallValue call value for retryable L3 message - * @param maxSubmissionCost Max gas deducted from user's L3 balance to cover base submission fee - * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on L3. - * @param callValueRefundAddress l3Callvalue gets credited here on L3 if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on L3. - * @param gasLimit Max gas deducted from user's L3 balance to cover L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L3 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost - * @param data ABI encoded data of L3 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l3CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 tokenTotalFeeAmount, - bytes calldata data - ) external returns (uint256); -} - -/** - * @notice Generic gateway contract for bridging standard ERC20s to Arbitrum-like networks. - */ -interface ArbitrumERC20GatewayLike { - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. - * @dev L3 address alias will not be applied to the following types of addresses on L2: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _l2Token L2 address of ERC20 - * @param _refundTo Account, or its L3 alias if it have code in L2, to be credited with excess gas refund in L3 - * @param _to Account to be credited with the tokens in the L3 (can be the user's L3 account or a contract), - * not subject to L3 aliasing. This account, or its L3 alias if it have code in L2, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L3 balance to cover L3 execution - * @param _gasPriceBid Gas price for L3 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _l2Token, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); -} - -/** - * @notice Contract containing logic to send messages from L2 to Arbitrum-like L3s. - * @dev This contract is meant to share code for Arbitrum L2 forwarder contracts deployed to various - * different L2 architectures (e.g. Base, Arbitrum, ZkSync, etc.). It assumes that the L3 conforms - * to an Arbitrum-like interface. - */ - -// solhint-disable-next-line contract-name-camelcase -abstract contract ArbitrumForwarderInterface { - // Amount of gas token allocated to pay for the base submission fee. The base submission fee is a parameter unique to - // retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their - // ticket’s calldata in the retry buffer. (current base submission fee is queryable via - // ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address - // 0x000000000000000000000000000000000000006E. - // @dev This is immutable because we don't know what precision the custom gas token has. - uint256 public immutable L3_MAX_SUBMISSION_COST; - - // L3 Gas price bid for immediate L3 execution attempt (queryable via standard eth*gasPrice RPC) - uint256 public constant L3_GAS_PRICE = 5e9; // 5 gWei - - // Native token expected to be sent in L3 message. Should be 0 for all use cases of this constant, which - // includes sending messages from L2 to L3 and sending Custom gas token ERC20's, which won't be the native token - // on the L3 by definition. - uint256 public constant L3_CALL_VALUE = 0; - - // Gas limit for L3 execution of a cross chain token transfer sent via the inbox. - uint32 public constant RELAY_TOKENS_L3_GAS_LIMIT = 300_000; - // Gas limit for L3 execution of a message sent via the inbox. - uint32 public constant RELAY_MESSAGE_L3_GAS_LIMIT = 2_000_000; - - // This address on L3 receives extra gas token that is left over after relaying a message via the inbox. - address public immutable L3_REFUND_L3_ADDRESS; - - // This is the address which receives messages and tokens on L3, assumed to be the spoke pool. - address public immutable L3_SPOKE_POOL; - - // This is the address which has permission to relay root bundles/messages to the L3 spoke pool. - address public immutable CROSS_DOMAIN_ADMIN; - - // Inbox system contract to send messages to Arbitrum-like L3s. Token bridges use this to send tokens to L3. - // https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol - ArbitrumInboxLike public immutable L2_INBOX; - - // Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this - // contract calls the Inbox. - // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol - // Gateway used for communicating with chains that use custom gas tokens: - // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol - ArbitrumERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; - - event TokensForwarded(address indexed l2Token, uint256 amount); - event MessageForwarded(address indexed target, bytes message); - - error RescueFailed(); - - /* - * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have - * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. - */ - modifier onlyAdmin() { - _requireAdminSender(); - _; - } - - /** - * @notice Constructs new Adapter. - * @param _l2ArbitrumInbox Inbox helper contract to send messages to Arbitrum-like L3s. - * @param _l2ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum-like L3s. - * @param _l3RefundL3Address L3 address to receive gas refunds on after a message is relayed. - * @param _l3MaxSubmissionCost Amount of gas token allocated to pay for the base submission fee. The base - * submission fee is a parameter unique to Arbitrum retryable transactions. This value is hardcoded - * and used for all messages sent by this adapter. - * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily - * stored in this contract on L2. - * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. - * In practice, this is the hub pool. - */ - constructor( - ArbitrumInboxLike _l2ArbitrumInbox, - ArbitrumERC20GatewayLike _l2ERC20GatewayRouter, - address _l3RefundL3Address, - uint256 _l3MaxSubmissionCost, - address _l3SpokePool, - address _crossDomainAdmin - ) { - L2_INBOX = _l2ArbitrumInbox; - L2_ERC20_GATEWAY_ROUTER = _l2ERC20GatewayRouter; - L3_REFUND_L3_ADDRESS = _l3RefundL3Address; - L3_MAX_SUBMISSION_COST = _l3MaxSubmissionCost; - L3_SPOKE_POOL = _l3SpokePool; - CROSS_DOMAIN_ADMIN = _crossDomainAdmin; - } - - // Added so that this function may receive ETH in the event of stuck transactions. - receive() external payable {} - - /** - * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function - * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward - * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder - */ - fallback() external payable onlyAdmin { - _relayMessage(L3_SPOKE_POOL, msg.data); - } - - /** - * @notice This function can only be called via a rescue adapter, and is used to recover potentially stuck - * funds on this contract. - */ - function rescue( - address target, - uint256 value, - bytes memory message - ) external onlyAdmin { - (bool success, ) = target.call{ value: value }(message); - if (!success) revert RescueFailed(); - } - - /** - * @notice Bridge tokens to an Arbitrum-like L3. - * @notice This contract must hold at least getL2CallValue() amount of ETH or custom gas token - * to send a message via the Inbox successfully, or the message will get stuck. - * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. - * @param l2Token L2 token to deposit. - * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. - */ - function relayTokens(address l2Token, uint256 amount) external payable virtual; - - /** - * @notice Relay a message to a contract on L2. Implementation changes on whether the - * target bridge supports a custom gas token or not. - * @notice This contract must hold at least getL2CallValue() amount of the custom gas token - * to send a message via the Inbox successfully, or the message will get stuck. - * @notice This function should be implmented differently based on whether the L2-L3 bridge - * requires custom gas tokens to fund cross-chain transactions. - */ - function _relayMessage(address target, bytes memory message) internal virtual; - - // Function to be overridden to accomodate for each L2's unique method of address aliasing. - function _requireAdminSender() internal virtual; - - /** - * @notice Returns required amount of gas token to send a message via the Inbox. - * @param l3GasLimit L3 gas limit for the message. - * @return amount of gas token that this contract needs to hold in order for relayMessage to succeed. - */ - function getL2CallValue(uint32 l3GasLimit) public view returns (uint256) { - return L3_MAX_SUBMISSION_COST + L3_GAS_PRICE * l3GasLimit; - } -} diff --git a/contracts/interfaces/ArbitrumBridgeInterfaces.sol b/contracts/interfaces/ArbitrumBridgeInterfaces.sol new file mode 100644 index 00000000..ea7f8189 --- /dev/null +++ b/contracts/interfaces/ArbitrumBridgeInterfaces.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Staging ground for incoming and outgoing messages + * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is + * used as native currency on upper layer. + * @dev Fees are paid in this token. There are certain restrictions on the native token: + * - The token can't be rebasing or have a transfer fee + * - The token must only be transferrable via a call to the token address itself + * - The token must only be able to set allowance via a call to the token address itself + * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert + * - The token must have a max of 2^256 - 1 wei total supply unscaled + * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals + */ +interface ArbitrumERC20Bridge { + /** + * @notice Returns token that is escrowed in bridge on the lower layer and minted on the upper layer as native currency. + * @dev This function doesn't exist on the generic Bridge interface. + * @return address of the native token. + */ + function nativeToken() external view returns (address); +} + +/** + * @title Inbox for user and contract originated messages + * @notice Messages created via this inbox are enqueued in the delayed accumulator + * to await inclusion in the SequencerInbox + */ +interface ArbitrumInboxLike { + /** + * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface + * to return an ArbitrumERC20Bridge instead of a more generic Bridge interface. + * @return address of the bridge. + */ + function bridge() external view returns (ArbitrumERC20Bridge); + + /** + * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. + * all msg.value will deposited to callValueRefundAddress on the upper layer + * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's (upper layer) balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on (upper layer) balance + * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's upper layer balance to cover upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + + /** + * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts + * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying + * for message using a custom gas token. + * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on upper layer + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - callValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's upper layer balance to cover base submission fee + * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on upper layer. + * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on upper layer. + * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost + * @param data ABI encoded data of message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256); + + /** + * @notice Put a message in the source chain inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed + * funds come from the deposit alone, rather than falling back on the user's balance + * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). + * createRetryableTicket method is the recommended standard. + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's source chain balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on source chain balance + * @param callValueRefundAddress callvalue gets credited here on source chain if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of the message + * @return unique message number of the retryable transaction + */ + function unsafeCreateRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); +} + +/** + * @notice Generic gateway contract for bridging standard ERC20s to Arbitrum-like networks. + */ +interface ArbitrumERC20GatewayLike { + /** + * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. + * @dev Upper layer address alias will not be applied to the following types of addresses on lower layer: + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * @param _sourceToken address of ERC20 on source chain. + * @param _refundTo Account, or its alias if it has code on the source chain, to be credited with excess gas refund at destination + * @param _to Account to be credited with the tokens in the L3 (can be the user's L3 account or a contract), + * not subject to aliasing. This account, or its alias if it has code on the source chain, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's balance to cover execution + * @param _gasPriceBid Gas price for execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransferCustomRefund( + address _sourceToken, + address _refundTo, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + /** + * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges + * like the DAI bridge. + * @dev Refunded to aliased address of sender if sender has code on source chain, otherwise to to sender's EOA on destination chain. + * @param _sourceToken address of ERC20 + * @param _to Account to be credited with the tokens at the destination (can be the user's account or a contract), + * not subject to aliasing. This account, or its alias if it has code in the source chain, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's balance to cover execution + * @param _gasPriceBid Gas price for execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransfer( + address _sourceToken, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + /** + * @notice get ERC20 gateway for token. + * @param _token ERC20 address. + * @return address of ERC20 gateway. + */ + function getGateway(address _token) external view returns (address); +} From fd94012e02e143448ff0e5005f3b66a34e04d1bc Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 23 Sep 2024 10:40:39 -0500 Subject: [PATCH 06/45] clean up interfaces Signed-off-by: bennett --- contracts/Arbitrum_SpokePool.sol | 12 +- .../chain-adapters/ArbitrumForwarderBase.sol | 6 +- contracts/chain-adapters/Arbitrum_Adapter.sol | 2 +- .../Arbitrum_CustomGasToken_Adapter.sol | 2 +- .../interfaces/ArbitrumBridgeInterfaces.sol | 124 +++++++++++------- 5 files changed, 83 insertions(+), 63 deletions(-) diff --git a/contracts/Arbitrum_SpokePool.sol b/contracts/Arbitrum_SpokePool.sol index ecdd5bab..7620b855 100644 --- a/contracts/Arbitrum_SpokePool.sol +++ b/contracts/Arbitrum_SpokePool.sol @@ -6,15 +6,7 @@ pragma solidity ^0.8.19; import "./SpokePool.sol"; import "./libraries/CircleCCTPAdapter.sol"; - -interface StandardBridgeLike { - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - bytes calldata _data - ) external payable returns (bytes memory); -} +import { ArbitrumL2ERC20GatewayLike } from "./interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice AVM specific SpokePool. Uses AVM cross-domain-enabled logic to implement admin only access to functions. @@ -100,7 +92,7 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter { address ethereumTokenToBridge = whitelistedTokens[l2TokenAddress]; require(ethereumTokenToBridge != address(0), "Uninitialized mainnet token"); //slither-disable-next-line unused-return - StandardBridgeLike(l2GatewayRouter).outboundTransfer( + ArbitrumL2ERC20GatewayLike(l2GatewayRouter).outboundTransfer( ethereumTokenToBridge, // _l1Token. Address of the L1 token to bridge over. hubPool, // _to. Withdraw, over the bridge, to the l1 hub pool contract. amountToReturn, // _amount. diff --git a/contracts/chain-adapters/ArbitrumForwarderBase.sol b/contracts/chain-adapters/ArbitrumForwarderBase.sol index 1ecc4388..3832519d 100644 --- a/contracts/chain-adapters/ArbitrumForwarderBase.sol +++ b/contracts/chain-adapters/ArbitrumForwarderBase.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { ArbitrumERC20Bridge, ArbitrumInboxLike, ArbitrumERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; +import { ArbitrumERC20Bridge, ArbitrumInboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; // solhint-disable-next-line contract-name-camelcase abstract contract ArbitrumForwarderBase { @@ -44,7 +44,7 @@ abstract contract ArbitrumForwarderBase { // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol // Gateway used for communicating with chains that use custom gas tokens: // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol - ArbitrumERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; + ArbitrumL1ERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; event TokensForwarded(address indexed l2Token, uint256 amount); event MessageForwarded(address indexed target, bytes message); @@ -75,7 +75,7 @@ abstract contract ArbitrumForwarderBase { */ constructor( ArbitrumInboxLike _l2ArbitrumInbox, - ArbitrumERC20GatewayLike _l2ERC20GatewayRouter, + ArbitrumL1ERC20GatewayLike _l2ERC20GatewayRouter, address _l3RefundL3Address, uint256 _l3MaxSubmissionCost, uint256 _l3GasPrice, diff --git a/contracts/chain-adapters/Arbitrum_Adapter.sol b/contracts/chain-adapters/Arbitrum_Adapter.sol index 8c58b911..f8ec507e 100644 --- a/contracts/chain-adapters/Arbitrum_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_Adapter.sol @@ -7,7 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../external/interfaces/CCTPInterfaces.sol"; import "../libraries/CircleCCTPAdapter.sol"; -import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumERC20GatewayLike as ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; +import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Contract containing logic to send messages from L1 to Arbitrum. diff --git a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol index 2cdddd34..203ee227 100644 --- a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol @@ -7,7 +7,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ITokenMessenger as ICCTPTokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; -import { ArbitrumERC20Bridge as ArbitrumL1ERC20Bridge, ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumERC20GatewayLike as ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; +import { ArbitrumERC20Bridge as ArbitrumL1ERC20Bridge, ArbitrumCustomGasTokenInbox as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Interface for funder contract that this contract pulls from to pay for relayMessage()/relayTokens() diff --git a/contracts/interfaces/ArbitrumBridgeInterfaces.sol b/contracts/interfaces/ArbitrumBridgeInterfaces.sol index 31ce1197..a2b31724 100644 --- a/contracts/interfaces/ArbitrumBridgeInterfaces.sol +++ b/contracts/interfaces/ArbitrumBridgeInterfaces.sol @@ -70,24 +70,23 @@ interface ArbitrumInboxLike { ) external payable returns (uint256); /** - * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts - * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying - * for message using a custom gas token. - * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on upper layer + * @notice Put a message in the source chain inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed + * funds come from the deposit alone, rather than falling back on the user's balance + * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). + * createRetryableTicket method is the recommended standard. * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - callValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. * @param to destination contract address * @param callValue call value for retryable message - * @param maxSubmissionCost Max gas deducted from user's upper layer balance to cover base submission fee - * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on upper layer. - * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on upper layer. + * @param maxSubmissionCost Max gas deducted from user's source chain balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on source chain balance + * @param callValueRefundAddress callvalue gets credited here on source chain if retryable txn times out or gets cancelled * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost - * @param data ABI encoded data of message + * @param data ABI encoded data of the message * @return unique message number of the retryable transaction */ - function createRetryableTicket( + function unsafeCreateRetryableTicket( address to, uint256 callValue, uint256 maxSubmissionCost, @@ -95,28 +94,33 @@ interface ArbitrumInboxLike { address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, - uint256 tokenTotalFeeAmount, bytes calldata data - ) external returns (uint256); + ) external payable returns (uint256); +} +/** + * @notice Interface which extends ArbitrumInboxLike with functions used to interact with bridges that use a custom gas token. + */ +interface ArbitrumCustomGasTokenInbox is ArbitrumInboxLike { /** - * @notice Put a message in the source chain inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed - * funds come from the deposit alone, rather than falling back on the user's balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. + * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts + * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying + * for message using a custom gas token. + * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on upper layer * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - callValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. * @param to destination contract address * @param callValue call value for retryable message - * @param maxSubmissionCost Max gas deducted from user's source chain balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on source chain balance - * @param callValueRefundAddress callvalue gets credited here on source chain if retryable txn times out or gets cancelled + * @param maxSubmissionCost Max gas deducted from user's upper layer balance to cover base submission fee + * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on upper layer. + * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on upper layer. * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of the message + * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost + * @param data ABI encoded data of message * @return unique message number of the retryable transaction */ - function unsafeCreateRetryableTicket( + function createRetryableTicket( address to, uint256 callValue, uint256 maxSubmissionCost, @@ -124,14 +128,46 @@ interface ArbitrumInboxLike { address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, bytes calldata data - ) external payable returns (uint256); + ) external returns (uint256); } /** - * @notice Generic gateway contract for bridging standard ERC20s to Arbitrum-like networks. + * @notice Generic gateway contract for bridging standard ERC20s to/from Arbitrum-like networks. + * @notice These function signatures are shared between the L1 and L2 gateway router contracts. */ -interface ArbitrumERC20GatewayLike { +interface ArbitrumL1ERC20GatewayLike { + /** + * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges + * like the DAI bridge. + * @dev Refunded to aliased address of sender if sender has code on source chain, otherwise to to sender's EOA on destination chain. + * @param _sourceToken address of ERC20 + * @param _to Account to be credited with the tokens at the destination (can be the user's account or a contract), + * not subject to aliasing. This account, or its alias if it has code in the source chain, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's balance to cover execution + * @param _gasPriceBid Gas price for execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransfer( + address _sourceToken, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + /** + * @notice get ERC20 gateway for token. + * @param _token ERC20 address. + * @return address of ERC20 gateway. + */ + function getGateway(address _token) external view returns (address); + /** * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. * @dev Upper layer address alias will not be applied to the following types of addresses on lower layer: @@ -159,34 +195,26 @@ interface ArbitrumERC20GatewayLike { uint256 _gasPriceBid, bytes calldata _data ) external payable returns (bytes memory); +} +interface ArbitrumL2ERC20GatewayLike { /** - * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges - * like the DAI bridge. - * @dev Refunded to aliased address of sender if sender has code on source chain, otherwise to to sender's EOA on destination chain. - * @param _sourceToken address of ERC20 - * @param _to Account to be credited with the tokens at the destination (can be the user's account or a contract), - * not subject to aliasing. This account, or its alias if it has code in the source chain, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's balance to cover execution - * @param _gasPriceBid Gas price for execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number + * @notice Fetches the l2 token address from the gateway router for the input l1 token address + * @param _l1Erc20 address of the l1 token. + */ + function calculateL2TokenAddress(address _l1Erc20) external view returns (address); + + /** + * @notice Withdraws a specified amount of an l2 token to an l1 token. + * @param _l1Token address of the token to withdraw on L1. + * @param _to address on L1 which will receive the tokens upon withdrawal. + * @param _amount amount of the token to withdraw. + * @param _data encoded data to send to the gateway router. */ function outboundTransfer( - address _sourceToken, + address _l1Token, address _to, uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, bytes calldata _data ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); } From 93d40cfdf0ca9364b3a11d3210ae35035e368bef Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 23 Sep 2024 14:48:01 -0500 Subject: [PATCH 07/45] add test Signed-off-by: bennett --- .../evm/foundry/local/Arbitrum_L3_Adapter.sol | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/evm/foundry/local/Arbitrum_L3_Adapter.sol diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.sol b/test/evm/foundry/local/Arbitrum_L3_Adapter.sol new file mode 100644 index 00000000..863e6323 --- /dev/null +++ b/test/evm/foundry/local/Arbitrum_L3_Adapter.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { MockERC20 } from "forge-std/mocks/MockERC20.sol"; +import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; +import { Arbitrum_L3_Adapter } from "../../../../contracts/chain-adapters/Arbitrum_L3_Adapter.sol"; +import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol"; +import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; +import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; + +// We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter +// directly, so in order to withdraw Weth, we need to have receive(). +contract Mock_L3_Adapter is Arbitrum_L3_Adapter { + constructor(address _adapter, address _l2Forwarder) Arbitrum_L3_Adapter(_adapter, _l2Forwarder) {} + + receive() external payable {} +} + +contract Token_ERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function mint(address to, uint256 value) public virtual { + _mint(to, value); + } + + function burn(address from, uint256 value) public virtual { + _burn(from, value); + } +} + +contract MinimalWeth is Token_ERC20 { + constructor(string memory name, string memory symbol) Token_ERC20(name, symbol) {} + + function withdraw(uint256 amount) public { + _burn(msg.sender, amount); + (bool success, ) = payable(msg.sender).call{ value: amount }(""); + require(success); + } +} + +contract CrossDomainMessenger { + event MessageSent(address indexed target); + + function sendMessage( + address target, + bytes calldata, + uint32 + ) external { + emit MessageSent(target); + } +} + +contract StandardBridge { + event ETHDepositInitiated(address indexed to, uint256 amount); + + function depositERC20To( + address l1Token, + address l2Token, + address to, + uint256 amount, + uint32, + bytes calldata + ) external { + Token_ERC20(l1Token).burn(msg.sender, amount); + Token_ERC20(l2Token).mint(to, amount); + } + + function depositETHTo( + address to, + uint32, + bytes calldata + ) external payable { + emit ETHDepositInitiated(to, msg.value); + } +} + +contract ArbitrumL3AdapterTest is Test { + Arbitrum_L3_Adapter l3Adapter; + Optimism_Adapter optimismAdapter; + + Token_ERC20 l1Token; + Token_ERC20 l2Token; + Token_ERC20 l1Weth; + Token_ERC20 l2Weth; + CrossDomainMessenger crossDomainMessenger; + StandardBridge standardBridge; + + address l2Forwarder; + + function setUp() public { + l2Forwarder = vm.addr(1); + + l1Token = new Token_ERC20("l1Token", "l1Token"); + l2Token = new Token_ERC20("l2Token", "l2Token"); + l1Weth = new MinimalWeth("l1Weth", "l1Weth"); + l2Weth = new MinimalWeth("l2Weth", "l2Weth"); + + crossDomainMessenger = new CrossDomainMessenger(); + standardBridge = new StandardBridge(); + + optimismAdapter = new Optimism_Adapter( + WETH9Interface(address(l1Weth)), + address(crossDomainMessenger), + IL1StandardBridge(address(standardBridge)), + IERC20(address(0)), + ITokenMessenger(address(0)) + ); + l3Adapter = new Mock_L3_Adapter(address(optimismAdapter), l2Forwarder); + } + + // Messages should be indiscriminately sent to the l2Forwarder. + function testRelayMessage(address target, bytes memory message) public { + vm.expectEmit(address(crossDomainMessenger)); + emit CrossDomainMessenger.MessageSent(l2Forwarder); + l3Adapter.relayMessage(target, message); + } + + // Sending Weth should call depositETHTo(). + function testRelayWeth(uint256 amountToSend, address random) public { + vm.deal(address(l1Weth), amountToSend); + l1Weth.mint(address(l3Adapter), amountToSend); + assertEq(amountToSend, l1Weth.totalSupply()); + vm.expectEmit(address(standardBridge)); + emit StandardBridge.ETHDepositInitiated(l2Forwarder, amountToSend); + l3Adapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); + assertEq(0, l1Weth.totalSupply()); + } + + // Sending any random token should call depositERC20To(). + function testRelayToken(uint256 amountToSend, address random) public { + l1Token.mint(address(l3Adapter), amountToSend); + assertEq(amountToSend, l1Token.totalSupply()); + l3Adapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); + assertEq(amountToSend, l2Token.balanceOf(l2Forwarder)); + assertEq(amountToSend, l2Token.totalSupply()); + assertEq(0, l1Token.totalSupply()); + } +} From 00aeb40f006244bff94733b3181b34fbb36bcbc4 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 23 Sep 2024 14:49:10 -0500 Subject: [PATCH 08/45] rename test Signed-off-by: bennett --- .../local/{Arbitrum_L3_Adapter.sol => Arbitrum_L3_Adapter.t.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/evm/foundry/local/{Arbitrum_L3_Adapter.sol => Arbitrum_L3_Adapter.t.sol} (100%) diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.sol b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol similarity index 100% rename from test/evm/foundry/local/Arbitrum_L3_Adapter.sol rename to test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol From f2e3c41f3a0f78ba94d2ee2dc749a782e54512e0 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 23 Sep 2024 15:33:00 -0500 Subject: [PATCH 09/45] simplify forwarder base and make it a proxy Signed-off-by: bennett --- .../chain-adapters/ArbitrumForwarderBase.sol | 150 ------------------ contracts/chain-adapters/ForwarderBase.sol | 110 +++++++++++++ 2 files changed, 110 insertions(+), 150 deletions(-) delete mode 100644 contracts/chain-adapters/ArbitrumForwarderBase.sol create mode 100644 contracts/chain-adapters/ForwarderBase.sol diff --git a/contracts/chain-adapters/ArbitrumForwarderBase.sol b/contracts/chain-adapters/ArbitrumForwarderBase.sol deleted file mode 100644 index 3832519d..00000000 --- a/contracts/chain-adapters/ArbitrumForwarderBase.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { ArbitrumERC20Bridge, ArbitrumInboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; - -// solhint-disable-next-line contract-name-camelcase -abstract contract ArbitrumForwarderBase { - // Amount of gas token allocated to pay for the base submission fee. The base submission fee is a parameter unique to - // retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their - // ticket’s calldata in the retry buffer. (current base submission fee is queryable via - // ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address - // 0x000000000000000000000000000000000000006E. - // @dev This is immutable because we don't know what precision the custom gas token has. - uint256 public immutable L3_MAX_SUBMISSION_COST; - - // L3 Gas price bid for immediate L3 execution attempt (queryable via standard eth*gasPrice RPC) - uint256 public immutable L3_GAS_PRICE; // The standard is 5 gWei - - // Native token expected to be sent in L3 message. Should be 0 for all use cases of this constant, which - // includes sending messages from L2 to L3 and sending Custom gas token ERC20's, which won't be the native token - // on the L3 by definition. - uint256 public constant L3_CALL_VALUE = 0; - - // Gas limit for L3 execution of a cross chain token transfer sent via the inbox. - uint32 public constant RELAY_TOKENS_L3_GAS_LIMIT = 300_000; - // Gas limit for L3 execution of a message sent via the inbox. - uint32 public constant RELAY_MESSAGE_L3_GAS_LIMIT = 2_000_000; - - // This address on L3 receives extra gas token that is left over after relaying a message via the inbox. - address public immutable L3_REFUND_L3_ADDRESS; - - // This is the address which receives messages and tokens on L3, assumed to be the spoke pool. - address public immutable L3_SPOKE_POOL; - - // This is the address which has permission to relay root bundles/messages to the L3 spoke pool. - address public immutable CROSS_DOMAIN_ADMIN; - - // Inbox system contract to send messages to Arbitrum-like L3s. Token bridges use this to send tokens to L3. - // https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol - ArbitrumInboxLike public immutable L2_INBOX; - - // Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this - // contract calls the Inbox. - // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol - // Gateway used for communicating with chains that use custom gas tokens: - // https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol - ArbitrumL1ERC20GatewayLike public immutable L2_ERC20_GATEWAY_ROUTER; - - event TokensForwarded(address indexed l2Token, uint256 amount); - event MessageForwarded(address indexed target, bytes message); - - error RescueFailed(); - - /* - * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have - * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. - */ - modifier onlyAdmin() { - _requireAdminSender(); - _; - } - - /** - * @notice Constructs new Adapter. - * @param _l2ArbitrumInbox Inbox helper contract to send messages to Arbitrum-like L3s. - * @param _l2ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum-like L3s. - * @param _l3RefundL3Address L3 address to receive gas refunds on after a message is relayed. - * @param _l3MaxSubmissionCost Amount of gas token allocated to pay for the base submission fee. The base - * submission fee is a parameter unique to Arbitrum retryable transactions. This value is hardcoded - * and used for all messages sent by this adapter. - * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily - * stored in this contract on L2. - * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. - * In practice, this is the hub pool. - */ - constructor( - ArbitrumInboxLike _l2ArbitrumInbox, - ArbitrumL1ERC20GatewayLike _l2ERC20GatewayRouter, - address _l3RefundL3Address, - uint256 _l3MaxSubmissionCost, - uint256 _l3GasPrice, - address _l3SpokePool, - address _crossDomainAdmin - ) { - L2_INBOX = _l2ArbitrumInbox; - L2_ERC20_GATEWAY_ROUTER = _l2ERC20GatewayRouter; - L3_REFUND_L3_ADDRESS = _l3RefundL3Address; - L3_MAX_SUBMISSION_COST = _l3MaxSubmissionCost; - L3_GAS_PRICE = _l3GasPrice; - L3_SPOKE_POOL = _l3SpokePool; - CROSS_DOMAIN_ADMIN = _crossDomainAdmin; - } - - // Added so that this function may receive ETH in the event of stuck transactions. - receive() external payable {} - - /** - * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function - * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward - * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder - */ - fallback() external payable onlyAdmin { - _relayMessage(L3_SPOKE_POOL, msg.data); - } - - /** - * @notice This function can only be called via a rescue adapter. It is used to recover potentially stuck - * funds on this contract. - */ - function adminCall( - address target, - uint256 value, - bytes memory message - ) external onlyAdmin { - (bool success, ) = target.call{ value: value }(message); - if (!success) revert RescueFailed(); - } - - /** - * @notice Bridge tokens to an Arbitrum-like L3. - * @notice This contract must hold at least getL2CallValue() amount of ETH or custom gas token - * to send a message via the Inbox successfully, or the message will get stuck. - * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. - * @param l2Token L2 token to deposit. - * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. - */ - function relayTokens(address l2Token, uint256 amount) external payable virtual; - - /** - * @notice Relay a message to a contract on L2. Implementation changes on whether the - * target bridge supports a custom gas token or not. - * @notice This contract must hold at least getL2CallValue() amount of the custom gas token - * to send a message via the Inbox successfully, or the message will get stuck. - * @notice This function should be implmented differently based on whether the L2-L3 bridge - * requires custom gas tokens to fund cross-chain transactions. - */ - function _relayMessage(address target, bytes memory message) internal virtual; - - // Function to be overridden to accomodate for each L2's unique method of address aliasing. - function _requireAdminSender() internal virtual; - - /** - * @notice Returns required amount of gas token to send a message via the Inbox. - * @param l3GasLimit L3 gas limit for the message. - * @return amount of gas token that this contract needs to hold in order for relayMessage to succeed. - */ - function getL2CallValue(uint32 l3GasLimit) public view returns (uint256) { - return L3_MAX_SUBMISSION_COST + L3_GAS_PRICE * l3GasLimit; - } -} diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol new file mode 100644 index 00000000..ed5f27f0 --- /dev/null +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { EIP712CrossChainUpgradeable } from "../upgradeable/EIP712CrossChainUpgradeable.sol"; + +abstract contract ForwarderBase is UUPSUpgradeable, EIP712CrossChainUpgradeable { + // L3 address of the recipient of fallback messages and tokens. + address public l3SpokePool; + + // L1 address of the contract which can relay messages to the l3SpokePool contract and update this proxy contract. + address public crossDomainAdmin; + + event TokensForwarded(address indexed l2Token, uint256 amount); + event MessageForwarded(address indexed target, bytes message); + event SetXDomainAdmin(address indexed crossDomainAdmin); + event SetL3SpokePool(address indexed l3SpokePool); + + error InvalidCrossDomainAdmin(); + error InvalidL3SpokePool(); + + /* + * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have + * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. + */ + modifier onlyAdmin() { + _requireAdminSender(); + _; + } + + /** + * @notice Initializes the forwarder contract. + * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily + * stored in this contract on L2. + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. + * In practice, this is the hub pool. + */ + function __Forwarder_init(address _l3SpokePool, address _crossDomainAdmin) public onlyInitializing { + __UUPSUpgradeable_init(); + __EIP712_init("ACROSS-V2", "1.0.0"); + _setL3SpokePool(_l3SpokePool); + _setCrossDomainAdmin(_crossDomainAdmin); + } + + // Added so that this function may receive ETH in the event of stuck transactions. + receive() external payable {} + + /** + * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function + * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward + * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder + */ + fallback() external payable onlyAdmin { + _relayL3Message(l3SpokePool, msg.data); + } + + /** + * @notice Sets a new cross domain admin for this contract. + * @param _newCrossDomainAdmin L1 address of the new cross domain admin. + */ + function setCrossDomainAdmin(address _newCrossDomainAdmin) external onlyAdmin { + _setCrossDomainAdmin(_newCrossDomainAdmin); + } + + /** + * @notice Sets a new spoke pool address. + * @param _newL3SpokePool L3 address of the new spoke pool contract. + */ + function setL3SpokePool(address _newL3SpokePool) external onlyAdmin { + _setL3SpokePool(_newL3SpokePool); + } + + /** + * @notice Bridge tokens to an L3. + * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. + * @param l2Token L2 token to deposit. + * @param l3Token L3 token to receive on the destination chain. + * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. + */ + function relayTokens( + address l2Token, + address l3Token, + uint256 amount + ) external payable virtual; + + /** + * @notice Relay a message to a contract on L3. Implementation changes on whether the + * @notice This function should be implmented differently based on whether the L2-L3 bridge + * requires custom gas tokens to fund cross-chain transactions. + */ + function _relayL3Message(address target, bytes memory message) internal virtual; + + // Function to be overridden to accomodate for each L2's unique method of address aliasing. + function _requireAdminSender() internal virtual; + + // Use the same access control logic implemented in the forwarders to authorize an upgrade. + function _authorizeUpgrade(address) internal virtual override onlyAdmin {} + + function _setCrossDomainAdmin(address _newCrossDomainAdmin) internal { + if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); + crossDomainAdmin = _newCrossDomainAdmin; + emit SetXDomainAdmin(_newCrossDomainAdmin); + } + + function _setL3SpokePool(address _newL3SpokePool) internal { + if (_newL3SpokePool == address(0)) revert InvalidL3SpokePool(); + l3SpokePool = _newL3SpokePool; + emit SetL3SpokePool(_newL3SpokePool); + } +} From f251354340210063881e794a59bed95274142e32 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 23 Sep 2024 15:38:52 -0500 Subject: [PATCH 10/45] disable implementation initialization Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index ed5f27f0..cf763004 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -28,6 +28,15 @@ abstract contract ForwarderBase is UUPSUpgradeable, EIP712CrossChainUpgradeable _; } + /** + @notice Constructs the Forwarder contract. + @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, + * may disrupt the proxy if another EOA were to initialize it. + */ + constructor() { + _disableInitializers(); + } + /** * @notice Initializes the forwarder contract. * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily From 1e984cc52ee30cb74196517e19e36ef7d39abede Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 08:03:17 -0500 Subject: [PATCH 11/45] change names Signed-off-by: bennett --- ...um_L3_Adapter.sol => Rerouter_Adapter.sol} | 38 +++++++++---------- .../foundry/local/Arbitrum_L3_Adapter.t.sol | 20 +++++----- 2 files changed, 29 insertions(+), 29 deletions(-) rename contracts/chain-adapters/{Arbitrum_L3_Adapter.sol => Rerouter_Adapter.sol} (52%) diff --git a/contracts/chain-adapters/Arbitrum_L3_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol similarity index 52% rename from contracts/chain-adapters/Arbitrum_L3_Adapter.sol rename to contracts/chain-adapters/Rerouter_Adapter.sol index d390aa8d..46f30689 100644 --- a/contracts/chain-adapters/Arbitrum_L3_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** - * @notice Contract containing logic to send messages from L1 to Arbitrum-like L3s using an intermediate L2 message forwarder. - * @notice This contract requires an L2 forwarder contract to be deployed, since we overwrite the target field to this new target. + * @notice Contract containing logic to send messages from L1 to a target (not necessarily a spoke pool) on L2. + * @notice The contract receiving messages on L2 will be "spoke pool like" functions, e.g. "relayRootBundle" and + * "relaySpokePoolAdminFunction". * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be * called via delegatecall, which will execute this contract's logic within the context of the originating contract. * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods @@ -13,41 +14,40 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; */ // solhint-disable-next-line contract-name-camelcase -contract Arbitrum_L3_Adapter is AdapterInterface { - address public immutable adapter; - address public immutable l2Forwarder; +contract Rerouter_Adapter is AdapterInterface { + address public immutable l1Adapter; + address public immutable l2Target; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); /** - * @notice Constructs new Adapter for sending tokens/messages to Arbitrum-like L3s. - * @param _adapter Address of the adapter contract on mainnet which implements message transfers + * @notice Constructs new Adapter for sending tokens/messages to an L2 target. + * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. - * @param _l2Forwarder Address of the l2 forwarder contract which relays messages up to the L3 spoke pool. + * @param _l2Target Address of the L2 contract which receives the token and message relays. */ - constructor(address _adapter, address _l2Forwarder) { - adapter = _adapter; - l2Forwarder = _l2Forwarder; + constructor(address _l1Adapter, address _l2Target) { + l1Adapter = _l1Adapter; + l2Target = _l2Target; } /** - * @notice Send cross-chain message to target on L2, which is forwarded to the Arbitrum-like L3. - * @dev there is a bijective mapping of L3 adapters (on L1) to L2 forwarders to L3 spoke pools. The - * spoke pool address is stored by the L2 forwarder and the L2 forwarder address is stored in this contract. + * @notice Send cross-chain message to a target on L2. + * @notice The original target field is omitted since messages are unconditionally sent to `l2Target`. * @param message Data to send to target. */ function relayMessage(address, bytes memory message) external payable override { - (bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Forwarder, message))); + (bool success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message))); if (!success) revert RelayMessageFailed(); } /** - * @notice Bridge tokens to an Arbitrum-like L3, using an L2 forwarder. + * @notice Bridge tokens to a target on L2. * @param l1Token L1 token to deposit. * @param l2Token L2 token to receive. * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. - * @dev we discard the "to" field since tokens are always sent to the l2Forwarder. + * @notice the "to" field is discarded since we unconditionally relay tokens to `l2Target`. */ function relayTokens( address l1Token, @@ -55,8 +55,8 @@ contract Arbitrum_L3_Adapter is AdapterInterface { uint256 amount, address ) external payable override { - (bool success, ) = adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Forwarder)) + (bool success, ) = l1Adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Target)) ); if (!success) revert RelayTokensFailed(l1Token); } diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol index 863e6323..ab04f0e7 100644 --- a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol +++ b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol @@ -6,15 +6,15 @@ import { MockERC20 } from "forge-std/mocks/MockERC20.sol"; import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; -import { Arbitrum_L3_Adapter } from "../../../../contracts/chain-adapters/Arbitrum_L3_Adapter.sol"; +import { Rerouter_Adapter } from "../../../../contracts/chain-adapters/Rerouter_Adapter.sol"; import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol"; import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). -contract Mock_L3_Adapter is Arbitrum_L3_Adapter { - constructor(address _adapter, address _l2Forwarder) Arbitrum_L3_Adapter(_adapter, _l2Forwarder) {} +contract Mock_Rerouter_Adapter is Rerouter_Adapter { + constructor(address _l1Adapter, address _l2Target) Rerouter_Adapter(_l1Adapter, _l2Target) {} receive() external payable {} } @@ -78,7 +78,7 @@ contract StandardBridge { } contract ArbitrumL3AdapterTest is Test { - Arbitrum_L3_Adapter l3Adapter; + Rerouter_Adapter l3Adapter; Optimism_Adapter optimismAdapter; Token_ERC20 l1Token; @@ -88,10 +88,10 @@ contract ArbitrumL3AdapterTest is Test { CrossDomainMessenger crossDomainMessenger; StandardBridge standardBridge; - address l2Forwarder; + address l2Target; function setUp() public { - l2Forwarder = vm.addr(1); + l2Target = makeAddr("l2Target"); l1Token = new Token_ERC20("l1Token", "l1Token"); l2Token = new Token_ERC20("l2Token", "l2Token"); @@ -108,13 +108,13 @@ contract ArbitrumL3AdapterTest is Test { IERC20(address(0)), ITokenMessenger(address(0)) ); - l3Adapter = new Mock_L3_Adapter(address(optimismAdapter), l2Forwarder); + l3Adapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target); } // Messages should be indiscriminately sent to the l2Forwarder. function testRelayMessage(address target, bytes memory message) public { vm.expectEmit(address(crossDomainMessenger)); - emit CrossDomainMessenger.MessageSent(l2Forwarder); + emit CrossDomainMessenger.MessageSent(l2Target); l3Adapter.relayMessage(target, message); } @@ -124,7 +124,7 @@ contract ArbitrumL3AdapterTest is Test { l1Weth.mint(address(l3Adapter), amountToSend); assertEq(amountToSend, l1Weth.totalSupply()); vm.expectEmit(address(standardBridge)); - emit StandardBridge.ETHDepositInitiated(l2Forwarder, amountToSend); + emit StandardBridge.ETHDepositInitiated(l2Target, amountToSend); l3Adapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); assertEq(0, l1Weth.totalSupply()); } @@ -134,7 +134,7 @@ contract ArbitrumL3AdapterTest is Test { l1Token.mint(address(l3Adapter), amountToSend); assertEq(amountToSend, l1Token.totalSupply()); l3Adapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); - assertEq(amountToSend, l2Token.balanceOf(l2Forwarder)); + assertEq(amountToSend, l2Token.balanceOf(l2Target)); assertEq(amountToSend, l2Token.totalSupply()); assertEq(0, l1Token.totalSupply()); } From fe66b780fa9c9e63e79f15fe7827bd2c6b86e3bb Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 08:48:35 -0500 Subject: [PATCH 12/45] remove EIP 712 Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index cf763004..5ca7c41b 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -2,9 +2,8 @@ pragma solidity ^0.8.0; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { EIP712CrossChainUpgradeable } from "../upgradeable/EIP712CrossChainUpgradeable.sol"; -abstract contract ForwarderBase is UUPSUpgradeable, EIP712CrossChainUpgradeable { +abstract contract ForwarderBase is UUPSUpgradeable { // L3 address of the recipient of fallback messages and tokens. address public l3SpokePool; @@ -46,7 +45,6 @@ abstract contract ForwarderBase is UUPSUpgradeable, EIP712CrossChainUpgradeable */ function __Forwarder_init(address _l3SpokePool, address _crossDomainAdmin) public onlyInitializing { __UUPSUpgradeable_init(); - __EIP712_init("ACROSS-V2", "1.0.0"); _setL3SpokePool(_l3SpokePool); _setCrossDomainAdmin(_crossDomainAdmin); } From 814498b0feb17b83088e27eaba26306cd0ac0108 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 09:13:38 -0500 Subject: [PATCH 13/45] add/improve comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 5ca7c41b..9e962dd1 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -3,8 +3,16 @@ pragma solidity ^0.8.0; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +/** + * @title ForwarderBase + * @notice Base contract deployed some network between L1 and the network housing a spoke pool contract. If a message is sent + * to this contract which came from the cross domain admin, then it is routed to the next network via the canonical messaging + * bridge (which may be done automatically by the network's finalization infrastructure). Tokens sent from L1 are stored temporarily + * on this contract. Any EOA can initiate a bridge of these tokens to the target `l3SpokePool`. + * @custom:security-contact bugs@across.to + */ abstract contract ForwarderBase is UUPSUpgradeable { - // L3 address of the recipient of fallback messages and tokens. + // L3 address of the recipient of L1 messages and tokens. address public l3SpokePool; // L1 address of the contract which can relay messages to the l3SpokePool contract and update this proxy contract. @@ -19,8 +27,8 @@ abstract contract ForwarderBase is UUPSUpgradeable { error InvalidL3SpokePool(); /* - * @dev All functions with this modifier must revert if msg.sender != CROSS_DOMAIN_ADMIN, but each L2 may have - * unique aliasing logic, so it is up to the forwarder contract to verify that the sender is valid. + * @dev All functions with this modifier must revert if msg.sender != crossDomainAdmin. Each L2 may have + * unique aliasing logic, so it is up to the chain-specific forwarder contract to verify that the sender is valid. */ modifier onlyAdmin() { _requireAdminSender(); @@ -55,7 +63,7 @@ abstract contract ForwarderBase is UUPSUpgradeable { /** * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward - * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder + * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder. */ fallback() external payable onlyAdmin { _relayL3Message(l3SpokePool, msg.data); From 43f03b80f4b408592a9ac5b22c1d109c4588a73a Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 09:16:45 -0500 Subject: [PATCH 14/45] add security contact Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 46f30689..7b6c87a4 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -11,6 +11,7 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; * called via delegatecall, which will execute this contract's logic within the context of the originating contract. * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods * that call this contract's logic guard against reentrancy. + * @custom:security-contact bugs@across.to */ // solhint-disable-next-line contract-name-camelcase From cc386a73aeb64319c413c171e677b2465502f431 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 12:12:16 -0500 Subject: [PATCH 15/45] update comment Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 7b6c87a4..08483432 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -5,6 +5,8 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @notice Contract containing logic to send messages from L1 to a target (not necessarily a spoke pool) on L2. + * @notice There should be one of these adapters for each L3 spoke pool deployment, or equivalently, each L2 + * forwarder/adapter contract. * @notice The contract receiving messages on L2 will be "spoke pool like" functions, e.g. "relayRootBundle" and * "relaySpokePoolAdminFunction". * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be From a9cf8b14c2fd7d39a9ac9548ea30f4572e41f6ad Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 12:18:27 -0500 Subject: [PATCH 16/45] update comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 9e962dd1..f59ec848 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -5,6 +5,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils /** * @title ForwarderBase + * @notice This contract expects to receive messages and tokens from the Hub Pool on L1. It rejects messages which do not originate + * from the hub pool. * @notice Base contract deployed some network between L1 and the network housing a spoke pool contract. If a message is sent * to this contract which came from the cross domain admin, then it is routed to the next network via the canonical messaging * bridge (which may be done automatically by the network's finalization infrastructure). Tokens sent from L1 are stored temporarily From 6cbbfd48618fff81f6027f9a88cc632757c1879c Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 24 Sep 2024 12:25:51 -0500 Subject: [PATCH 17/45] add comment about cross chain contracts Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 08483432..11b348e9 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -5,6 +5,11 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @notice Contract containing logic to send messages from L1 to a target (not necessarily a spoke pool) on L2. + * @notice Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` + * will be the L3 spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this + * contract DOES NOT send anything to the L2 containing info on the target L3 spoke pool. The L3 spoke pool address + * must instead be initialized on the `l2Target` contract as the same spoke pool address found in the hub pool's + * `crossChainContracts` mapping. * @notice There should be one of these adapters for each L3 spoke pool deployment, or equivalently, each L2 * forwarder/adapter contract. * @notice The contract receiving messages on L2 will be "spoke pool like" functions, e.g. "relayRootBundle" and From 6d8a6339a81368fc47271c89581408d5d8b94f1b Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 10:43:07 -0500 Subject: [PATCH 18/45] fix comments and refactor test Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 20 ++-- contracts/test/MockBedrockStandardBridge.sol | 37 +++++++ .../foundry/local/Arbitrum_L3_Adapter.t.sol | 96 +++++++------------ 3 files changed, 80 insertions(+), 73 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 11b348e9..a3f4006b 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -4,16 +4,13 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** - * @notice Contract containing logic to send messages from L1 to a target (not necessarily a spoke pool) on L2. - * @notice Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` + * @notice Contract containing logic to send messages from L1 to a target on L2. + * @dev Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` * will be the L3 spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this - * contract DOES NOT send anything to the L2 containing info on the target L3 spoke pool. The L3 spoke pool address - * must instead be initialized on the `l2Target` contract as the same spoke pool address found in the hub pool's - * `crossChainContracts` mapping. - * @notice There should be one of these adapters for each L3 spoke pool deployment, or equivalently, each L2 - * forwarder/adapter contract. - * @notice The contract receiving messages on L2 will be "spoke pool like" functions, e.g. "relayRootBundle" and - * "relaySpokePoolAdminFunction". + * contract cannot send anything directly to the L3 target. Instead, it "re-routes" messages to the L3 via an L2 + * contract set as the `l2Target` in this contract. The L3 spoke pool address must be initialized on the `l2Target` + * contract to the same L3 spoke pool address found in the hub pool's `crossChainContracts` mapping. There should be + * one of these adapters for each L3 spoke pool deployment. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be * called via delegatecall, which will execute this contract's logic within the context of the originating contract. * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods @@ -30,7 +27,8 @@ contract Rerouter_Adapter is AdapterInterface { error RelayTokensFailed(address l1Token); /** - * @notice Constructs new Adapter for sending tokens/messages to an L2 target. + * @notice Constructs new Adapter for sending tokens/messages to an L2 target. This contract will + * re-route messages to the _l2Target via the _l1Adapter. * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. * @param _l2Target Address of the L2 contract which receives the token and message relays. @@ -41,7 +39,7 @@ contract Rerouter_Adapter is AdapterInterface { } /** - * @notice Send cross-chain message to a target on L2. + * @notice Send cross-chain message to a target on L2 which will re-route messages to the intended L3 target. * @notice The original target field is omitted since messages are unconditionally sent to `l2Target`. * @param message Data to send to target. */ diff --git a/contracts/test/MockBedrockStandardBridge.sol b/contracts/test/MockBedrockStandardBridge.sol index 2aede20a..61e9576e 100644 --- a/contracts/test/MockBedrockStandardBridge.sol +++ b/contracts/test/MockBedrockStandardBridge.sol @@ -28,3 +28,40 @@ contract MockBedrockL2StandardBridge is IL2ERC20Bridge { // do nothing } } + +contract MockBedrockL1StandardBridge { + event ETHDepositInitiated(address indexed to, uint256 amount); + event ERC20DepositInitiated(address indexed to, address l1Token, address l2Token, uint256 amount); + + function depositERC20To( + address l1Token, + address l2Token, + address to, + uint256 amount, + uint32, + bytes calldata + ) external { + IERC20(l1Token).transferFrom(msg.sender, address(this), amount); + emit ERC20DepositInitiated(to, l1Token, l2Token, amount); + } + + function depositETHTo( + address to, + uint32, + bytes calldata + ) external payable { + emit ETHDepositInitiated(to, msg.value); + } +} + +contract MockBedrockCrossDomainMessenger { + event MessageSent(address indexed target); + + function sendMessage( + address target, + bytes calldata, + uint32 + ) external { + emit MessageSent(target); + } +} diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol index ab04f0e7..c1e8a4fa 100644 --- a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol +++ b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol @@ -9,7 +9,11 @@ import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1Stand import { Rerouter_Adapter } from "../../../../contracts/chain-adapters/Rerouter_Adapter.sol"; import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol"; import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; +import { WETH9 } from "../../../../contracts/external/WETH9.sol"; import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; +import { MockBedrockL1StandardBridge, MockBedrockCrossDomainMessenger } from "../../../../contracts/test/MockBedrockStandardBridge.sol"; + +import "forge-std/console.sol"; // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). @@ -31,62 +35,16 @@ contract Token_ERC20 is ERC20 { } } -contract MinimalWeth is Token_ERC20 { - constructor(string memory name, string memory symbol) Token_ERC20(name, symbol) {} - - function withdraw(uint256 amount) public { - _burn(msg.sender, amount); - (bool success, ) = payable(msg.sender).call{ value: amount }(""); - require(success); - } -} - -contract CrossDomainMessenger { - event MessageSent(address indexed target); - - function sendMessage( - address target, - bytes calldata, - uint32 - ) external { - emit MessageSent(target); - } -} - -contract StandardBridge { - event ETHDepositInitiated(address indexed to, uint256 amount); - - function depositERC20To( - address l1Token, - address l2Token, - address to, - uint256 amount, - uint32, - bytes calldata - ) external { - Token_ERC20(l1Token).burn(msg.sender, amount); - Token_ERC20(l2Token).mint(to, amount); - } - - function depositETHTo( - address to, - uint32, - bytes calldata - ) external payable { - emit ETHDepositInitiated(to, msg.value); - } -} - contract ArbitrumL3AdapterTest is Test { Rerouter_Adapter l3Adapter; Optimism_Adapter optimismAdapter; Token_ERC20 l1Token; Token_ERC20 l2Token; - Token_ERC20 l1Weth; - Token_ERC20 l2Weth; - CrossDomainMessenger crossDomainMessenger; - StandardBridge standardBridge; + WETH9 l1Weth; + WETH9 l2Weth; + MockBedrockCrossDomainMessenger crossDomainMessenger; + MockBedrockL1StandardBridge standardBridge; address l2Target; @@ -95,11 +53,11 @@ contract ArbitrumL3AdapterTest is Test { l1Token = new Token_ERC20("l1Token", "l1Token"); l2Token = new Token_ERC20("l2Token", "l2Token"); - l1Weth = new MinimalWeth("l1Weth", "l1Weth"); - l2Weth = new MinimalWeth("l2Weth", "l2Weth"); + l1Weth = new WETH9(); + l2Weth = new WETH9(); - crossDomainMessenger = new CrossDomainMessenger(); - standardBridge = new StandardBridge(); + crossDomainMessenger = new MockBedrockCrossDomainMessenger(); + standardBridge = new MockBedrockL1StandardBridge(); optimismAdapter = new Optimism_Adapter( WETH9Interface(address(l1Weth)), @@ -113,29 +71,43 @@ contract ArbitrumL3AdapterTest is Test { // Messages should be indiscriminately sent to the l2Forwarder. function testRelayMessage(address target, bytes memory message) public { + vm.assume(target != l2Target); vm.expectEmit(address(crossDomainMessenger)); - emit CrossDomainMessenger.MessageSent(l2Target); + emit MockBedrockCrossDomainMessenger.MessageSent(l2Target); l3Adapter.relayMessage(target, message); } // Sending Weth should call depositETHTo(). function testRelayWeth(uint256 amountToSend, address random) public { + // Prevent fuzz testing with amountToSend * 2 > 2^256 + amountToSend = uint256(bound(amountToSend, 1, 2**254)); vm.deal(address(l1Weth), amountToSend); - l1Weth.mint(address(l3Adapter), amountToSend); - assertEq(amountToSend, l1Weth.totalSupply()); + vm.deal(address(l3Adapter), amountToSend); + + vm.startPrank(address(l3Adapter)); + l1Weth.deposit{ value: amountToSend }(); + vm.stopPrank(); + + assertEq(amountToSend * 2, l1Weth.totalSupply()); vm.expectEmit(address(standardBridge)); - emit StandardBridge.ETHDepositInitiated(l2Target, amountToSend); + emit MockBedrockL1StandardBridge.ETHDepositInitiated(l2Target, amountToSend); l3Adapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); - assertEq(0, l1Weth.totalSupply()); + assertEq(0, l1Weth.balanceOf(address(l3Adapter))); } // Sending any random token should call depositERC20To(). function testRelayToken(uint256 amountToSend, address random) public { l1Token.mint(address(l3Adapter), amountToSend); assertEq(amountToSend, l1Token.totalSupply()); + + vm.expectEmit(address(standardBridge)); + emit MockBedrockL1StandardBridge.ERC20DepositInitiated( + l2Target, + address(l1Token), + address(l2Token), + amountToSend + ); l3Adapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); - assertEq(amountToSend, l2Token.balanceOf(l2Target)); - assertEq(amountToSend, l2Token.totalSupply()); - assertEq(0, l1Token.totalSupply()); + assertEq(0, l1Token.balanceOf(address(l3Adapter))); } } From 4076b6bd58ad6f16a303f549118ee03301a6a0b1 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 11:15:26 -0500 Subject: [PATCH 19/45] comments Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 3 ++- test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index a3f4006b..46904de8 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -4,7 +4,8 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** - * @notice Contract containing logic to send messages from L1 to a target on L2. + * @notice Contract containing logic to send messages from L1 to a target on L3 via re-routing messages to an + * intermediate contract on L2. * @dev Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` * will be the L3 spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this * contract cannot send anything directly to the L3 target. Instead, it "re-routes" messages to the L3 via an L2 diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol index c1e8a4fa..d2759e97 100644 --- a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol +++ b/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol @@ -13,8 +13,6 @@ import { WETH9 } from "../../../../contracts/external/WETH9.sol"; import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; import { MockBedrockL1StandardBridge, MockBedrockCrossDomainMessenger } from "../../../../contracts/test/MockBedrockStandardBridge.sol"; -import "forge-std/console.sol"; - // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). contract Mock_Rerouter_Adapter is Rerouter_Adapter { From 110a8e93ce65176a786469002300dd5b87fdaf45 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 11:49:39 -0500 Subject: [PATCH 20/45] comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index f59ec848..09d886b9 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -5,16 +5,16 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils /** * @title ForwarderBase - * @notice This contract expects to receive messages and tokens from the Hub Pool on L1. It rejects messages which do not originate - * from the hub pool. - * @notice Base contract deployed some network between L1 and the network housing a spoke pool contract. If a message is sent - * to this contract which came from the cross domain admin, then it is routed to the next network via the canonical messaging - * bridge (which may be done automatically by the network's finalization infrastructure). Tokens sent from L1 are stored temporarily - * on this contract. Any EOA can initiate a bridge of these tokens to the target `l3SpokePool`. + * @notice This contract expects to receive messages and tokens from the Hub Pool on L1 and forwards them to targets on L3. Messages + * are intended to originate from the re-router adapter on L1 which will send messages here to re-route them to the corresponding L3 + * spoke pool. It rejects messages which do not originate from the cross domain admin, which should be set to the hub pool. + * @dev Base contract designed to be deployed on L2 to re-route messages from L1 to L3. If a message is sent to this contract which + * came from the L1 cross domain admin, then it is routed to the L3 via the canonical messaging bridge. Tokens sent from L1 are stored + * temporarily on this contract. Any EOA can initiate a bridge of these tokens to the target `l3SpokePool`. * @custom:security-contact bugs@across.to */ abstract contract ForwarderBase is UUPSUpgradeable { - // L3 address of the recipient of L1 messages and tokens. + // L3 address of the recipient of L1 messages and tokens sent via the re-router adapter on L1. address public l3SpokePool; // L1 address of the contract which can relay messages to the l3SpokePool contract and update this proxy contract. @@ -59,12 +59,12 @@ abstract contract ForwarderBase is UUPSUpgradeable { _setCrossDomainAdmin(_crossDomainAdmin); } - // Added so that this function may receive ETH in the event of stuck transactions. + // Added so that this contract may receive ETH to fund transaction gas fees. receive() external payable {} /** - * @notice When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function - * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we simply forward + * @dev When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function + * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we forward * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder. */ fallback() external payable onlyAdmin { @@ -89,10 +89,11 @@ abstract contract ForwarderBase is UUPSUpgradeable { /** * @notice Bridge tokens to an L3. - * @notice relayTokens should only send tokens to L3_SPOKE_POOL, so no access control is required. * @param l2Token L2 token to deposit. * @param l3Token L3 token to receive on the destination chain. * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. + * @dev relayTokens should only send tokens to the l3SpokePool address, so no access control + * is required. */ function relayTokens( address l2Token, @@ -101,8 +102,8 @@ abstract contract ForwarderBase is UUPSUpgradeable { ) external payable virtual; /** - * @notice Relay a message to a contract on L3. Implementation changes on whether the - * @notice This function should be implmented differently based on whether the L2-L3 bridge + * @notice Relay a message to a contract on L3. + * @dev This function should be implmented differently based on whether the L2-L3 bridge * requires custom gas tokens to fund cross-chain transactions. */ function _relayL3Message(address target, bytes memory message) internal virtual; From ac7b0dedc4756165664582cca00144b323793e8b Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 12:29:28 -0500 Subject: [PATCH 21/45] wrap message Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 46904de8..d8a34273 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -41,11 +41,14 @@ contract Rerouter_Adapter is AdapterInterface { /** * @notice Send cross-chain message to a target on L2 which will re-route messages to the intended L3 target. - * @notice The original target field is omitted since messages are unconditionally sent to `l2Target`. + * @param target Address of the L3 contract which receives `message` after it has been forwarded on L2. * @param message Data to send to target. */ - function relayMessage(address, bytes memory message) external payable override { - (bool success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message))); + function relayMessage(address target, bytes memory message) external payable override { + bytes memory updatedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); + (bool success, ) = l1Adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (l2Target, updatedMessage)) + ); if (!success) revert RelayMessageFailed(); } From 19813c6c0a7f304969f64249b9a8ea213c804815 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 12:35:05 -0500 Subject: [PATCH 22/45] update forwarder base Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 33 ---------------------- 1 file changed, 33 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 09d886b9..da91f0e2 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -59,18 +59,6 @@ abstract contract ForwarderBase is UUPSUpgradeable { _setCrossDomainAdmin(_crossDomainAdmin); } - // Added so that this contract may receive ETH to fund transaction gas fees. - receive() external payable {} - - /** - * @dev When called by the cross domain admin (i.e. the hub pool), the msg.data should be some function - * recognizable by the L3 spoke pool, such as "relayRootBundle" or "upgradeTo". Therefore, we forward - * this message to the L3 spoke pool using the implemented messaging logic of the L2 forwarder. - */ - fallback() external payable onlyAdmin { - _relayL3Message(l3SpokePool, msg.data); - } - /** * @notice Sets a new cross domain admin for this contract. * @param _newCrossDomainAdmin L1 address of the new cross domain admin. @@ -87,27 +75,6 @@ abstract contract ForwarderBase is UUPSUpgradeable { _setL3SpokePool(_newL3SpokePool); } - /** - * @notice Bridge tokens to an L3. - * @param l2Token L2 token to deposit. - * @param l3Token L3 token to receive on the destination chain. - * @param amount Amount of L2 tokens to deposit and L3 tokens to receive. - * @dev relayTokens should only send tokens to the l3SpokePool address, so no access control - * is required. - */ - function relayTokens( - address l2Token, - address l3Token, - uint256 amount - ) external payable virtual; - - /** - * @notice Relay a message to a contract on L3. - * @dev This function should be implmented differently based on whether the L2-L3 bridge - * requires custom gas tokens to fund cross-chain transactions. - */ - function _relayL3Message(address target, bytes memory message) internal virtual; - // Function to be overridden to accomodate for each L2's unique method of address aliasing. function _requireAdminSender() internal virtual; From d87d09c0dc549bd4451bae0557c0e09bca1d95d6 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 13:01:36 -0500 Subject: [PATCH 23/45] comment Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index d8a34273..96071e94 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -42,7 +42,10 @@ contract Rerouter_Adapter is AdapterInterface { /** * @notice Send cross-chain message to a target on L2 which will re-route messages to the intended L3 target. * @param target Address of the L3 contract which receives `message` after it has been forwarded on L2. - * @param message Data to send to target. + * @param message Data to send to `target`. + * @dev The message passed into this function is wrapped into a `relayMessage` function call, which is then passed + * to L2. The l2Target contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's + * `relayMessage` call will be these target and message values. */ function relayMessage(address target, bytes memory message) external payable override { bytes memory updatedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); From d78ad6fdd7e6b09cd7ffd4ca20d51df534ac7ba1 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 25 Sep 2024 13:17:57 -0500 Subject: [PATCH 24/45] extend AdapterInterface Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index da91f0e2..5b64fa68 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @title ForwarderBase @@ -13,7 +14,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils * temporarily on this contract. Any EOA can initiate a bridge of these tokens to the target `l3SpokePool`. * @custom:security-contact bugs@across.to */ -abstract contract ForwarderBase is UUPSUpgradeable { +abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // L3 address of the recipient of L1 messages and tokens sent via the re-router adapter on L1. address public l3SpokePool; From 8ee13c0dc190bdd0b9ec308466c0b76f08035b67 Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 26 Sep 2024 09:29:39 -0500 Subject: [PATCH 25/45] refactor: move all Arbitrum interfaces to a single location Signed-off-by: bennett --- contracts/Arbitrum_SpokePool.sol | 12 +- contracts/chain-adapters/Arbitrum_Adapter.sol | 125 +--------- .../Arbitrum_CustomGasToken_Adapter.sol | 113 +-------- .../interfaces/ArbitrumBridgeInterfaces.sol | 220 ++++++++++++++++++ 4 files changed, 224 insertions(+), 246 deletions(-) create mode 100644 contracts/interfaces/ArbitrumBridgeInterfaces.sol diff --git a/contracts/Arbitrum_SpokePool.sol b/contracts/Arbitrum_SpokePool.sol index ecdd5bab..7620b855 100644 --- a/contracts/Arbitrum_SpokePool.sol +++ b/contracts/Arbitrum_SpokePool.sol @@ -6,15 +6,7 @@ pragma solidity ^0.8.19; import "./SpokePool.sol"; import "./libraries/CircleCCTPAdapter.sol"; - -interface StandardBridgeLike { - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - bytes calldata _data - ) external payable returns (bytes memory); -} +import { ArbitrumL2ERC20GatewayLike } from "./interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice AVM specific SpokePool. Uses AVM cross-domain-enabled logic to implement admin only access to functions. @@ -100,7 +92,7 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter { address ethereumTokenToBridge = whitelistedTokens[l2TokenAddress]; require(ethereumTokenToBridge != address(0), "Uninitialized mainnet token"); //slither-disable-next-line unused-return - StandardBridgeLike(l2GatewayRouter).outboundTransfer( + ArbitrumL2ERC20GatewayLike(l2GatewayRouter).outboundTransfer( ethereumTokenToBridge, // _l1Token. Address of the L1 token to bridge over. hubPool, // _to. Withdraw, over the bridge, to the l1 hub pool contract. amountToReturn, // _amount. diff --git a/contracts/chain-adapters/Arbitrum_Adapter.sol b/contracts/chain-adapters/Arbitrum_Adapter.sol index 3cfd0f32..f8ec507e 100644 --- a/contracts/chain-adapters/Arbitrum_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_Adapter.sol @@ -7,130 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../external/interfaces/CCTPInterfaces.sol"; import "../libraries/CircleCCTPAdapter.sol"; - -/** - * @notice Interface for Arbitrum's L1 Inbox contract used to send messages to Arbitrum. - * @custom:security-contact bugs@across.to - */ -interface ArbitrumL1InboxLike { - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. - * all msg.value will deposited to callValueRefundAddress on L2 - * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed - * funds come from the deposit alone, rather than falling back on the user's L2 balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function unsafeCreateRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); -} - -/** - * @notice Layer 1 Gateway contract for bridging standard ERC20s to Arbitrum. - */ -interface ArbitrumL1ERC20GatewayLike { - /** - * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges - * like the DAI bridge. - * @dev Refunded to aliased L2 address of sender if sender has code on L1, otherwise to to sender's EOA on L2. - * @param _l1Token L1 address of ERC20 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum. - * @dev L2 address alias will not be applied to the following types of addresses on L1: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _l1Token L1 address of ERC20 - * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _l1Token, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); -} +import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Contract containing logic to send messages from L1 to Arbitrum. diff --git a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol index f1c36f1c..203ee227 100644 --- a/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol +++ b/contracts/chain-adapters/Arbitrum_CustomGasToken_Adapter.sol @@ -7,6 +7,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ITokenMessenger as ICCTPTokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; +import { ArbitrumERC20Bridge as ArbitrumL1ERC20Bridge, ArbitrumCustomGasTokenInbox as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; /** * @notice Interface for funder contract that this contract pulls from to pay for relayMessage()/relayTokens() @@ -23,118 +24,6 @@ interface FunderInterface { function withdraw(IERC20 token, uint256 amount) external; } -/** - * @title Staging ground for incoming and outgoing messages - * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is - * used as native currency on L2. - * @dev Fees are paid in this token. There are certain restrictions on the native token: - * - The token can't be rebasing or have a transfer fee - * - The token must only be transferrable via a call to the token address itself - * - The token must only be able to set allowance via a call to the token address itself - * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert - * - The token must have a max of 2^256 - 1 wei total supply unscaled - * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals - */ -interface ArbitrumL1ERC20Bridge { - /** - * @notice Returns token that is escrowed in bridge on L1 side and minted on L2 as native currency. - * @dev This function doesn't exist on the generic Bridge interface. - * @return address of the native token. - */ - function nativeToken() external view returns (address); - - /** - * @dev number of decimals used by the native token - * This is set on bridge initialization using nativeToken.decimals() - * If the token does not have decimals() method, we assume it have 0 decimals - */ - function nativeTokenDecimals() external view returns (uint8); -} - -/** - * @title Inbox for user and contract originated messages - * @notice Messages created via this inbox are enqueued in the delayed accumulator - * to await inclusion in the SequencerInbox - */ -interface ArbitrumL1InboxLike { - /** - * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface - * to return an ArbitrumL1ERC20Bridge instead of a more generic Bridge interface. - * @return address of the bridge. - */ - function bridge() external view returns (ArbitrumL1ERC20Bridge); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying - * for L1 to L2 message using a custom gas token. - * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on L2 - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - l2CallValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on L2. - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on L2. - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 tokenTotalFeeAmount, - bytes calldata data - ) external returns (uint256); -} - -/** - * @notice Layer 1 Gateway contract for bridging standard ERC20s to Arbitrum. - */ -interface ArbitrumL1ERC20GatewayLike { - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum. - * @dev L2 address alias will not be applied to the following types of addresses on L1: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _l1Token L1 address of ERC20 - * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 - * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), - * not subject to L2 aliasing. This account, or its L2 alias if it have code in L1, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param _gasPriceBid Gas price for L2 execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _l1Token, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); -} - /** * @notice Contract containing logic to send messages from L1 to Arbitrum. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be diff --git a/contracts/interfaces/ArbitrumBridgeInterfaces.sol b/contracts/interfaces/ArbitrumBridgeInterfaces.sol new file mode 100644 index 00000000..a2b31724 --- /dev/null +++ b/contracts/interfaces/ArbitrumBridgeInterfaces.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Staging ground for incoming and outgoing messages + * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is + * used as native currency on upper layer. + * @dev Fees are paid in this token. There are certain restrictions on the native token: + * - The token can't be rebasing or have a transfer fee + * - The token must only be transferrable via a call to the token address itself + * - The token must only be able to set allowance via a call to the token address itself + * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert + * - The token must have a max of 2^256 - 1 wei total supply unscaled + * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals + */ +interface ArbitrumERC20Bridge { + /** + * @notice Returns token that is escrowed in bridge on the lower layer and minted on the upper layer as native currency. + * @dev This function doesn't exist on the generic Bridge interface. + * @return address of the native token. + */ + function nativeToken() external view returns (address); + + /** + * @dev number of decimals used by the native token + * This is set on bridge initialization using nativeToken.decimals() + * If the token does not have decimals() method, we assume it have 0 decimals + */ + function nativeTokenDecimals() external view returns (uint8); +} + +/** + * @title Inbox for user and contract originated messages + * @notice Messages created via this inbox are enqueued in the delayed accumulator + * to await inclusion in the SequencerInbox + */ +interface ArbitrumInboxLike { + /** + * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface + * to return an ArbitrumERC20Bridge instead of a more generic Bridge interface. + * @return address of the bridge. + */ + function bridge() external view returns (ArbitrumERC20Bridge); + + /** + * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. + * all msg.value will deposited to callValueRefundAddress on the upper layer + * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's (upper layer) balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on (upper layer) balance + * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's upper layer balance to cover upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + + /** + * @notice Put a message in the source chain inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed + * funds come from the deposit alone, rather than falling back on the user's balance + * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). + * createRetryableTicket method is the recommended standard. + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's source chain balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on source chain balance + * @param callValueRefundAddress callvalue gets credited here on source chain if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of the message + * @return unique message number of the retryable transaction + */ + function unsafeCreateRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); +} + +/** + * @notice Interface which extends ArbitrumInboxLike with functions used to interact with bridges that use a custom gas token. + */ +interface ArbitrumCustomGasTokenInbox is ArbitrumInboxLike { + /** + * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts + * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying + * for message using a custom gas token. + * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on upper layer + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - callValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. + * @param to destination contract address + * @param callValue call value for retryable message + * @param maxSubmissionCost Max gas deducted from user's upper layer balance to cover base submission fee + * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on upper layer. + * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on upper layer. + * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost + * @param data ABI encoded data of message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 callValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256); +} + +/** + * @notice Generic gateway contract for bridging standard ERC20s to/from Arbitrum-like networks. + * @notice These function signatures are shared between the L1 and L2 gateway router contracts. + */ +interface ArbitrumL1ERC20GatewayLike { + /** + * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges + * like the DAI bridge. + * @dev Refunded to aliased address of sender if sender has code on source chain, otherwise to to sender's EOA on destination chain. + * @param _sourceToken address of ERC20 + * @param _to Account to be credited with the tokens at the destination (can be the user's account or a contract), + * not subject to aliasing. This account, or its alias if it has code in the source chain, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's balance to cover execution + * @param _gasPriceBid Gas price for execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransfer( + address _sourceToken, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + /** + * @notice get ERC20 gateway for token. + * @param _token ERC20 address. + * @return address of ERC20 gateway. + */ + function getGateway(address _token) external view returns (address); + + /** + * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. + * @dev Upper layer address alias will not be applied to the following types of addresses on lower layer: + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * @param _sourceToken address of ERC20 on source chain. + * @param _refundTo Account, or its alias if it has code on the source chain, to be credited with excess gas refund at destination + * @param _to Account to be credited with the tokens in the L3 (can be the user's L3 account or a contract), + * not subject to aliasing. This account, or its alias if it has code on the source chain, will also be able to + * cancel the retryable ticket and receive callvalue refund + * @param _amount Token Amount + * @param _maxGas Max gas deducted from user's balance to cover execution + * @param _gasPriceBid Gas price for execution + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function outboundTransferCustomRefund( + address _sourceToken, + address _refundTo, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); +} + +interface ArbitrumL2ERC20GatewayLike { + /** + * @notice Fetches the l2 token address from the gateway router for the input l1 token address + * @param _l1Erc20 address of the l1 token. + */ + function calculateL2TokenAddress(address _l1Erc20) external view returns (address); + + /** + * @notice Withdraws a specified amount of an l2 token to an l1 token. + * @param _l1Token address of the token to withdraw on L1. + * @param _to address on L1 which will receive the tokens upon withdrawal. + * @param _amount amount of the token to withdraw. + * @param _data encoded data to send to the gateway router. + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable returns (bytes memory); +} From a282d8301d93a0fce8b62d4fbadb993aafeb950a Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 08:20:57 -0500 Subject: [PATCH 26/45] update rerouter adapter with newest version Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 34 ++++++++++++++++--- ...3_Adapter.t.sol => Rerouter_Adapter.t.sol} | 28 ++++++++------- 2 files changed, 45 insertions(+), 17 deletions(-) rename test/evm/foundry/local/{Arbitrum_L3_Adapter.t.sol => Rerouter_Adapter.t.sol} (81%) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 96071e94..35842e1d 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -12,6 +12,8 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; * contract set as the `l2Target` in this contract. The L3 spoke pool address must be initialized on the `l2Target` * contract to the same L3 spoke pool address found in the hub pool's `crossChainContracts` mapping. There should be * one of these adapters for each L3 spoke pool deployment. + * @dev The contract set as the l2Target must implement ForwarderBase in order for tokens and messages to be automatically + * forwarded to the next layers. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be * called via delegatecall, which will execute this contract's logic within the context of the originating contract. * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods @@ -23,6 +25,7 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; contract Rerouter_Adapter is AdapterInterface { address public immutable l1Adapter; address public immutable l2Target; + uint256 public immutable spokePoolChainId; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); @@ -33,10 +36,16 @@ contract Rerouter_Adapter is AdapterInterface { * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. * @param _l2Target Address of the L2 contract which receives the token and message relays. + * @param _spokePoolChainId Chain ID of the network which contains this adapter's corresponding spoke pool deployment. */ - constructor(address _l1Adapter, address _l2Target) { + constructor( + address _l1Adapter, + address _l2Target, + uint256 _spokePoolChainId + ) { l1Adapter = _l1Adapter; l2Target = _l2Target; + spokePoolChainId = _spokePoolChainId; } /** @@ -45,10 +54,14 @@ contract Rerouter_Adapter is AdapterInterface { * @param message Data to send to `target`. * @dev The message passed into this function is wrapped into a `relayMessage` function call, which is then passed * to L2. The l2Target contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's - * `relayMessage` call will be these target and message values. + * `relayMessage` call will be these target and message values. When sending a message, `spokePoolChainId` is abi-encoded + * to provide the forwarder with the necessary information to determine subsequent bridge routes. */ function relayMessage(address target, bytes memory message) external payable override { - bytes memory updatedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); + bytes memory updatedMessage = abi.encode( + spokePoolChainId, + abi.encodeCall(AdapterInterface.relayMessage, (target, message)) + ); (bool success, ) = l1Adapter.delegatecall( abi.encodeCall(AdapterInterface.relayMessage, (l2Target, updatedMessage)) ); @@ -60,17 +73,28 @@ contract Rerouter_Adapter is AdapterInterface { * @param l1Token L1 token to deposit. * @param l2Token L2 token to receive. * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. - * @notice the "to" field is discarded since we unconditionally relay tokens to `l2Target`. + * @param target The address of the spoke pool which should ultimately receive `amount` of `l1Token`. + * @dev When sending a message, `spokePoolChainId` is abi-encoded to provide the forwarder with the necessary information + * to determine subsequent bridge routes. */ function relayTokens( address l1Token, address l2Token, uint256 amount, - address + address target ) external payable override { + // Relay tokens to the forwarder. (bool success, ) = l1Adapter.delegatecall( abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Target)) ); if (!success) revert RelayTokensFailed(l1Token); + + // Follow-up token relay with a message to continue the token relay on L2. + bytes memory message = abi.encode( + spokePoolChainId, + abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, target)) + ); + (success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message))); + if (!success) revert RelayMessageFailed(); } } diff --git a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol b/test/evm/foundry/local/Rerouter_Adapter.t.sol similarity index 81% rename from test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol rename to test/evm/foundry/local/Rerouter_Adapter.t.sol index d2759e97..e55b51b9 100644 --- a/test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol +++ b/test/evm/foundry/local/Rerouter_Adapter.t.sol @@ -16,7 +16,11 @@ import { MockBedrockL1StandardBridge, MockBedrockCrossDomainMessenger } from ".. // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). contract Mock_Rerouter_Adapter is Rerouter_Adapter { - constructor(address _l1Adapter, address _l2Target) Rerouter_Adapter(_l1Adapter, _l2Target) {} + constructor( + address _l1Adapter, + address _l2Target, + uint256 _spokePoolChainId + ) Rerouter_Adapter(_l1Adapter, _l2Target, _spokePoolChainId) {} receive() external payable {} } @@ -33,8 +37,8 @@ contract Token_ERC20 is ERC20 { } } -contract ArbitrumL3AdapterTest is Test { - Rerouter_Adapter l3Adapter; +contract RerouterAdapterTest is Test { + Rerouter_Adapter rerouterAdapter; Optimism_Adapter optimismAdapter; Token_ERC20 l1Token; @@ -64,7 +68,7 @@ contract ArbitrumL3AdapterTest is Test { IERC20(address(0)), ITokenMessenger(address(0)) ); - l3Adapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target); + rerouterAdapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target, 100); } // Messages should be indiscriminately sent to the l2Forwarder. @@ -72,7 +76,7 @@ contract ArbitrumL3AdapterTest is Test { vm.assume(target != l2Target); vm.expectEmit(address(crossDomainMessenger)); emit MockBedrockCrossDomainMessenger.MessageSent(l2Target); - l3Adapter.relayMessage(target, message); + rerouterAdapter.relayMessage(target, message); } // Sending Weth should call depositETHTo(). @@ -80,22 +84,22 @@ contract ArbitrumL3AdapterTest is Test { // Prevent fuzz testing with amountToSend * 2 > 2^256 amountToSend = uint256(bound(amountToSend, 1, 2**254)); vm.deal(address(l1Weth), amountToSend); - vm.deal(address(l3Adapter), amountToSend); + vm.deal(address(rerouterAdapter), amountToSend); - vm.startPrank(address(l3Adapter)); + vm.startPrank(address(rerouterAdapter)); l1Weth.deposit{ value: amountToSend }(); vm.stopPrank(); assertEq(amountToSend * 2, l1Weth.totalSupply()); vm.expectEmit(address(standardBridge)); emit MockBedrockL1StandardBridge.ETHDepositInitiated(l2Target, amountToSend); - l3Adapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); - assertEq(0, l1Weth.balanceOf(address(l3Adapter))); + rerouterAdapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); + assertEq(0, l1Weth.balanceOf(address(rerouterAdapter))); } // Sending any random token should call depositERC20To(). function testRelayToken(uint256 amountToSend, address random) public { - l1Token.mint(address(l3Adapter), amountToSend); + l1Token.mint(address(rerouterAdapter), amountToSend); assertEq(amountToSend, l1Token.totalSupply()); vm.expectEmit(address(standardBridge)); @@ -105,7 +109,7 @@ contract ArbitrumL3AdapterTest is Test { address(l2Token), amountToSend ); - l3Adapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); - assertEq(0, l1Token.balanceOf(address(l3Adapter))); + rerouterAdapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); + assertEq(0, l1Token.balanceOf(address(rerouterAdapter))); } } From 1339b5f42e6379a2c96945cfa471a2958ed35e65 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 10:28:56 -0500 Subject: [PATCH 27/45] remove dependency on chain id Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 28 +++++-------------- test/evm/foundry/local/Rerouter_Adapter.t.sol | 8 ++---- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 35842e1d..0b984aa6 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -25,7 +25,6 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; contract Rerouter_Adapter is AdapterInterface { address public immutable l1Adapter; address public immutable l2Target; - uint256 public immutable spokePoolChainId; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); @@ -36,16 +35,10 @@ contract Rerouter_Adapter is AdapterInterface { * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. * @param _l2Target Address of the L2 contract which receives the token and message relays. - * @param _spokePoolChainId Chain ID of the network which contains this adapter's corresponding spoke pool deployment. */ - constructor( - address _l1Adapter, - address _l2Target, - uint256 _spokePoolChainId - ) { + constructor(address _l1Adapter, address _l2Target) { l1Adapter = _l1Adapter; l2Target = _l2Target; - spokePoolChainId = _spokePoolChainId; } /** @@ -54,16 +47,12 @@ contract Rerouter_Adapter is AdapterInterface { * @param message Data to send to `target`. * @dev The message passed into this function is wrapped into a `relayMessage` function call, which is then passed * to L2. The l2Target contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's - * `relayMessage` call will be these target and message values. When sending a message, `spokePoolChainId` is abi-encoded - * to provide the forwarder with the necessary information to determine subsequent bridge routes. + * `relayMessage` call will be these target and message values. */ function relayMessage(address target, bytes memory message) external payable override { - bytes memory updatedMessage = abi.encode( - spokePoolChainId, - abi.encodeCall(AdapterInterface.relayMessage, (target, message)) - ); + bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); (bool success, ) = l1Adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (l2Target, updatedMessage)) + abi.encodeCall(AdapterInterface.relayMessage, (l2Target, wrappedMessage)) ); if (!success) revert RelayMessageFailed(); } @@ -74,8 +63,8 @@ contract Rerouter_Adapter is AdapterInterface { * @param l2Token L2 token to receive. * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. * @param target The address of the spoke pool which should ultimately receive `amount` of `l1Token`. - * @dev When sending a message, `spokePoolChainId` is abi-encoded to provide the forwarder with the necessary information - * to determine subsequent bridge routes. + * @dev When sending tokens, we follow-up with a message describing the amount of tokens we wish to continue bridging. + * This allows forwarders to know how much of some token to allocate to a certain target. */ function relayTokens( address l1Token, @@ -90,10 +79,7 @@ contract Rerouter_Adapter is AdapterInterface { if (!success) revert RelayTokensFailed(l1Token); // Follow-up token relay with a message to continue the token relay on L2. - bytes memory message = abi.encode( - spokePoolChainId, - abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, target)) - ); + bytes memory message = abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, target)); (success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message))); if (!success) revert RelayMessageFailed(); } diff --git a/test/evm/foundry/local/Rerouter_Adapter.t.sol b/test/evm/foundry/local/Rerouter_Adapter.t.sol index e55b51b9..e355312d 100644 --- a/test/evm/foundry/local/Rerouter_Adapter.t.sol +++ b/test/evm/foundry/local/Rerouter_Adapter.t.sol @@ -16,11 +16,7 @@ import { MockBedrockL1StandardBridge, MockBedrockCrossDomainMessenger } from ".. // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). contract Mock_Rerouter_Adapter is Rerouter_Adapter { - constructor( - address _l1Adapter, - address _l2Target, - uint256 _spokePoolChainId - ) Rerouter_Adapter(_l1Adapter, _l2Target, _spokePoolChainId) {} + constructor(address _l1Adapter, address _l2Target) Rerouter_Adapter(_l1Adapter, _l2Target) {} receive() external payable {} } @@ -68,7 +64,7 @@ contract RerouterAdapterTest is Test { IERC20(address(0)), ITokenMessenger(address(0)) ); - rerouterAdapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target, 100); + rerouterAdapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target); } // Messages should be indiscriminately sent to the l2Forwarder. From c3950acbdd5a3edb2316f182e5c1516b0c04d182 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 11:54:29 -0500 Subject: [PATCH 28/45] new recursive format Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 115 ++++++++++++++++++--- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 5b64fa68..90bb63bb 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -15,11 +15,25 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; * @custom:security-contact bugs@across.to */ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { - // L3 address of the recipient of L1 messages and tokens sent via the re-router adapter on L1. - address public l3SpokePool; + /** + * @notice Struct containing all information needed in order to send a message to the next layer. + * @dev We need to know three different addresses: + * - The address of the adapter to delegatecall, so that we may propagate message and token relays. + * - The address of the next layer's forwarder, so that we can identify the intended recipient of message and token relays. + * - The address of the next layer's spoke pool, so that we can determine whether the next chain contains the target spoke pool. + */ + struct RouteIndex { + address adapter; + address forwarder; + address spokePool; + } // L1 address of the contract which can relay messages to the l3SpokePool contract and update this proxy contract. address public crossDomainAdmin; + // Map from a destination address to the next token/message bridge route to reach the destination address. The destination + // address can either be a spoke pool or a forwarder. This mapping requires that no destination addresses (i.e. spoke pool or + // forwarder addresses) collide. + mapping(address => RouteIndex) availableRoutes; event TokensForwarded(address indexed l2Token, uint256 amount); event MessageForwarded(address indexed target, bytes message); @@ -28,6 +42,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { error InvalidCrossDomainAdmin(); error InvalidL3SpokePool(); + error RelayMessageFailed(); + error RelayTokensFailed(address baseToken); /* * @dev All functions with this modifier must revert if msg.sender != crossDomainAdmin. Each L2 may have @@ -49,14 +65,11 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Initializes the forwarder contract. - * @param _l3SpokePool L3 address of the contract which will receive messages and tokens which are temporarily - * stored in this contract on L2. * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. * In practice, this is the hub pool. */ - function __Forwarder_init(address _l3SpokePool, address _crossDomainAdmin) public onlyInitializing { + function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { __UUPSUpgradeable_init(); - _setL3SpokePool(_l3SpokePool); _setCrossDomainAdmin(_crossDomainAdmin); } @@ -69,11 +82,87 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { } /** - * @notice Sets a new spoke pool address. - * @param _newL3SpokePool L3 address of the new spoke pool contract. + * @notice Sets a new RouteIndex to a specified address. + * @param _destinationAddress The address to set in the availableRoutes mapping. + * @param _routeIndex RouteIndex struct containing the next immediate path to reach the _destination address. + * @dev Each forwarder will not know how many layers it must traverse to arrive at _destinationAddress until it is precisely + * one layer away. The availableRoutes mapping lets the forwarder know what path it should take to progess, and then relies on + * the forwarders on the subsequent layers to derive the next route in sequence. + */ + function updateRoute(address _destinationAddress, RouteIndex memory _routeIndex) external onlyAdmin { + availableRoutes[_destinationAddress] = _routeIndex; + } + + /** + * @notice Relays a specified message to a target on an arbitrary layer. + * @param target The address of the contract that will receive the input message. + * @param message The data to execute on the contract. + * @dev Each forwarder will only know if they are on the layer directly before the layer which contains the `target` contract. + * If the next layer contains the target, we send `message` cross-chain normally. Otherwise, we wrap `message` into a `relayMessage` + * call to the next layer's forwarder contract. */ - function setL3SpokePool(address _newL3SpokePool) external onlyAdmin { - _setL3SpokePool(_newL3SpokePool); + function relayMessage(address target, bytes memory message) external payable override onlyAdmin { + RouteIndex storage route = availableRoutes[target]; + bool success; + // Case 1: We are on the network immediately before our target, so we forward the original message over the canonical messaging bridge. + if (target == route.spokePool || target == route.forwarder) { + (success, ) = route.adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); + } else { + // Case 2: We are not on the network immediately before our target, so we wrap the message in a `relayMessage` call and forward it + // to the next forwarder on the path to the target contract. + bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); + (success, ) = route.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (route.forwarder, wrappedMessage)) + ); + } + if (!success) revert RelayMessageFailed(); + } + + /** + * @notice Relays `amount` of a token to `target` over an arbitrary number of layers. + * @param baseToken This layer's address of the token to send. + * @param remoteToken The next layer's address of the token to send. + * @param amount The amount of the token to send. + * @param target The address of the contract that will receive the tokens. + * @dev The relayTokens function (which must be implemented on a per-chain basis) needs to inductively determine the address of the + * remote token given a known baseToken. Then, once the correct addresses are obtained, this function may be called to finish the + * relay of tokens. + */ + function _relayTokens( + address baseToken, + address remoteToken, + uint256 amount, + address target + ) internal { + RouteIndex storage route = availableRoutes[target]; + bool success; + // Case 1: We are immediately before the target spoke pool, so we send the tokens to the spoke pool + // and do NOT follow it up with a message. + if (target == route.spokePool) { + (success, ) = route.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (baseToken, remoteToken, amount, target)) + ); + if (!success) revert RelayTokensFailed(baseToken); + } else { + // Case 2: We are not immediately before the target spoke pool, so we send the tokens to the next + // forwarder and accompany it with a message containing information about its intended destination. + + // Send tokens to the forwarder. + (success, ) = route.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (baseToken, remoteToken, amount, route.forwarder)) + ); + if (!success) revert RelayTokensFailed(baseToken); + + // Send a follow-up message to the forwarder which tells it to continue bridging. + bytes memory message = abi.encodeCall( + AdapterInterface.relayTokens, + (baseToken, remoteToken, amount, target) + ); + (success, ) = route.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (route.forwarder, message)) + ); + if (!success) revert RelayMessageFailed(); + } } // Function to be overridden to accomodate for each L2's unique method of address aliasing. @@ -87,10 +176,4 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { crossDomainAdmin = _newCrossDomainAdmin; emit SetXDomainAdmin(_newCrossDomainAdmin); } - - function _setL3SpokePool(address _newL3SpokePool) internal { - if (_newL3SpokePool == address(0)) revert InvalidL3SpokePool(); - l3SpokePool = _newL3SpokePool; - emit SetL3SpokePool(_newL3SpokePool); - } } From 2aac0f7b92168def160098cec2020ae7b2c063a6 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 12:36:10 -0500 Subject: [PATCH 29/45] use events Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 90bb63bb..f232cf92 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -35,10 +35,10 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // forwarder addresses) collide. mapping(address => RouteIndex) availableRoutes; - event TokensForwarded(address indexed l2Token, uint256 amount); + event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); event MessageForwarded(address indexed target, bytes message); event SetXDomainAdmin(address indexed crossDomainAdmin); - event SetL3SpokePool(address indexed l3SpokePool); + event RouteUpdated(address indexed target, RouteIndex route); error InvalidCrossDomainAdmin(); error InvalidL3SpokePool(); @@ -91,6 +91,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { */ function updateRoute(address _destinationAddress, RouteIndex memory _routeIndex) external onlyAdmin { availableRoutes[_destinationAddress] = _routeIndex; + emit RouteUpdated(_destinationAddress, _routeIndex); } /** @@ -116,6 +117,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { ); } if (!success) revert RelayMessageFailed(); + emit MessageForwarded(target, message); } /** @@ -163,6 +165,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { ); if (!success) revert RelayMessageFailed(); } + emit TokensForwarded(target, baseToken, amount); } // Function to be overridden to accomodate for each L2's unique method of address aliasing. From 06c8877e56afaaac5532a7800d67c6411317ac92 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 13:36:51 -0500 Subject: [PATCH 30/45] add remote token mapping and update comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index f232cf92..7bfc737b 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -6,12 +6,12 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @title ForwarderBase - * @notice This contract expects to receive messages and tokens from the Hub Pool on L1 and forwards them to targets on L3. Messages - * are intended to originate from the re-router adapter on L1 which will send messages here to re-route them to the corresponding L3 - * spoke pool. It rejects messages which do not originate from the cross domain admin, which should be set to the hub pool. - * @dev Base contract designed to be deployed on L2 to re-route messages from L1 to L3. If a message is sent to this contract which - * came from the L1 cross domain admin, then it is routed to the L3 via the canonical messaging bridge. Tokens sent from L1 are stored - * temporarily on this contract. Any EOA can initiate a bridge of these tokens to the target `l3SpokePool`. + * @notice This contract expects to receive messages and tokens from an authorized sender on a previous layer and forwards messages and tokens + * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. It rejects messages which + * do not originate from the cross domain admin, which should be set to these contracts, depending on the sender on the previous layer. + * @dev Base contract designed to be deployed on a layer > L1 to re-route messages to a further layer. If a message is sent to this contract which + * came from the cross domain admin, then it is routed to the next layer via the canonical messaging bridge. Tokens sent from L1 are accompanied by a + * message directing this contract on the next steps of the token relay. * @custom:security-contact bugs@across.to */ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { @@ -34,11 +34,14 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // address can either be a spoke pool or a forwarder. This mapping requires that no destination addresses (i.e. spoke pool or // forwarder addresses) collide. mapping(address => RouteIndex) availableRoutes; + // Map which takes inputs the destination address and a token address on the current network and outputs the corresponding remote token address. + mapping(address => mapping(address => address)) remoteTokens; event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); event MessageForwarded(address indexed target, bytes message); event SetXDomainAdmin(address indexed crossDomainAdmin); event RouteUpdated(address indexed target, RouteIndex route); + event RemoteTokensUpdated(address indexed target, address indexed baseToken, address indexed remoteToken); error InvalidCrossDomainAdmin(); error InvalidL3SpokePool(); @@ -55,8 +58,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { } /** - @notice Constructs the Forwarder contract. - @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, + * @notice Constructs the Forwarder contract. + * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, * may disrupt the proxy if another EOA were to initialize it. */ constructor() { @@ -94,6 +97,23 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { emit RouteUpdated(_destinationAddress, _routeIndex); } + /** + * @notice Sets a new remote token in the remoteTokens mapping. + * @param _destinationAddress The address to set in the remoteTokens mapping. This is to identify the network associated with the + * remote token. + * @param _baseToken The address of the token which exists on the current network. + * @param _remoteToken The address of the token on the network which is next on the path to _destinationAddress. + * @dev This mapping also relies on using _destinationAddress to determine the network to bridge to. + */ + function updateRemoteToken( + address _destinationAddress, + address _baseToken, + address _remoteToken + ) external onlyAdmin { + remoteTokens[_destinationAddress][_baseToken] = _remoteToken; + emit RemoteTokensUpdated(_destinationAddress, _baseToken, _remoteToken); + } + /** * @notice Relays a specified message to a target on an arbitrary layer. * @param target The address of the contract that will receive the input message. From 77bc3d542945a25b9703af62c34976b9225ff5d2 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 15:02:14 -0500 Subject: [PATCH 31/45] remove unused file Signed-off-by: bennett --- .../interfaces/ArbitrumBridgeInterfaces.sol | 220 ------------------ 1 file changed, 220 deletions(-) delete mode 100644 contracts/interfaces/ArbitrumBridgeInterfaces.sol diff --git a/contracts/interfaces/ArbitrumBridgeInterfaces.sol b/contracts/interfaces/ArbitrumBridgeInterfaces.sol deleted file mode 100644 index a2b31724..00000000 --- a/contracts/interfaces/ArbitrumBridgeInterfaces.sol +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title Staging ground for incoming and outgoing messages - * @notice Unlike the standard Eth bridge, native token bridge escrows the custom ERC20 token which is - * used as native currency on upper layer. - * @dev Fees are paid in this token. There are certain restrictions on the native token: - * - The token can't be rebasing or have a transfer fee - * - The token must only be transferrable via a call to the token address itself - * - The token must only be able to set allowance via a call to the token address itself - * - The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert - * - The token must have a max of 2^256 - 1 wei total supply unscaled - * - The token must have a max of 2^256 - 1 wei total supply when scaled to 18 decimals - */ -interface ArbitrumERC20Bridge { - /** - * @notice Returns token that is escrowed in bridge on the lower layer and minted on the upper layer as native currency. - * @dev This function doesn't exist on the generic Bridge interface. - * @return address of the native token. - */ - function nativeToken() external view returns (address); - - /** - * @dev number of decimals used by the native token - * This is set on bridge initialization using nativeToken.decimals() - * If the token does not have decimals() method, we assume it have 0 decimals - */ - function nativeTokenDecimals() external view returns (uint8); -} - -/** - * @title Inbox for user and contract originated messages - * @notice Messages created via this inbox are enqueued in the delayed accumulator - * to await inclusion in the SequencerInbox - */ -interface ArbitrumInboxLike { - /** - * @dev we only use this function to check the native token used by the bridge, so we hardcode the interface - * to return an ArbitrumERC20Bridge instead of a more generic Bridge interface. - * @return address of the bridge. - */ - function bridge() external view returns (ArbitrumERC20Bridge); - - /** - * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev Caller must set msg.value equal to at least `maxSubmissionCost + maxGas * gasPriceBid`. - * all msg.value will deposited to callValueRefundAddress on the upper layer - * @dev More details can be found here: https://developer.arbitrum.io/arbos/l1-to-l2-messaging - * @param to destination contract address - * @param callValue call value for retryable message - * @param maxSubmissionCost Max gas deducted from user's (upper layer) balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on (upper layer) balance - * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's upper layer balance to cover upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for upper layer execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 callValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the source chain inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed - * funds come from the deposit alone, rather than falling back on the user's balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination contract address - * @param callValue call value for retryable message - * @param maxSubmissionCost Max gas deducted from user's source chain balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on source chain balance - * @param callValueRefundAddress callvalue gets credited here on source chain if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of the message - * @return unique message number of the retryable transaction - */ - function unsafeCreateRetryableTicket( - address to, - uint256 callValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); -} - -/** - * @notice Interface which extends ArbitrumInboxLike with functions used to interact with bridges that use a custom gas token. - */ -interface ArbitrumCustomGasTokenInbox is ArbitrumInboxLike { - /** - * @notice Put a message in the inbox that can be reexecuted for some fixed amount of time if it reverts - * @notice Overloads the `createRetryableTicket` function but is not payable, and should only be called when paying - * for message using a custom gas token. - * @dev all tokenTotalFeeAmount will be deposited to callValueRefundAddress on upper layer - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @dev In case of native token having non-18 decimals: tokenTotalFeeAmount is denominated in native token's decimals. All other value params - callValue, maxSubmissionCost and maxFeePerGas are denominated in child chain's native 18 decimals. - * @param to destination contract address - * @param callValue call value for retryable message - * @param maxSubmissionCost Max gas deducted from user's upper layer balance to cover base submission fee - * @param excessFeeRefundAddress the address which receives the difference between execution fee paid and the actual execution cost. In case this address is a contract, funds will be received in its alias on upper layer. - * @param callValueRefundAddress callvalue gets credited here on upper layer if retryable txn times out or gets cancelled. In case this address is a contract, funds will be received in its alias on upper layer. - * @param gasLimit Max gas deducted from user's balance to cover execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param tokenTotalFeeAmount amount of fees to be deposited in native token to cover for retryable ticket cost - * @param data ABI encoded data of message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 callValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 tokenTotalFeeAmount, - bytes calldata data - ) external returns (uint256); -} - -/** - * @notice Generic gateway contract for bridging standard ERC20s to/from Arbitrum-like networks. - * @notice These function signatures are shared between the L1 and L2 gateway router contracts. - */ -interface ArbitrumL1ERC20GatewayLike { - /** - * @notice Deprecated in favor of outboundTransferCustomRefund but still used in custom bridges - * like the DAI bridge. - * @dev Refunded to aliased address of sender if sender has code on source chain, otherwise to to sender's EOA on destination chain. - * @param _sourceToken address of ERC20 - * @param _to Account to be credited with the tokens at the destination (can be the user's account or a contract), - * not subject to aliasing. This account, or its alias if it has code in the source chain, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's balance to cover execution - * @param _gasPriceBid Gas price for execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransfer( - address _sourceToken, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - /** - * @notice get ERC20 gateway for token. - * @param _token ERC20 address. - * @return address of ERC20 gateway. - */ - function getGateway(address _token) external view returns (address); - - /** - * @notice Deposit ERC20 token from Ethereum into Arbitrum-like networks. - * @dev Upper layer address alias will not be applied to the following types of addresses on lower layer: - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * @param _sourceToken address of ERC20 on source chain. - * @param _refundTo Account, or its alias if it has code on the source chain, to be credited with excess gas refund at destination - * @param _to Account to be credited with the tokens in the L3 (can be the user's L3 account or a contract), - * not subject to aliasing. This account, or its alias if it has code on the source chain, will also be able to - * cancel the retryable ticket and receive callvalue refund - * @param _amount Token Amount - * @param _maxGas Max gas deducted from user's balance to cover execution - * @param _gasPriceBid Gas price for execution - * @param _data encoded data from router and user - * @return res abi encoded inbox sequence number - */ - function outboundTransferCustomRefund( - address _sourceToken, - address _refundTo, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); -} - -interface ArbitrumL2ERC20GatewayLike { - /** - * @notice Fetches the l2 token address from the gateway router for the input l1 token address - * @param _l1Erc20 address of the l1 token. - */ - function calculateL2TokenAddress(address _l1Erc20) external view returns (address); - - /** - * @notice Withdraws a specified amount of an l2 token to an l1 token. - * @param _l1Token address of the token to withdraw on L1. - * @param _to address on L1 which will receive the tokens upon withdrawal. - * @param _amount amount of the token to withdraw. - * @param _data encoded data to send to the gateway router. - */ - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - bytes calldata _data - ) external payable returns (bytes memory); -} From 64d350383a805128c6ea79f53d74da99af1eaf7f Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 27 Sep 2024 15:50:46 -0500 Subject: [PATCH 32/45] add more checks and improve comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 26 +++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 7bfc737b..4ad57964 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -7,8 +7,13 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @title ForwarderBase * @notice This contract expects to receive messages and tokens from an authorized sender on a previous layer and forwards messages and tokens - * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. It rejects messages which - * do not originate from the cross domain admin, which should be set to these contracts, depending on the sender on the previous layer. + * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. + * @dev This contract rejects messages which do not originate from the cross domain admin, which should be set to these contracts, depending on the + * sender on the previous layer. Additionally, this contract only knows information about networks which roll up their state to the network on which + * this contract is deployed. Messages continue to be forwarded across an indefinite amount of layers by recursively wrapping and sending messages + * over canonical bridges, only stopping when the following network contains the intended target contract. + * @dev Since these contracts use destination addresses to derive a route to subsequent networks, it requires that all of these forwarder contracts + * AND spoke pool contracts which require forwarders to receive messages to not have same addresses. * @dev Base contract designed to be deployed on a layer > L1 to re-route messages to a further layer. If a message is sent to this contract which * came from the cross domain admin, then it is routed to the next layer via the canonical messaging bridge. Tokens sent from L1 are accompanied by a * message directing this contract on the next steps of the token relay. @@ -19,8 +24,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { * @notice Struct containing all information needed in order to send a message to the next layer. * @dev We need to know three different addresses: * - The address of the adapter to delegatecall, so that we may propagate message and token relays. - * - The address of the next layer's forwarder, so that we can identify the intended recipient of message and token relays. - * - The address of the next layer's spoke pool, so that we can determine whether the next chain contains the target spoke pool. + * - The address of the next layer's forwarder, so that we can continue the message and token relay chain. + * - The address of the next layer's spoke pool, so that we can determine whether the following network contains the target spoke pool. */ struct RouteIndex { address adapter; @@ -28,13 +33,14 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address spokePool; } - // L1 address of the contract which can relay messages to the l3SpokePool contract and update this proxy contract. + // Address of the contract which can relay messages to the subsequent forwarders/spoke pools and update this proxy contract. address public crossDomainAdmin; // Map from a destination address to the next token/message bridge route to reach the destination address. The destination // address can either be a spoke pool or a forwarder. This mapping requires that no destination addresses (i.e. spoke pool or // forwarder addresses) collide. mapping(address => RouteIndex) availableRoutes; // Map which takes inputs the destination address and a token address on the current network and outputs the corresponding remote token address. + // This also requires that no destination addresses collide. mapping(address => mapping(address => address)) remoteTokens; event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); @@ -47,6 +53,10 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { error InvalidL3SpokePool(); error RelayMessageFailed(); error RelayTokensFailed(address baseToken); + // Error which is triggered when there is no adapter set for a given route. + error UninitializedRoute(); + // Error which is triggered when we must send tokens or a message relay to a forwarder which is uninitialized. + error MalformedRoute(); /* * @dev All functions with this modifier must revert if msg.sender != crossDomainAdmin. Each L2 may have @@ -69,7 +79,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Initializes the forwarder contract. * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. - * In practice, this is the hub pool. + * In practice, this is the hub pool for forwarders deployed on L2, and other forwarder contracts on layers != L2. */ function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { __UUPSUpgradeable_init(); @@ -124,11 +134,13 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { */ function relayMessage(address target, bytes memory message) external payable override onlyAdmin { RouteIndex storage route = availableRoutes[target]; + if (route.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are on the network immediately before our target, so we forward the original message over the canonical messaging bridge. if (target == route.spokePool || target == route.forwarder) { (success, ) = route.adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); } else { + if (route.forwarder == address(0)) revert MalformedRoute(); // Case 2: We are not on the network immediately before our target, so we wrap the message in a `relayMessage` call and forward it // to the next forwarder on the path to the target contract. bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); @@ -157,6 +169,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address target ) internal { RouteIndex storage route = availableRoutes[target]; + if (route.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are immediately before the target spoke pool, so we send the tokens to the spoke pool // and do NOT follow it up with a message. @@ -169,6 +182,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // Case 2: We are not immediately before the target spoke pool, so we send the tokens to the next // forwarder and accompany it with a message containing information about its intended destination. + if (route.forwarder == address(0)) revert MalformedRoute(); // Send tokens to the forwarder. (success, ) = route.adapter.delegatecall( abi.encodeCall(AdapterInterface.relayTokens, (baseToken, remoteToken, amount, route.forwarder)) From 1274920a219caf9f9fc492f6dac6e067a979c67d Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 30 Sep 2024 10:09:42 -0500 Subject: [PATCH 33/45] rename immutable vars Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index c5d62476..7e1893cf 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -23,8 +23,8 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; // solhint-disable-next-line contract-name-camelcase contract Rerouter_Adapter is AdapterInterface { - address public immutable l1Adapter; - address public immutable l2Target; + address public immutable L1_ADAPTER; + address public immutable L2_TARGET; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); @@ -37,8 +37,8 @@ contract Rerouter_Adapter is AdapterInterface { * @param _l2Target Address of the L2 contract which receives the token and message relays. */ constructor(address _l1Adapter, address _l2Target) { - l1Adapter = _l1Adapter; - l2Target = _l2Target; + L1_ADAPTER = _l1Adapter; + L2_TARGET = _l2Target; } /** @@ -52,8 +52,8 @@ contract Rerouter_Adapter is AdapterInterface { */ function relayMessage(address target, bytes memory message) external payable override { bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); - (bool success, ) = l1Adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (l2Target, wrappedMessage)) + (bool success, ) = L1_ADAPTER.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (L2_TARGET, wrappedMessage)) ); if (!success) revert RelayMessageFailed(); } @@ -74,14 +74,14 @@ contract Rerouter_Adapter is AdapterInterface { address target ) external payable override { // Relay tokens to the forwarder. - (bool success, ) = l1Adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Target)) + (bool success, ) = L1_ADAPTER.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, L2_TARGET)) ); if (!success) revert RelayTokensFailed(l1Token); // Follow-up token relay with a message to continue the token relay on L2. bytes memory message = abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, target)); - (success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message))); + (success, ) = L1_ADAPTER.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (L2_TARGET, message))); if (!success) revert RelayMessageFailed(); } } From d33099343f18b48c9db005412529ced8980b7c86 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 30 Sep 2024 12:20:46 -0500 Subject: [PATCH 34/45] naming Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 67 +++++++++++++--------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 4ad57964..af9dff1d 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -26,8 +26,11 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { * - The address of the adapter to delegatecall, so that we may propagate message and token relays. * - The address of the next layer's forwarder, so that we can continue the message and token relay chain. * - The address of the next layer's spoke pool, so that we can determine whether the following network contains the target spoke pool. + * these addresses describe the path a message or token relay must take to arrive at the next layer. A Route only describes the path to arrive + * a layer closer to to the ultimate target, unless one of the `forwarder` or `spokePool` addresses equal the target contract, in which case + * the path ends. */ - struct RouteIndex { + struct Route { address adapter; address forwarder; address spokePool; @@ -37,17 +40,21 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address public crossDomainAdmin; // Map from a destination address to the next token/message bridge route to reach the destination address. The destination // address can either be a spoke pool or a forwarder. This mapping requires that no destination addresses (i.e. spoke pool or - // forwarder addresses) collide. - mapping(address => RouteIndex) availableRoutes; + // forwarder addresses) collide. We cannot use the destination chain ID as a key here since we do not have that information. + mapping(address => Route) possibleRoutes; // Map which takes inputs the destination address and a token address on the current network and outputs the corresponding remote token address. // This also requires that no destination addresses collide. - mapping(address => mapping(address => address)) remoteTokens; + mapping(address => mapping(address => address)) destinationChainTokens; event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); event MessageForwarded(address indexed target, bytes message); event SetXDomainAdmin(address indexed crossDomainAdmin); - event RouteUpdated(address indexed target, RouteIndex route); - event RemoteTokensUpdated(address indexed target, address indexed baseToken, address indexed remoteToken); + event RouteUpdated(address indexed target, Route route); + event DestinationChainTokensUpdated( + address indexed target, + address indexed baseToken, + address indexed destinationChainToken + ); error InvalidCrossDomainAdmin(); error InvalidL3SpokePool(); @@ -95,37 +102,38 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { } /** - * @notice Sets a new RouteIndex to a specified address. - * @param _destinationAddress The address to set in the availableRoutes mapping. - * @param _routeIndex RouteIndex struct containing the next immediate path to reach the _destination address. + * @notice Sets a new Route to a specified address. + * @param _destinationAddress The address to set in the possibleRoutes mapping. + * @param _route Route struct containing the next immediate path to reach the _destination address. * @dev Each forwarder will not know how many layers it must traverse to arrive at _destinationAddress until it is precisely - * one layer away. The availableRoutes mapping lets the forwarder know what path it should take to progess, and then relies on + * one layer away. The possibleRoutes mapping lets the forwarder know what path it should take to progess, and then relies on * the forwarders on the subsequent layers to derive the next route in sequence. */ - function updateRoute(address _destinationAddress, RouteIndex memory _routeIndex) external onlyAdmin { - availableRoutes[_destinationAddress] = _routeIndex; - emit RouteUpdated(_destinationAddress, _routeIndex); + function updateRoute(address _destinationAddress, Route memory _route) external onlyAdmin { + possibleRoutes[_destinationAddress] = _route; + emit RouteUpdated(_destinationAddress, _route); } /** - * @notice Sets a new remote token in the remoteTokens mapping. - * @param _destinationAddress The address to set in the remoteTokens mapping. This is to identify the network associated with the - * remote token. + * @notice Sets a new remote token in the destinationChainTokens mapping. + * @param _destinationAddress The address to set in the destinationChainTokens mapping. This is to identify the network associated with the + * destination chain token. * @param _baseToken The address of the token which exists on the current network. - * @param _remoteToken The address of the token on the network which is next on the path to _destinationAddress. + * @param _destinationChainToken The address of the token on the network which is next on the path to _destinationAddress. * @dev This mapping also relies on using _destinationAddress to determine the network to bridge to. */ function updateRemoteToken( address _destinationAddress, address _baseToken, - address _remoteToken + address _destinationChainToken ) external onlyAdmin { - remoteTokens[_destinationAddress][_baseToken] = _remoteToken; - emit RemoteTokensUpdated(_destinationAddress, _baseToken, _remoteToken); + destinationChainTokens[_destinationAddress][_baseToken] = _destinationChainToken; + emit DestinationChainTokensUpdated(_destinationAddress, _baseToken, _destinationChainToken); } /** - * @notice Relays a specified message to a target on an arbitrary layer. + * @notice Relays a specified message to a contract on the following layer. If `target` exists on the following layer, then `message` is sent + * to it. Otherwise, `message` is sent to the next layer's forwarder contract. * @param target The address of the contract that will receive the input message. * @param message The data to execute on the contract. * @dev Each forwarder will only know if they are on the layer directly before the layer which contains the `target` contract. @@ -133,7 +141,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { * call to the next layer's forwarder contract. */ function relayMessage(address target, bytes memory message) external payable override onlyAdmin { - RouteIndex storage route = availableRoutes[target]; + Route storage route = possibleRoutes[target]; if (route.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are on the network immediately before our target, so we forward the original message over the canonical messaging bridge. @@ -155,7 +163,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Relays `amount` of a token to `target` over an arbitrary number of layers. * @param baseToken This layer's address of the token to send. - * @param remoteToken The next layer's address of the token to send. + * @param destinationChainToken The next layer's address of the token to send. * @param amount The amount of the token to send. * @param target The address of the contract that will receive the tokens. * @dev The relayTokens function (which must be implemented on a per-chain basis) needs to inductively determine the address of the @@ -164,18 +172,18 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { */ function _relayTokens( address baseToken, - address remoteToken, + address destinationChainToken, uint256 amount, address target ) internal { - RouteIndex storage route = availableRoutes[target]; + Route storage route = possibleRoutes[target]; if (route.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are immediately before the target spoke pool, so we send the tokens to the spoke pool // and do NOT follow it up with a message. if (target == route.spokePool) { (success, ) = route.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayTokens, (baseToken, remoteToken, amount, target)) + abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target)) ); if (!success) revert RelayTokensFailed(baseToken); } else { @@ -185,14 +193,17 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { if (route.forwarder == address(0)) revert MalformedRoute(); // Send tokens to the forwarder. (success, ) = route.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayTokens, (baseToken, remoteToken, amount, route.forwarder)) + abi.encodeCall( + AdapterInterface.relayTokens, + (baseToken, destinationChainToken, amount, route.forwarder) + ) ); if (!success) revert RelayTokensFailed(baseToken); // Send a follow-up message to the forwarder which tells it to continue bridging. bytes memory message = abi.encodeCall( AdapterInterface.relayTokens, - (baseToken, remoteToken, amount, target) + (baseToken, destinationChainToken, amount, target) ); (success, ) = route.adapter.delegatecall( abi.encodeCall(AdapterInterface.relayMessage, (route.forwarder, message)) From 5271df886032bccb6bf4ea1f4fb5c8a4aba1d608 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 30 Sep 2024 12:53:25 -0500 Subject: [PATCH 35/45] update comments Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 7e1893cf..a4b56855 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** - * @notice Contract containing logic to send messages from L1 to a target on an arbitrary layer via re-routing messages - * through intermediate contracts. + * @notice Contract containing logic to send messages from L1 to non-spoke pool L2 targets via re-routing message recipients + * before submission. This ultimately enables sending messages to targets on arbitrary layers via forwarding through intermediate + * contracts. * @dev Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` * will be the remote spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this * contract cannot send anything directly to the this target, since it does not exist on L2. Instead, it "re-routes" @@ -31,7 +32,7 @@ contract Rerouter_Adapter is AdapterInterface { /** * @notice Constructs new Adapter for sending tokens/messages to an L2 target. This contract will - * re-route messages to the _l2Target via the _l1Adapter. + * re-route messages to L2_TARGET via the L1_ADAPTER contract. * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays. * @param _l2Target Address of the L2 contract which receives the token and message relays. @@ -42,13 +43,14 @@ contract Rerouter_Adapter is AdapterInterface { } /** - * @notice Send cross-chain message to a target on L2 which will re-route messages to the intended remote target. + * @notice Send cross-chain message to a target on L2 which will forward messages to the intended remote target. * @param target Address of the remote contract which receives `message` after it has been forwarded by all intermediate * contracts. * @param message Data to send to `target`. * @dev The message passed into this function is wrapped into a `relayMessage` function call, which is then passed - * to L2. The `l2Target` contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's - * `relayMessage` call will be these target and message values. + * to L2. The `L2_TARGET` contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's + * `relayMessage` call will be these `target` and `message` values. From there, the forwarder derives the next appropriate + * method to send `message` to the following layers. */ function relayMessage(address target, bytes memory message) external payable override { bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); @@ -59,11 +61,11 @@ contract Rerouter_Adapter is AdapterInterface { } /** - * @notice Bridge tokens to a target on L2. + * @notice Bridge tokens to a target on L2 and follow up the token bridge with a call to continue bridging the sent tokens. * @param l1Token L1 token to deposit. * @param l2Token L2 token to receive. * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. - * @param target The address of the spoke pool which should ultimately receive `amount` of `l1Token`. + * @param target The address of the contract which should ultimately receive `amount` of `l1Token`. * @dev When sending tokens, we follow-up with a message describing the amount of tokens we wish to continue bridging. * This allows forwarders to know how much of some token to allocate to a certain target. */ From 7aa7d11266a664414d079a6210c4649c81384e2c Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 30 Sep 2024 13:19:08 -0500 Subject: [PATCH 36/45] comments Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index af9dff1d..253c3227 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -8,12 +8,15 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; * @title ForwarderBase * @notice This contract expects to receive messages and tokens from an authorized sender on a previous layer and forwards messages and tokens * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. - * @dev This contract rejects messages which do not originate from the cross domain admin, which should be set to these contracts, depending on the - * sender on the previous layer. Additionally, this contract only knows information about networks which roll up their state to the network on which - * this contract is deployed. Messages continue to be forwarded across an indefinite amount of layers by recursively wrapping and sending messages - * over canonical bridges, only stopping when the following network contains the intended target contract. + * @dev This contract rejects messages which do not originate from the cross domain admin, which should be set to authorized contracts, such as the hub + * pool or other forwarder contracts, depending on the `msg.sender` on the previous layer. This contract only knows information about networks which roll + * up their state to the network on which this contract is deployed. Messages continue to be forwarded across an indefinite amount of layers by recursively + * wrapping and sending messages over canonical bridges, only stopping when the following network contains the intended target contract. * @dev Since these contracts use destination addresses to derive a route to subsequent networks, it requires that all of these forwarder contracts - * AND spoke pool contracts which require forwarders to receive messages to not have same addresses. + * AND spoke pool contracts which require forwarders to receive messages do not have same addresses. + * @dev Every contract which inherits ForwarderBase must implement two functions, which are both dependent on the architecture of the network on which the + * contract is deployed: `_requireAdminSender` should enforce that the `msg.sender` of calls to this contract are from the initialized `crossDomainAdmin`, and + * `relayTokens` should determine the address of the token to relay on the subsequent network, before calling `_relayTokens`. * @dev Base contract designed to be deployed on a layer > L1 to re-route messages to a further layer. If a message is sent to this contract which * came from the cross domain admin, then it is routed to the next layer via the canonical messaging bridge. Tokens sent from L1 are accompanied by a * message directing this contract on the next steps of the token relay. @@ -21,14 +24,17 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; */ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** - * @notice Struct containing all information needed in order to send a message to the next layer. + * @notice Route contains all information needed in order to send a message to the next layer. It describes two possible paths: a message is sent + * through `adapter` to the `forwarder`, or a message is sent through `adapter` to the `spokePool`. * @dev We need to know three different addresses: * - The address of the adapter to delegatecall, so that we may propagate message and token relays. * - The address of the next layer's forwarder, so that we can continue the message and token relay chain. * - The address of the next layer's spoke pool, so that we can determine whether the following network contains the target spoke pool. * these addresses describe the path a message or token relay must take to arrive at the next layer. A Route only describes the path to arrive * a layer closer to to the ultimate target, unless one of the `forwarder` or `spokePool` addresses equal the target contract, in which case - * the path ends. + * the path ends. All Routes must contain an adapter address; however, not all Routes must contain a forwarder or spokePool address. For example, + * we may send a message to a spoke pool on a network with no corresponding forwarder contract, or we may send a message to a network which contains + * a forwarder contract but no spoke pool. */ struct Route { address adapter; @@ -38,9 +44,10 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // Address of the contract which can relay messages to the subsequent forwarders/spoke pools and update this proxy contract. address public crossDomainAdmin; - // Map from a destination address to the next token/message bridge route to reach the destination address. The destination - // address can either be a spoke pool or a forwarder. This mapping requires that no destination addresses (i.e. spoke pool or - // forwarder addresses) collide. We cannot use the destination chain ID as a key here since we do not have that information. + // Map from a destination address to the next token/message bridge route to reach the destination address. The destination address can either be a + // spoke pool or a forwarder. + // This mapping requires that no destination addresses (i.e. spoke pool or forwarder addresses) collide. We cannot use the + // destination chain ID as a key here since we do not have that information. mapping(address => Route) possibleRoutes; // Map which takes inputs the destination address and a token address on the current network and outputs the corresponding remote token address. // This also requires that no destination addresses collide. @@ -62,7 +69,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { error RelayTokensFailed(address baseToken); // Error which is triggered when there is no adapter set for a given route. error UninitializedRoute(); - // Error which is triggered when we must send tokens or a message relay to a forwarder which is uninitialized. + // Error which is triggered when we attempted send tokens or a message relay to a forwarder which is uninitialized. error MalformedRoute(); /* @@ -120,7 +127,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { * destination chain token. * @param _baseToken The address of the token which exists on the current network. * @param _destinationChainToken The address of the token on the network which is next on the path to _destinationAddress. - * @dev This mapping also relies on using _destinationAddress to determine the network to bridge to. + * @dev This mapping also relies on using _destinationAddress to determine the network to bridge to. Consequently, it requires that a new + * _destinationAddress does not collide with an existing key in the `destinationChainTokens` mapping. */ function updateRemoteToken( address _destinationAddress, @@ -134,8 +142,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Relays a specified message to a contract on the following layer. If `target` exists on the following layer, then `message` is sent * to it. Otherwise, `message` is sent to the next layer's forwarder contract. - * @param target The address of the contract that will receive the input message. - * @param message The data to execute on the contract. + * @param target The address of the spoke pool or forwarder contract that will receive the input message. + * @param message The data to execute on the target contract. * @dev Each forwarder will only know if they are on the layer directly before the layer which contains the `target` contract. * If the next layer contains the target, we send `message` cross-chain normally. Otherwise, we wrap `message` into a `relayMessage` * call to the next layer's forwarder contract. @@ -161,14 +169,15 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { } /** - * @notice Relays `amount` of a token to `target` over an arbitrary number of layers. + * @notice Relays `amount` of a token to a contract on the following layer. If `target` does not exist on the following layer, then the tokens + * are sent to the following layer's forwarder contract. * @param baseToken This layer's address of the token to send. * @param destinationChainToken The next layer's address of the token to send. * @param amount The amount of the token to send. - * @param target The address of the contract that will receive the tokens. - * @dev The relayTokens function (which must be implemented on a per-chain basis) needs to inductively determine the address of the - * remote token given a known baseToken. Then, once the correct addresses are obtained, this function may be called to finish the - * relay of tokens. + * @param target The address of the contract that which will *ultimately* receive the tokens. That is, the spoke pool contract address on an + * arbitrary layer which receives `amount` of a token after all forwards have been completed. + * @dev The relayTokens function (which must be implemented on a per-chain basis) needs to inductively determine the address of the remote token + * given a known baseToken. Then, once the correct addresses are obtained, this function may be called to finish the relay of tokens. */ function _relayTokens( address baseToken, From 20c89a7e8e96e5ccfc22e362b35f176ab131c356 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:37:08 -0400 Subject: [PATCH 37/45] clean up comments --- contracts/chain-adapters/Rerouter_Adapter.sol | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index a4b56855..2a9f6318 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -4,17 +4,15 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** - * @notice Contract containing logic to send messages from L1 to non-spoke pool L2 targets via re-routing message recipients - * before submission. This ultimately enables sending messages to targets on arbitrary layers via forwarding through intermediate - * contracts. - * @dev Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens` - * will be the remote spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this - * contract cannot send anything directly to the this target, since it does not exist on L2. Instead, it "re-routes" - * messages to the remote network via intermediate forwarder contracts,beginning with an L2 forwarder, which is set as the - * `l2Target` in this contract. Each forwarder contract contains logic which determines the path a message or token relay - * must take to ultimately arrive at the spoke pool. There should be one of these adapters for each L3 spoke pool deployment. - * @dev All forwarder contracts, including `l2Target`, must implement ForwarderBase in order for tokens and messages to be - * automatically relayed to the subsequent layers. + * @notice Contract containing logic to send messages from L1 to "L3" networks that do not have direct connections + * with L1. L3's are defined as networks that connect to L1 indirectly via L2, and this contract sends + * messages to those L3's by rerouting them via those L2's. This contract is called a "Rerouter" because it uses + * (i.e. delegatecall's) existing L2 adapter logic to send a message first from L1 to L2 and then from L2 to L3. + * @dev Due to the constraints of the `SetCrossChainContracts` event as outlined in UMIP-157 and how the HubPool + * delegatecalls adapters like this one, all messages relayed through this + * adapter have target addresses on the L3's. However, these target addresses do not exist on L2 where all messages are + * rerouted through. Therefore, this contract is designed to be used in tandem with "L2 Forwarder Adapters" which help + * get the messages from L1 to L3 via L2's. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be * called via delegatecall, which will execute this contract's logic within the context of the originating contract. * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods @@ -24,18 +22,21 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; // solhint-disable-next-line contract-name-camelcase contract Rerouter_Adapter is AdapterInterface { + // Adapter designed to relay messages from L1 to L2 addresses and delegatecalled by this contract to reroute + // messages to L3 via the L2_TARGET. address public immutable L1_ADAPTER; + // L2_TARGET is a "Forwarder" contract that will help relay messages from L1 to L3. Messages are "rerouted" through + // the L2_TARGET. address public immutable L2_TARGET; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); /** - * @notice Constructs new Adapter for sending tokens/messages to an L2 target. This contract will - * re-route messages to L2_TARGET via the L1_ADAPTER contract. + * @notice Constructs new Adapter. This contract will re-route messages destined for an L3 to L2_TARGET via the L1_ADAPTER contract. * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers - * and token relays. - * @param _l2Target Address of the L2 contract which receives the token and message relays. + * and token relays to the L2 where _l2Target is deployed. + * @param _l2Target Address of the L2 contract which receives the token and message relays in order to forward them to an L3. */ constructor(address _l1Adapter, address _l2Target) { L1_ADAPTER = _l1Adapter; @@ -43,14 +44,14 @@ contract Rerouter_Adapter is AdapterInterface { } /** - * @notice Send cross-chain message to a target on L2 which will forward messages to the intended remote target. + * @notice Send cross-chain message to a target on L2 which will forward messages to the intended remote target on an L3. * @param target Address of the remote contract which receives `message` after it has been forwarded by all intermediate * contracts. * @param message Data to send to `target`. * @dev The message passed into this function is wrapped into a `relayMessage` function call, which is then passed * to L2. The `L2_TARGET` contract implements AdapterInterface, so upon arrival on L2, the arguments to the L2 contract's * `relayMessage` call will be these `target` and `message` values. From there, the forwarder derives the next appropriate - * method to send `message` to the following layers. + * method to send `message` to the following layers and ultimately to the target on L3. */ function relayMessage(address target, bytes memory message) external payable override { bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); From e8590d4a0bfdf52c814a7c3ebca4c3ff1485c4c9 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:13:46 -0400 Subject: [PATCH 38/45] Update ForwarderBase.sol --- contracts/chain-adapters/ForwarderBase.sol | 107 +++++++++------------ 1 file changed, 44 insertions(+), 63 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 253c3227..8d702477 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -7,27 +7,22 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @title ForwarderBase * @notice This contract expects to receive messages and tokens from an authorized sender on a previous layer and forwards messages and tokens - * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. - * @dev This contract rejects messages which do not originate from the cross domain admin, which should be set to authorized contracts, such as the hub - * pool or other forwarder contracts, depending on the `msg.sender` on the previous layer. This contract only knows information about networks which roll - * up their state to the network on which this contract is deployed. Messages continue to be forwarded across an indefinite amount of layers by recursively - * wrapping and sending messages over canonical bridges, only stopping when the following network contains the intended target contract. - * @dev Since these contracts use destination addresses to derive a route to subsequent networks, it requires that all of these forwarder contracts - * AND spoke pool contracts which require forwarders to receive messages do not have same addresses. - * @dev Every contract which inherits ForwarderBase must implement two functions, which are both dependent on the architecture of the network on which the - * contract is deployed: `_requireAdminSender` should enforce that the `msg.sender` of calls to this contract are from the initialized `crossDomainAdmin`, and - * `relayTokens` should determine the address of the token to relay on the subsequent network, before calling `_relayTokens`. - * @dev Base contract designed to be deployed on a layer > L1 to re-route messages to a further layer. If a message is sent to this contract which - * came from the cross domain admin, then it is routed to the next layer via the canonical messaging bridge. Tokens sent from L1 are accompanied by a - * message directing this contract on the next steps of the token relay. + * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. The motivating use case + * for this contract is to aid with sending messages from L1 to an L3, which by definition is a network which does not have a direct connection + * with L1 but instead must communicate with that L1 via an L2. Each contract that extends the ForwarderBase maintains a mapping of + * contracts that exist on networks that are connected to the network that this contract is deployed on. For example, if this contract is deployed on + * Arbitrum, then there could be a mapping of contracts for each L3 that is connected to Arbitrum and communicates to L1 via Arbitrum. In other words, + * this contract maintains a mapping of important contracts on the "next layer" and is used to help relay messages from L1 to L3 by sending messages + * through the next layer. Messages could be forwarded across an indefinite amount of layers by recursively wrapping and sending messages over canonical + * bridges, only stopping when the following network contains the intended target contract. * @custom:security-contact bugs@across.to */ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Route contains all information needed in order to send a message to the next layer. It describes two possible paths: a message is sent - * through `adapter` to the `forwarder`, or a message is sent through `adapter` to the `spokePool`. + * through `adapter` (on this network) to the `forwarder` on the next layer, or a message is sent through `adapter` to the `spokePool`. * @dev We need to know three different addresses: - * - The address of the adapter to delegatecall, so that we may propagate message and token relays. + * - The address of the adapter to delegatecall, so that we may propagate message and token relays. This contract lives on this layer. * - The address of the next layer's forwarder, so that we can continue the message and token relay chain. * - The address of the next layer's spoke pool, so that we can determine whether the following network contains the target spoke pool. * these addresses describe the path a message or token relay must take to arrive at the next layer. A Route only describes the path to arrive @@ -44,18 +39,20 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // Address of the contract which can relay messages to the subsequent forwarders/spoke pools and update this proxy contract. address public crossDomainAdmin; + // Map from a destination address to the next token/message bridge route to reach the destination address. The destination address can either be a - // spoke pool or a forwarder. - // This mapping requires that no destination addresses (i.e. spoke pool or forwarder addresses) collide. We cannot use the - // destination chain ID as a key here since we do not have that information. + // spoke pool or a forwarder. Destination addresses are used as the key in this mapping because the `target` address is the only information + // we receive when `relay{Message/Token}` is called on this contract, so we assume that these destination contracts on all the possible next + // layers have unique addresses. Ideally we could use the destination chain ID as a key for this mapping but we don't have that available where this + // mapping is accessed in `relay{Message/Token}` mapping(address => Route) possibleRoutes; - // Map which takes inputs the destination address and a token address on the current network and outputs the corresponding remote token address. - // This also requires that no destination addresses collide. + + // Map which takes as input the destination address and a token address on the current network and outputs the corresponding remote token address, + // which is the equivalent of the token address on the destination network. mapping(address => mapping(address => address)) destinationChainTokens; event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); event MessageForwarded(address indexed target, bytes message); - event SetXDomainAdmin(address indexed crossDomainAdmin); event RouteUpdated(address indexed target, Route route); event DestinationChainTokensUpdated( address indexed target, @@ -63,7 +60,6 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address indexed destinationChainToken ); - error InvalidCrossDomainAdmin(); error InvalidL3SpokePool(); error RelayMessageFailed(); error RelayTokensFailed(address baseToken); @@ -73,8 +69,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { error MalformedRoute(); /* - * @dev All functions with this modifier must revert if msg.sender != crossDomainAdmin. Each L2 may have - * unique aliasing logic, so it is up to the chain-specific forwarder contract to verify that the sender is valid. + * @dev Cross domain admin permissioning is implemented specifically for each L2 that this contract is deployed on, so this base contract + * simply prescribes this modifier to protect external functions using that L2's specific admin permissioning logic. */ modifier onlyAdmin() { _requireAdminSender(); @@ -92,26 +88,15 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Initializes the forwarder contract. - * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. - * In practice, this is the hub pool for forwarders deployed on L2, and other forwarder contracts on layers != L2. */ - function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { + function __Forwarder_init() public onlyInitializing { __UUPSUpgradeable_init(); - _setCrossDomainAdmin(_crossDomainAdmin); - } - - /** - * @notice Sets a new cross domain admin for this contract. - * @param _newCrossDomainAdmin L1 address of the new cross domain admin. - */ - function setCrossDomainAdmin(address _newCrossDomainAdmin) external onlyAdmin { - _setCrossDomainAdmin(_newCrossDomainAdmin); } /** - * @notice Sets a new Route to a specified address. - * @param _destinationAddress The address to set in the possibleRoutes mapping. - * @param _route Route struct containing the next immediate path to reach the _destination address. + * @notice Maps a new set of contracts that exist on the network to a destination address. + * @param _destinationAddress The address on the destination chain that we want to send messages to from this contract. + * @param _route Contracts available on the destinatio network that we want to send messages to. * @dev Each forwarder will not know how many layers it must traverse to arrive at _destinationAddress until it is precisely * one layer away. The possibleRoutes mapping lets the forwarder know what path it should take to progess, and then relies on * the forwarders on the subsequent layers to derive the next route in sequence. @@ -149,19 +134,19 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { * call to the next layer's forwarder contract. */ function relayMessage(address target, bytes memory message) external payable override onlyAdmin { - Route storage route = possibleRoutes[target]; - if (route.adapter == address(0)) revert UninitializedRoute(); + Route storage nextLayerContracts = possibleRoutes[target]; + if (nextLayerContracts.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are on the network immediately before our target, so we forward the original message over the canonical messaging bridge. - if (target == route.spokePool || target == route.forwarder) { - (success, ) = route.adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); + if (target == nextLayerContracts.spokePool || target == nextLayerContracts.forwarder) { + (success, ) = nextLayerContracts.adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); } else { - if (route.forwarder == address(0)) revert MalformedRoute(); + if (nextLayerContracts.forwarder == address(0)) revert MalformedRoute(); // Case 2: We are not on the network immediately before our target, so we wrap the message in a `relayMessage` call and forward it // to the next forwarder on the path to the target contract. bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); - (success, ) = route.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (route.forwarder, wrappedMessage)) + (success, ) = nextLayerContracts.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (nextLayerContracts.forwarder, wrappedMessage)) ); } if (!success) revert RelayMessageFailed(); @@ -185,13 +170,13 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { uint256 amount, address target ) internal { - Route storage route = possibleRoutes[target]; - if (route.adapter == address(0)) revert UninitializedRoute(); + Route storage nextLayerContracts = possibleRoutes[target]; + if (nextLayerContracts.adapter == address(0)) revert UninitializedRoute(); bool success; // Case 1: We are immediately before the target spoke pool, so we send the tokens to the spoke pool // and do NOT follow it up with a message. - if (target == route.spokePool) { - (success, ) = route.adapter.delegatecall( + if (target == nextLayerContracts.spokePool) { + (success, ) = nextLayerContracts.adapter.delegatecall( abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target)) ); if (!success) revert RelayTokensFailed(baseToken); @@ -199,12 +184,12 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // Case 2: We are not immediately before the target spoke pool, so we send the tokens to the next // forwarder and accompany it with a message containing information about its intended destination. - if (route.forwarder == address(0)) revert MalformedRoute(); + if (nextLayerContracts.forwarder == address(0)) revert MalformedRoute(); // Send tokens to the forwarder. - (success, ) = route.adapter.delegatecall( + (success, ) = nextLayerContracts.adapter.delegatecall( abi.encodeCall( AdapterInterface.relayTokens, - (baseToken, destinationChainToken, amount, route.forwarder) + (baseToken, destinationChainToken, amount, nextLayerContracts.forwarder) ) ); if (!success) revert RelayTokensFailed(baseToken); @@ -214,23 +199,19 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target) ); - (success, ) = route.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (route.forwarder, message)) + (success, ) = nextLayerContracts.adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayMessage, (nextLayerContracts.forwarder, message)) ); if (!success) revert RelayMessageFailed(); } emit TokensForwarded(target, baseToken, amount); } - // Function to be overridden to accomodate for each L2's unique method of address aliasing. + // Function to be overridden in order to authenticate that messages sent to this contract originated + // from the expected account. function _requireAdminSender() internal virtual; - // Use the same access control logic implemented in the forwarders to authorize an upgrade. + // We also want to restrict who can upgrade this contract. The same admin that can relay messages through this + // contract can upgrade this contract. function _authorizeUpgrade(address) internal virtual override onlyAdmin {} - - function _setCrossDomainAdmin(address _newCrossDomainAdmin) internal { - if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); - crossDomainAdmin = _newCrossDomainAdmin; - emit SetXDomainAdmin(_newCrossDomainAdmin); - } } From cbe295393e4a7497c2c72b1d7603ef1eae239c76 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:14:43 -0400 Subject: [PATCH 39/45] Update ForwarderBase.sol --- contracts/chain-adapters/ForwarderBase.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 8d702477..ffec99b4 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -37,9 +37,6 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address spokePool; } - // Address of the contract which can relay messages to the subsequent forwarders/spoke pools and update this proxy contract. - address public crossDomainAdmin; - // Map from a destination address to the next token/message bridge route to reach the destination address. The destination address can either be a // spoke pool or a forwarder. Destination addresses are used as the key in this mapping because the `target` address is the only information // we receive when `relay{Message/Token}` is called on this contract, so we assume that these destination contracts on all the possible next From 14c5c41c19830c14b0ecee10bc38ddd28123a18f Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:40:34 -0400 Subject: [PATCH 40/45] Update contracts/chain-adapters/ForwarderBase.sol Co-authored-by: Matt Rice --- contracts/chain-adapters/ForwarderBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index ffec99b4..c55e39eb 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -93,7 +93,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Maps a new set of contracts that exist on the network to a destination address. * @param _destinationAddress The address on the destination chain that we want to send messages to from this contract. - * @param _route Contracts available on the destinatio network that we want to send messages to. + * @param _route Contracts available on the destination network that we want to send messages to. * @dev Each forwarder will not know how many layers it must traverse to arrive at _destinationAddress until it is precisely * one layer away. The possibleRoutes mapping lets the forwarder know what path it should take to progess, and then relies on * the forwarders on the subsequent layers to derive the next route in sequence. From 242775d1091c221fa84defd6fbb7a4e9dccfa6d5 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:44:38 -0400 Subject: [PATCH 41/45] Update ForwarderBase.sol --- contracts/chain-adapters/ForwarderBase.sol | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index c55e39eb..23e26f3d 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -37,6 +37,9 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address spokePool; } + // Address that can relay messages using this contract and also upgrade this contract. + address public crossDomainAdmin; + // Map from a destination address to the next token/message bridge route to reach the destination address. The destination address can either be a // spoke pool or a forwarder. Destination addresses are used as the key in this mapping because the `target` address is the only information // we receive when `relay{Message/Token}` is called on this contract, so we assume that these destination contracts on all the possible next @@ -51,6 +54,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); event MessageForwarded(address indexed target, bytes message); event RouteUpdated(address indexed target, Route route); + event SetXDomainAdmin(address indexed crossDomainAdmin); event DestinationChainTokensUpdated( address indexed target, address indexed baseToken, @@ -76,6 +80,8 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Constructs the Forwarder contract. + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages + * to this forwarder contract. * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, * may disrupt the proxy if another EOA were to initialize it. */ @@ -86,8 +92,17 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Initializes the forwarder contract. */ - function __Forwarder_init() public onlyInitializing { + function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { __UUPSUpgradeable_init(); + _setCrossDomainAdmin(_crossDomainAdmin); + } + + /** + * @notice Sets a new cross domain admin for this contract. + * @param _newCrossDomainAdmin L1 address of the new cross domain admin. + */ + function setCrossDomainAdmin(address _newCrossDomainAdmin) external onlyAdmin { + _setCrossDomainAdmin(_newCrossDomainAdmin); } /** @@ -211,4 +226,15 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { // We also want to restrict who can upgrade this contract. The same admin that can relay messages through this // contract can upgrade this contract. function _authorizeUpgrade(address) internal virtual override onlyAdmin {} + + function _setCrossDomainAdmin(address _newCrossDomainAdmin) internal { + if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); + crossDomainAdmin = _newCrossDomainAdmin; + emit SetXDomainAdmin(_newCrossDomainAdmin); + } + + // Reserve storage slots for future versions of this base contract to add state variables without + // affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables + // are added. This is at bottom of contract to make sure it's always at the end of storage. + uint256[1000] private __gap; } From 87cdbe6dddb4875e14c58c2d65d8830c02f9e8bd Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:49:09 +0000 Subject: [PATCH 42/45] fix: lint --- contracts/chain-adapters/Rerouter_Adapter.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 2a9f6318..99aa9514 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -6,11 +6,11 @@ import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @notice Contract containing logic to send messages from L1 to "L3" networks that do not have direct connections * with L1. L3's are defined as networks that connect to L1 indirectly via L2, and this contract sends - * messages to those L3's by rerouting them via those L2's. This contract is called a "Rerouter" because it uses + * messages to those L3's by rerouting them via those L2's. This contract is called a "Rerouter" because it uses * (i.e. delegatecall's) existing L2 adapter logic to send a message first from L1 to L2 and then from L2 to L3. - * @dev Due to the constraints of the `SetCrossChainContracts` event as outlined in UMIP-157 and how the HubPool + * @dev Due to the constraints of the `SetCrossChainContracts` event as outlined in UMIP-157 and how the HubPool * delegatecalls adapters like this one, all messages relayed through this - * adapter have target addresses on the L3's. However, these target addresses do not exist on L2 where all messages are + * adapter have target addresses on the L3's. However, these target addresses do not exist on L2 where all messages are * rerouted through. Therefore, this contract is designed to be used in tandem with "L2 Forwarder Adapters" which help * get the messages from L1 to L3 via L2's. * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be From 41b4856a3bf93ba93296db1a38ea69b899e9f926 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 1 Oct 2024 09:39:35 -0500 Subject: [PATCH 43/45] reduce scope so that we only accomodate L3s. Use chain IDs as keys. Signed-off-by: bennett --- contracts/chain-adapters/ForwarderBase.sol | 196 +++++------------- .../interfaces/ForwarderInterface.sol | 53 +++++ 2 files changed, 108 insertions(+), 141 deletions(-) create mode 100644 contracts/chain-adapters/interfaces/ForwarderInterface.sol diff --git a/contracts/chain-adapters/ForwarderBase.sol b/contracts/chain-adapters/ForwarderBase.sol index 23e26f3d..faeaec25 100644 --- a/contracts/chain-adapters/ForwarderBase.sol +++ b/contracts/chain-adapters/ForwarderBase.sol @@ -2,58 +2,30 @@ pragma solidity ^0.8.0; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ForwarderInterface } from "./interfaces/ForwarderInterface.sol"; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; /** * @title ForwarderBase - * @notice This contract expects to receive messages and tokens from an authorized sender on a previous layer and forwards messages and tokens - * to contracts on subsequent layers. Messages are intended to originate from the hub pool or other forwarder contracts. The motivating use case - * for this contract is to aid with sending messages from L1 to an L3, which by definition is a network which does not have a direct connection - * with L1 but instead must communicate with that L1 via an L2. Each contract that extends the ForwarderBase maintains a mapping of - * contracts that exist on networks that are connected to the network that this contract is deployed on. For example, if this contract is deployed on - * Arbitrum, then there could be a mapping of contracts for each L3 that is connected to Arbitrum and communicates to L1 via Arbitrum. In other words, - * this contract maintains a mapping of important contracts on the "next layer" and is used to help relay messages from L1 to L3 by sending messages - * through the next layer. Messages could be forwarded across an indefinite amount of layers by recursively wrapping and sending messages over canonical - * bridges, only stopping when the following network contains the intended target contract. + * @notice This contract expects to receive messages and tokens from an authorized sender on L1 and forwards messages and tokens to spoke pool contracts on + * L3. Messages are intended to originate from the hub pool. The motivating use case for this contract is to aid with sending messages from L1 to an L3, which + * by definition is a network which does not have a direct connection with L1 but instead must communicate with that L1 via an L2. Each contract that extends + * the ForwarderBase maintains a mapping of chain IDs to a bridge adapter addresses. For example, if this contract is deployed on Arbitrum, then this mapping + * would send L3 chain IDs which roll up to Arbitrum to an adapter contract address deployed on Arbitrum which directly interfaces with the L3 token/message + * bridge. In other words, this contract maintains a mapping of important contracts which helps transmit messages to the "next layer". * @custom:security-contact bugs@across.to */ -abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { - /** - * @notice Route contains all information needed in order to send a message to the next layer. It describes two possible paths: a message is sent - * through `adapter` (on this network) to the `forwarder` on the next layer, or a message is sent through `adapter` to the `spokePool`. - * @dev We need to know three different addresses: - * - The address of the adapter to delegatecall, so that we may propagate message and token relays. This contract lives on this layer. - * - The address of the next layer's forwarder, so that we can continue the message and token relay chain. - * - The address of the next layer's spoke pool, so that we can determine whether the following network contains the target spoke pool. - * these addresses describe the path a message or token relay must take to arrive at the next layer. A Route only describes the path to arrive - * a layer closer to to the ultimate target, unless one of the `forwarder` or `spokePool` addresses equal the target contract, in which case - * the path ends. All Routes must contain an adapter address; however, not all Routes must contain a forwarder or spokePool address. For example, - * we may send a message to a spoke pool on a network with no corresponding forwarder contract, or we may send a message to a network which contains - * a forwarder contract but no spoke pool. - */ - struct Route { - address adapter; - address forwarder; - address spokePool; - } - +abstract contract ForwarderBase is UUPSUpgradeable, ForwarderInterface { // Address that can relay messages using this contract and also upgrade this contract. address public crossDomainAdmin; - // Map from a destination address to the next token/message bridge route to reach the destination address. The destination address can either be a - // spoke pool or a forwarder. Destination addresses are used as the key in this mapping because the `target` address is the only information - // we receive when `relay{Message/Token}` is called on this contract, so we assume that these destination contracts on all the possible next - // layers have unique addresses. Ideally we could use the destination chain ID as a key for this mapping but we don't have that available where this - // mapping is accessed in `relay{Message/Token}` - mapping(address => Route) possibleRoutes; - - // Map which takes as input the destination address and a token address on the current network and outputs the corresponding remote token address, - // which is the equivalent of the token address on the destination network. - mapping(address => mapping(address => address)) destinationChainTokens; + // Map from a destination chain ID to the address of an adapter contract which interfaces with the L2-L3 bridge. The destination chain ID corresponds to + // the network ID of an L3. These chain IDs are used as the key in this mapping because network IDs are enforced to be unique. Since we require the chain + // ID to be sent along with a message or token relay, ForwarderInterface's relay functions include an extra field, `destinationChainId`, when compared to the + // relay functions of `AdapterInterface`. + mapping(uint256 => address) chainAdapters; - event TokensForwarded(address indexed target, address indexed baseToken, uint256 amount); - event MessageForwarded(address indexed target, bytes message); - event RouteUpdated(address indexed target, Route route); + event ChainAdaptersUpdated(uint256 indexed destinationChainId, address l2Adapter); event SetXDomainAdmin(address indexed crossDomainAdmin); event DestinationChainTokensUpdated( address indexed target, @@ -61,13 +33,11 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { address indexed destinationChainToken ); - error InvalidL3SpokePool(); + error InvalidCrossDomainAdmin(); error RelayMessageFailed(); error RelayTokensFailed(address baseToken); - // Error which is triggered when there is no adapter set for a given route. + // Error which is triggered when there is no adapter set in the `chainAdapters` mapping. error UninitializedRoute(); - // Error which is triggered when we attempted send tokens or a message relay to a forwarder which is uninitialized. - error MalformedRoute(); /* * @dev Cross domain admin permissioning is implemented specifically for each L2 that this contract is deployed on, so this base contract @@ -80,8 +50,6 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Constructs the Forwarder contract. - * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages - * to this forwarder contract. * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, * may disrupt the proxy if another EOA were to initialize it. */ @@ -91,6 +59,7 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { /** * @notice Initializes the forwarder contract. + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. */ function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { __UUPSUpgradeable_init(); @@ -106,117 +75,62 @@ abstract contract ForwarderBase is UUPSUpgradeable, AdapterInterface { } /** - * @notice Maps a new set of contracts that exist on the network to a destination address. - * @param _destinationAddress The address on the destination chain that we want to send messages to from this contract. - * @param _route Contracts available on the destination network that we want to send messages to. - * @dev Each forwarder will not know how many layers it must traverse to arrive at _destinationAddress until it is precisely - * one layer away. The possibleRoutes mapping lets the forwarder know what path it should take to progess, and then relies on - * the forwarders on the subsequent layers to derive the next route in sequence. - */ - function updateRoute(address _destinationAddress, Route memory _route) external onlyAdmin { - possibleRoutes[_destinationAddress] = _route; - emit RouteUpdated(_destinationAddress, _route); - } - - /** - * @notice Sets a new remote token in the destinationChainTokens mapping. - * @param _destinationAddress The address to set in the destinationChainTokens mapping. This is to identify the network associated with the - * destination chain token. - * @param _baseToken The address of the token which exists on the current network. - * @param _destinationChainToken The address of the token on the network which is next on the path to _destinationAddress. - * @dev This mapping also relies on using _destinationAddress to determine the network to bridge to. Consequently, it requires that a new - * _destinationAddress does not collide with an existing key in the `destinationChainTokens` mapping. + * @notice Maps a new destination chain ID to an adapter contract which facilitates bridging to that chain. + * @param _destinationChainId The chain ID of the target network. + * @param _l2Adapter Contract address of the adapter which interfaces with the L2-L3 bridge. + * @dev Actual bridging logic is delegated to the adapter contract so that the forwarder can function irrespective of the "flavor" of + * L3 (e.g. ArbitrumOrbit, OpStack, etc.). */ - function updateRemoteToken( - address _destinationAddress, - address _baseToken, - address _destinationChainToken - ) external onlyAdmin { - destinationChainTokens[_destinationAddress][_baseToken] = _destinationChainToken; - emit DestinationChainTokensUpdated(_destinationAddress, _baseToken, _destinationChainToken); + function updateRoute(uint256 _destinationChainId, address _l2Adapter) external onlyAdmin { + chainAdapters[_destinationChainId] = _l2Adapter; + emit ChainAdaptersUpdated(_destinationChainId, _l2Adapter); } /** - * @notice Relays a specified message to a contract on the following layer. If `target` exists on the following layer, then `message` is sent - * to it. Otherwise, `message` is sent to the next layer's forwarder contract. - * @param target The address of the spoke pool or forwarder contract that will receive the input message. + * @notice Relays a specified message to a contract on L3. This contract assumes that `target` exists on the L3 and can properly + * receive the function being called. + * @param target The address of the spoke pool contract that will receive the input message. + * @param destinationChainId The chain ID of the network which contains `target`. * @param message The data to execute on the target contract. - * @dev Each forwarder will only know if they are on the layer directly before the layer which contains the `target` contract. - * If the next layer contains the target, we send `message` cross-chain normally. Otherwise, we wrap `message` into a `relayMessage` - * call to the next layer's forwarder contract. */ - function relayMessage(address target, bytes memory message) external payable override onlyAdmin { - Route storage nextLayerContracts = possibleRoutes[target]; - if (nextLayerContracts.adapter == address(0)) revert UninitializedRoute(); - bool success; - // Case 1: We are on the network immediately before our target, so we forward the original message over the canonical messaging bridge. - if (target == nextLayerContracts.spokePool || target == nextLayerContracts.forwarder) { - (success, ) = nextLayerContracts.adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); - } else { - if (nextLayerContracts.forwarder == address(0)) revert MalformedRoute(); - // Case 2: We are not on the network immediately before our target, so we wrap the message in a `relayMessage` call and forward it - // to the next forwarder on the path to the target contract. - bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); - (success, ) = nextLayerContracts.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (nextLayerContracts.forwarder, wrappedMessage)) - ); - } + function relayMessage( + address target, + uint256 destinationChainId, + bytes memory message + ) external payable override onlyAdmin { + address adapter = chainAdapters[destinationChainId]; + if (adapter == address(0)) revert UninitializedRoute(); + + // The forwarder assumes that `target` exists on the following network. + (bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); if (!success) revert RelayMessageFailed(); - emit MessageForwarded(target, message); + emit MessageForwarded(target, destinationChainId, message); } /** - * @notice Relays `amount` of a token to a contract on the following layer. If `target` does not exist on the following layer, then the tokens - * are sent to the following layer's forwarder contract. + * @notice Relays `amount` of a token to a contract on L3. Importantly, this contract assumes that `target` exists on L3. * @param baseToken This layer's address of the token to send. * @param destinationChainToken The next layer's address of the token to send. * @param amount The amount of the token to send. - * @param target The address of the contract that which will *ultimately* receive the tokens. That is, the spoke pool contract address on an - * arbitrary layer which receives `amount` of a token after all forwards have been completed. - * @dev The relayTokens function (which must be implemented on a per-chain basis) needs to inductively determine the address of the remote token - * given a known baseToken. Then, once the correct addresses are obtained, this function may be called to finish the relay of tokens. + * @param destinationChainId The chain ID of the network which contains `target`. + * @param target The address of the contract that which will *ultimately* receive the tokens. For most cases, this is the spoke pool contract on L3. + * @dev While `relayMessage` also assumes that `target` is correct, this function has the potential of deleting funds if `target` is incorrectly set. + * This should be guarded by the logic of the Hub Pool on L1, since the Hub Pool will always set `target` to the L3 spoke pool per UMIP-157. */ - function _relayTokens( + function relayTokens( address baseToken, address destinationChainToken, uint256 amount, + uint256 destinationChainId, address target - ) internal { - Route storage nextLayerContracts = possibleRoutes[target]; - if (nextLayerContracts.adapter == address(0)) revert UninitializedRoute(); - bool success; - // Case 1: We are immediately before the target spoke pool, so we send the tokens to the spoke pool - // and do NOT follow it up with a message. - if (target == nextLayerContracts.spokePool) { - (success, ) = nextLayerContracts.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target)) - ); - if (!success) revert RelayTokensFailed(baseToken); - } else { - // Case 2: We are not immediately before the target spoke pool, so we send the tokens to the next - // forwarder and accompany it with a message containing information about its intended destination. - - if (nextLayerContracts.forwarder == address(0)) revert MalformedRoute(); - // Send tokens to the forwarder. - (success, ) = nextLayerContracts.adapter.delegatecall( - abi.encodeCall( - AdapterInterface.relayTokens, - (baseToken, destinationChainToken, amount, nextLayerContracts.forwarder) - ) - ); - if (!success) revert RelayTokensFailed(baseToken); - - // Send a follow-up message to the forwarder which tells it to continue bridging. - bytes memory message = abi.encodeCall( - AdapterInterface.relayTokens, - (baseToken, destinationChainToken, amount, target) - ); - (success, ) = nextLayerContracts.adapter.delegatecall( - abi.encodeCall(AdapterInterface.relayMessage, (nextLayerContracts.forwarder, message)) - ); - if (!success) revert RelayMessageFailed(); - } - emit TokensForwarded(target, baseToken, amount); + ) external payable override onlyAdmin { + address adapter = chainAdapters[destinationChainId]; + if (adapter == address(0)) revert UninitializedRoute(); + (bool success, ) = adapter.delegatecall( + abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target)) + ); + if (!success) revert RelayTokensFailed(baseToken); + emit TokensForwarded(baseToken, destinationChainToken, amount, destinationChainId, target); } // Function to be overridden in order to authenticate that messages sent to this contract originated diff --git a/contracts/chain-adapters/interfaces/ForwarderInterface.sol b/contracts/chain-adapters/interfaces/ForwarderInterface.sol new file mode 100644 index 00000000..95378828 --- /dev/null +++ b/contracts/chain-adapters/interfaces/ForwarderInterface.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @notice Sends cross chain messages and tokens to contracts on a specific L3 network. + * This interface is implemented by forwarder contracts deployed to L2s. + */ + +interface ForwarderInterface { + event MessageForwarded(address indexed target, uint256 indexed chainId, bytes message); + + event TokensForwarded( + address baseToken, + address remoteToken, + uint256 amount, + uint256 indexed destinationChainId, + address indexed to + ); + + /** + * @notice Send message to `target` on L3. + * @dev This method is marked payable because relaying the message might require a fee + * to be paid by the sender to forward the message to L3. However, it will not send msg.value + * to the target contract on L3. + * @param target L3 address to send message to. + * @param destinationChainId Chain ID of the L3 network. + * @param message Message to send to `target`. + */ + function relayMessage( + address target, + uint256 destinationChainId, + bytes calldata message + ) external payable; + + /** + * @notice Send `amount` of `l2Token` to `to` on L3. `l3oken` is the L3 address equivalent of `l2Token`. + * @dev This method is marked payable because relaying the message might require a fee + * to be paid by the sender to forward the message to L2. However, it will not send msg.value + * to the target contract on L2. + * @param l2Token L2 token to bridge. + * @param l3Token L3 token to receive. + * @param amount Amount of `l2Token` to bridge. + * @param destinationChainId Chain ID of the L3 network. + * @param to Bridge recipient. + */ + function relayTokens( + address l2Token, + address l3Token, + uint256 amount, + uint256 destinationChainId, + address to + ) external payable; +} From 1b1ceb5a2a2edfbd42e6614dd77f13353978405e Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 1 Oct 2024 11:31:37 -0500 Subject: [PATCH 44/45] update rerouter adapter to support new forwarder interface Signed-off-by: bennett --- contracts/chain-adapters/Rerouter_Adapter.sol | 46 +++++++++++++++--- test/evm/foundry/local/Rerouter_Adapter.t.sol | 47 ++++++++++++++++++- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Rerouter_Adapter.sol index 99aa9514..f1263b16 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Rerouter_Adapter.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; +import { ForwarderInterface } from "./interfaces/ForwarderInterface.sol"; +import { HubPoolInterface } from "../interfaces/HubPoolInterface.sol"; /** * @notice Contract containing logic to send messages from L1 to "L3" networks that do not have direct connections @@ -28,19 +30,40 @@ contract Rerouter_Adapter is AdapterInterface { // L2_TARGET is a "Forwarder" contract that will help relay messages from L1 to L3. Messages are "rerouted" through // the L2_TARGET. address public immutable L2_TARGET; + // L2_CHAIN_ID is the chain ID of the network which is "in between" the L1 and L3. This network will contain the forwarder. + uint256 public immutable L2_CHAIN_ID; + // L3_CHAIN_ID is the chain ID of the network which contains the target spoke pool. This chain id is passed to the + // forwarder contract so that it may determine the correct L2-L3 bridge to use to arrive at L3. + uint256 public immutable L3_CHAIN_ID; + // Interface of the Hub Pool. Used to query state related to L1-L2 token mappings. + HubPoolInterface public immutable HUB_POOL; error RelayMessageFailed(); error RelayTokensFailed(address l1Token); + error L2RouteNotWhitelisted(address l1Token); /** * @notice Constructs new Adapter. This contract will re-route messages destined for an L3 to L2_TARGET via the L1_ADAPTER contract. * @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers * and token relays to the L2 where _l2Target is deployed. * @param _l2Target Address of the L2 contract which receives the token and message relays in order to forward them to an L3. + * @param _l2ChainId Chain ID of the network which contains the forwarder. It is the network which is intermediate in message + * transmission from L1 to L3. + * @param _l3ChainId Chain ID of the network which contains the spoke pool which corresponds to this adapter instance. + * @param _hubPool Address of the hub pool deployed on L1. */ - constructor(address _l1Adapter, address _l2Target) { + constructor( + address _l1Adapter, + address _l2Target, + uint256 _l2ChainId, + uint256 _l3ChainId, + HubPoolInterface _hubPool + ) { L1_ADAPTER = _l1Adapter; L2_TARGET = _l2Target; + L2_CHAIN_ID = _l2ChainId; + L3_CHAIN_ID = _l3ChainId; + HUB_POOL = _hubPool; } /** @@ -54,7 +77,7 @@ contract Rerouter_Adapter is AdapterInterface { * method to send `message` to the following layers and ultimately to the target on L3. */ function relayMessage(address target, bytes memory message) external payable override { - bytes memory wrappedMessage = abi.encodeCall(AdapterInterface.relayMessage, (target, message)); + bytes memory wrappedMessage = abi.encodeCall(ForwarderInterface.relayMessage, (target, L3_CHAIN_ID, message)); (bool success, ) = L1_ADAPTER.delegatecall( abi.encodeCall(AdapterInterface.relayMessage, (L2_TARGET, wrappedMessage)) ); @@ -64,18 +87,24 @@ contract Rerouter_Adapter is AdapterInterface { /** * @notice Bridge tokens to a target on L2 and follow up the token bridge with a call to continue bridging the sent tokens. * @param l1Token L1 token to deposit. - * @param l2Token L2 token to receive. - * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. - * @param target The address of the contract which should ultimately receive `amount` of `l1Token`. + * @param l3Token L3 token to receive. + * @param amount Amount of L1 tokens to deposit and L3 tokens to receive. + * @param target The address of the contract which should ultimately receive `amount` of `l3Token`. * @dev When sending tokens, we follow-up with a message describing the amount of tokens we wish to continue bridging. * This allows forwarders to know how much of some token to allocate to a certain target. */ function relayTokens( address l1Token, - address l2Token, + address l3Token, uint256 amount, address target ) external payable override { + // Fetch the address of the L2 token, as defined in the Hub Pool. This is to complete a proper token bridge from L1 to L2. + address l2Token = HUB_POOL.poolRebalanceRoute(L2_CHAIN_ID, l1Token); + // If l2Token is the zero address, then this means that the L2 token is not enabled as a pool rebalance route. This is similar + // to the check in the hub pool contract found here: https://github.com/across-protocol/contracts/blob/a2afefecba57177a62be35be092516d0c106097e/contracts/HubPool.sol#L890 + if (l2Token == address(0)) revert L2RouteNotWhitelisted(l1Token); + // Relay tokens to the forwarder. (bool success, ) = L1_ADAPTER.delegatecall( abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, L2_TARGET)) @@ -83,7 +112,10 @@ contract Rerouter_Adapter is AdapterInterface { if (!success) revert RelayTokensFailed(l1Token); // Follow-up token relay with a message to continue the token relay on L2. - bytes memory message = abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, target)); + bytes memory message = abi.encodeCall( + ForwarderInterface.relayTokens, + (l2Token, l3Token, amount, L3_CHAIN_ID, target) + ); (success, ) = L1_ADAPTER.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (L2_TARGET, message))); if (!success) revert RelayMessageFailed(); } diff --git a/test/evm/foundry/local/Rerouter_Adapter.t.sol b/test/evm/foundry/local/Rerouter_Adapter.t.sol index e355312d..5d0cb94c 100644 --- a/test/evm/foundry/local/Rerouter_Adapter.t.sol +++ b/test/evm/foundry/local/Rerouter_Adapter.t.sol @@ -3,20 +3,32 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { MockERC20 } from "forge-std/mocks/MockERC20.sol"; + import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; +import { FinderInterface } from "@uma/core/contracts/data-verification-mechanism/interfaces/FinderInterface.sol"; + import { Rerouter_Adapter } from "../../../../contracts/chain-adapters/Rerouter_Adapter.sol"; import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol"; import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; import { WETH9 } from "../../../../contracts/external/WETH9.sol"; import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; import { MockBedrockL1StandardBridge, MockBedrockCrossDomainMessenger } from "../../../../contracts/test/MockBedrockStandardBridge.sol"; +import { HubPoolInterface } from "../../../../contracts/interfaces/HubPoolInterface.sol"; +import { HubPool } from "../../../../contracts/HubPool.sol"; +import { LpTokenFactoryInterface } from "../../../../contracts/interfaces/LpTokenFactoryInterface.sol"; // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). contract Mock_Rerouter_Adapter is Rerouter_Adapter { - constructor(address _l1Adapter, address _l2Target) Rerouter_Adapter(_l1Adapter, _l2Target) {} + constructor( + address _l1Adapter, + address _l2Target, + uint256 _l2ChainId, + uint256 _l3ChainId, + HubPoolInterface _hubPool + ) Rerouter_Adapter(_l1Adapter, _l2Target, _l2ChainId, _l3ChainId, _hubPool) {} receive() external payable {} } @@ -43,17 +55,42 @@ contract RerouterAdapterTest is Test { WETH9 l2Weth; MockBedrockCrossDomainMessenger crossDomainMessenger; MockBedrockL1StandardBridge standardBridge; + HubPool hubPool; address l2Target; + address owner; + + uint256 constant L2_CHAIN_ID = 10; + uint256 constant L3_CHAIN_ID = 100; function setUp() public { l2Target = makeAddr("l2Target"); + owner = makeAddr("owner"); + + // Temporary values to initialize the hub pool. We do not set a new LP token, nor do we dispute, so these + // do not need to be contracts. + address finder = makeAddr("finder"); + address lpTokenFactory = makeAddr("lpTokenFactory"); + address timer = makeAddr("timer"); l1Token = new Token_ERC20("l1Token", "l1Token"); l2Token = new Token_ERC20("l2Token", "l2Token"); l1Weth = new WETH9(); l2Weth = new WETH9(); + vm.startPrank(owner); + hubPool = new HubPool( + LpTokenFactoryInterface(lpTokenFactory), + FinderInterface(finder), + WETH9Interface(address(l1Weth)), + timer + ); + // Whitelist l1Token and l1Weth for relaying on L2. Note that the hub pool does checks to ensure that the L3 token is whitelisted when it performs + // a token bridge. + hubPool.setPoolRebalanceRoute(L2_CHAIN_ID, address(l1Token), address(l2Token)); + hubPool.setPoolRebalanceRoute(L2_CHAIN_ID, address(l1Weth), address(l2Weth)); + vm.stopPrank(); + crossDomainMessenger = new MockBedrockCrossDomainMessenger(); standardBridge = new MockBedrockL1StandardBridge(); @@ -64,7 +101,13 @@ contract RerouterAdapterTest is Test { IERC20(address(0)), ITokenMessenger(address(0)) ); - rerouterAdapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target); + rerouterAdapter = new Mock_Rerouter_Adapter( + address(optimismAdapter), + l2Target, + L2_CHAIN_ID, + L3_CHAIN_ID, + hubPool + ); } // Messages should be indiscriminately sent to the l2Forwarder. From 074acc75288a8aaa5ee4cdd4b27bac26e726228f Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 1 Oct 2024 14:25:52 -0500 Subject: [PATCH 45/45] Rerouter -> Router Signed-off-by: bennett --- ...erouter_Adapter.sol => Router_Adapter.sol} | 4 +-- ...ter_Adapter.t.sol => Router_Adapter.t.sol} | 34 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) rename contracts/chain-adapters/{Rerouter_Adapter.sol => Router_Adapter.sol} (98%) rename test/evm/foundry/local/{Rerouter_Adapter.t.sol => Router_Adapter.t.sol} (83%) diff --git a/contracts/chain-adapters/Rerouter_Adapter.sol b/contracts/chain-adapters/Router_Adapter.sol similarity index 98% rename from contracts/chain-adapters/Rerouter_Adapter.sol rename to contracts/chain-adapters/Router_Adapter.sol index f1263b16..635bc021 100644 --- a/contracts/chain-adapters/Rerouter_Adapter.sol +++ b/contracts/chain-adapters/Router_Adapter.sol @@ -8,7 +8,7 @@ import { HubPoolInterface } from "../interfaces/HubPoolInterface.sol"; /** * @notice Contract containing logic to send messages from L1 to "L3" networks that do not have direct connections * with L1. L3's are defined as networks that connect to L1 indirectly via L2, and this contract sends - * messages to those L3's by rerouting them via those L2's. This contract is called a "Rerouter" because it uses + * messages to those L3's by rerouting them via those L2's. This contract is called a "Router" because it uses * (i.e. delegatecall's) existing L2 adapter logic to send a message first from L1 to L2 and then from L2 to L3. * @dev Due to the constraints of the `SetCrossChainContracts` event as outlined in UMIP-157 and how the HubPool * delegatecalls adapters like this one, all messages relayed through this @@ -23,7 +23,7 @@ import { HubPoolInterface } from "../interfaces/HubPoolInterface.sol"; */ // solhint-disable-next-line contract-name-camelcase -contract Rerouter_Adapter is AdapterInterface { +contract Router_Adapter is AdapterInterface { // Adapter designed to relay messages from L1 to L2 addresses and delegatecalled by this contract to reroute // messages to L3 via the L2_TARGET. address public immutable L1_ADAPTER; diff --git a/test/evm/foundry/local/Rerouter_Adapter.t.sol b/test/evm/foundry/local/Router_Adapter.t.sol similarity index 83% rename from test/evm/foundry/local/Rerouter_Adapter.t.sol rename to test/evm/foundry/local/Router_Adapter.t.sol index 5d0cb94c..9059c71e 100644 --- a/test/evm/foundry/local/Rerouter_Adapter.t.sol +++ b/test/evm/foundry/local/Router_Adapter.t.sol @@ -9,7 +9,7 @@ import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; import { FinderInterface } from "@uma/core/contracts/data-verification-mechanism/interfaces/FinderInterface.sol"; -import { Rerouter_Adapter } from "../../../../contracts/chain-adapters/Rerouter_Adapter.sol"; +import { Router_Adapter } from "../../../../contracts/chain-adapters/Router_Adapter.sol"; import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol"; import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; import { WETH9 } from "../../../../contracts/external/WETH9.sol"; @@ -21,14 +21,14 @@ import { LpTokenFactoryInterface } from "../../../../contracts/interfaces/LpToke // We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter // directly, so in order to withdraw Weth, we need to have receive(). -contract Mock_Rerouter_Adapter is Rerouter_Adapter { +contract Mock_Router_Adapter is Router_Adapter { constructor( address _l1Adapter, address _l2Target, uint256 _l2ChainId, uint256 _l3ChainId, HubPoolInterface _hubPool - ) Rerouter_Adapter(_l1Adapter, _l2Target, _l2ChainId, _l3ChainId, _hubPool) {} + ) Router_Adapter(_l1Adapter, _l2Target, _l2ChainId, _l3ChainId, _hubPool) {} receive() external payable {} } @@ -45,8 +45,8 @@ contract Token_ERC20 is ERC20 { } } -contract RerouterAdapterTest is Test { - Rerouter_Adapter rerouterAdapter; +contract RouterAdapterTest is Test { + Router_Adapter routerAdapter; Optimism_Adapter optimismAdapter; Token_ERC20 l1Token; @@ -101,13 +101,7 @@ contract RerouterAdapterTest is Test { IERC20(address(0)), ITokenMessenger(address(0)) ); - rerouterAdapter = new Mock_Rerouter_Adapter( - address(optimismAdapter), - l2Target, - L2_CHAIN_ID, - L3_CHAIN_ID, - hubPool - ); + routerAdapter = new Mock_Router_Adapter(address(optimismAdapter), l2Target, L2_CHAIN_ID, L3_CHAIN_ID, hubPool); } // Messages should be indiscriminately sent to the l2Forwarder. @@ -115,7 +109,7 @@ contract RerouterAdapterTest is Test { vm.assume(target != l2Target); vm.expectEmit(address(crossDomainMessenger)); emit MockBedrockCrossDomainMessenger.MessageSent(l2Target); - rerouterAdapter.relayMessage(target, message); + routerAdapter.relayMessage(target, message); } // Sending Weth should call depositETHTo(). @@ -123,22 +117,22 @@ contract RerouterAdapterTest is Test { // Prevent fuzz testing with amountToSend * 2 > 2^256 amountToSend = uint256(bound(amountToSend, 1, 2**254)); vm.deal(address(l1Weth), amountToSend); - vm.deal(address(rerouterAdapter), amountToSend); + vm.deal(address(routerAdapter), amountToSend); - vm.startPrank(address(rerouterAdapter)); + vm.startPrank(address(routerAdapter)); l1Weth.deposit{ value: amountToSend }(); vm.stopPrank(); assertEq(amountToSend * 2, l1Weth.totalSupply()); vm.expectEmit(address(standardBridge)); emit MockBedrockL1StandardBridge.ETHDepositInitiated(l2Target, amountToSend); - rerouterAdapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); - assertEq(0, l1Weth.balanceOf(address(rerouterAdapter))); + routerAdapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random); + assertEq(0, l1Weth.balanceOf(address(routerAdapter))); } // Sending any random token should call depositERC20To(). function testRelayToken(uint256 amountToSend, address random) public { - l1Token.mint(address(rerouterAdapter), amountToSend); + l1Token.mint(address(routerAdapter), amountToSend); assertEq(amountToSend, l1Token.totalSupply()); vm.expectEmit(address(standardBridge)); @@ -148,7 +142,7 @@ contract RerouterAdapterTest is Test { address(l2Token), amountToSend ); - rerouterAdapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); - assertEq(0, l1Token.balanceOf(address(rerouterAdapter))); + routerAdapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random); + assertEq(0, l1Token.balanceOf(address(routerAdapter))); } }