|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity ^0.8.0; |
| 3 | + |
| 4 | +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; |
| 5 | +import { ForwarderInterface } from "./interfaces/ForwarderInterface.sol"; |
| 6 | +import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; |
| 7 | + |
| 8 | +/** |
| 9 | + * @title ForwarderBase |
| 10 | + * @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 |
| 11 | + * 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 |
| 12 | + * 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 |
| 13 | + * 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 |
| 14 | + * 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 |
| 15 | + * bridge. In other words, this contract maintains a mapping of important contracts which helps transmit messages to the "next layer". |
| 16 | + * @custom:security-contact [email protected] |
| 17 | + */ |
| 18 | +abstract contract ForwarderBase is UUPSUpgradeable, ForwarderInterface { |
| 19 | + // Address that can relay messages using this contract and also upgrade this contract. |
| 20 | + address public crossDomainAdmin; |
| 21 | + |
| 22 | + // 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 |
| 23 | + // 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 |
| 24 | + // ID to be sent along with a message or token relay, ForwarderInterface's relay functions include an extra field, `destinationChainId`, when compared to the |
| 25 | + // relay functions of `AdapterInterface`. |
| 26 | + mapping(uint256 => address) chainAdapters; |
| 27 | + |
| 28 | + event ChainAdaptersUpdated(uint256 indexed destinationChainId, address l2Adapter); |
| 29 | + event SetXDomainAdmin(address indexed crossDomainAdmin); |
| 30 | + |
| 31 | + error InvalidCrossDomainAdmin(); |
| 32 | + error InvalidChainAdapter(); |
| 33 | + error RelayMessageFailed(); |
| 34 | + error RelayTokensFailed(address baseToken); |
| 35 | + // Error which is triggered when there is no adapter set in the `chainAdapters` mapping. |
| 36 | + error UninitializedChainAdapter(); |
| 37 | + |
| 38 | + /* |
| 39 | + * @dev Cross domain admin permissioning is implemented specifically for each L2 that this contract is deployed on, so this base contract |
| 40 | + * simply prescribes this modifier to protect external functions using that L2's specific admin permissioning logic. |
| 41 | + */ |
| 42 | + modifier onlyAdmin() { |
| 43 | + _requireAdminSender(); |
| 44 | + _; |
| 45 | + } |
| 46 | + |
| 47 | + /** |
| 48 | + * @notice Constructs the Forwarder contract. |
| 49 | + * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done, |
| 50 | + * may disrupt the proxy if another EOA were to initialize it. |
| 51 | + */ |
| 52 | + constructor() { |
| 53 | + _disableInitializers(); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * @notice Initializes the forwarder contract. |
| 58 | + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. |
| 59 | + */ |
| 60 | + function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing { |
| 61 | + __UUPSUpgradeable_init(); |
| 62 | + _setCrossDomainAdmin(_crossDomainAdmin); |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * @notice Sets a new cross domain admin for this contract. |
| 67 | + * @param _newCrossDomainAdmin L1 address of the new cross domain admin. |
| 68 | + */ |
| 69 | + function setCrossDomainAdmin(address _newCrossDomainAdmin) external onlyAdmin { |
| 70 | + if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); |
| 71 | + _setCrossDomainAdmin(_newCrossDomainAdmin); |
| 72 | + emit SetXDomainAdmin(_newCrossDomainAdmin); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * @notice Maps a new destination chain ID to an adapter contract which facilitates bridging to that chain. |
| 77 | + * @param _destinationChainId The chain ID of the target network. |
| 78 | + * @param _l2Adapter Contract address of the adapter which interfaces with the L2-L3 bridge. |
| 79 | + * @dev Actual bridging logic is delegated to the adapter contract so that the forwarder can function irrespective of the "flavor" of |
| 80 | + * L3 (e.g. ArbitrumOrbit, OpStack, etc.). |
| 81 | + */ |
| 82 | + function updateAdapter(uint256 _destinationChainId, address _l2Adapter) external onlyAdmin { |
| 83 | + if (_l2Adapter == address(0)) revert InvalidChainAdapter(); |
| 84 | + chainAdapters[_destinationChainId] = _l2Adapter; |
| 85 | + emit ChainAdaptersUpdated(_destinationChainId, _l2Adapter); |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * @notice Relays a specified message to a contract on L3. This contract assumes that `target` exists on the L3 and can properly |
| 90 | + * receive the function being called. |
| 91 | + * @param target The address of the spoke pool contract that will receive the input message. |
| 92 | + * @param destinationChainId The chain ID of the network which contains `target`. |
| 93 | + * @param message The data to execute on the target contract. |
| 94 | + */ |
| 95 | + function relayMessage( |
| 96 | + address target, |
| 97 | + uint256 destinationChainId, |
| 98 | + bytes memory message |
| 99 | + ) external payable override onlyAdmin { |
| 100 | + address adapter = chainAdapters[destinationChainId]; |
| 101 | + if (adapter == address(0)) revert UninitializedChainAdapter(); |
| 102 | + |
| 103 | + // The forwarder assumes that `target` exists on the following network. |
| 104 | + (bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message))); |
| 105 | + if (!success) revert RelayMessageFailed(); |
| 106 | + emit MessageForwarded(target, destinationChainId, message); |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * @notice Relays `amount` of a token to a contract on L3. Importantly, this contract assumes that `target` exists on L3. |
| 111 | + * @param baseToken This layer's address of the token to send. |
| 112 | + * @param destinationChainToken The next layer's address of the token to send. |
| 113 | + * @param amount The amount of the token to send. |
| 114 | + * @param destinationChainId The chain ID of the network which contains `target`. |
| 115 | + * @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. |
| 116 | + * @dev While `relayMessage` also assumes that `target` is correct, this function has the potential of deleting funds if `target` is incorrectly set. |
| 117 | + * 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. |
| 118 | + */ |
| 119 | + function relayTokens( |
| 120 | + address baseToken, |
| 121 | + address destinationChainToken, |
| 122 | + uint256 amount, |
| 123 | + uint256 destinationChainId, |
| 124 | + address target |
| 125 | + ) external payable override onlyAdmin { |
| 126 | + address adapter = chainAdapters[destinationChainId]; |
| 127 | + if (adapter == address(0)) revert UninitializedChainAdapter(); |
| 128 | + (bool success, ) = adapter.delegatecall( |
| 129 | + abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target)) |
| 130 | + ); |
| 131 | + if (!success) revert RelayTokensFailed(baseToken); |
| 132 | + emit TokensForwarded(baseToken, destinationChainToken, amount, destinationChainId, target); |
| 133 | + } |
| 134 | + |
| 135 | + // Function to be overridden in order to authenticate that messages sent to this contract originated |
| 136 | + // from the expected account. |
| 137 | + function _requireAdminSender() internal virtual; |
| 138 | + |
| 139 | + // We also want to restrict who can upgrade this contract. The same admin that can relay messages through this |
| 140 | + // contract can upgrade this contract. |
| 141 | + function _authorizeUpgrade(address) internal virtual override onlyAdmin {} |
| 142 | + |
| 143 | + function _setCrossDomainAdmin(address _newCrossDomainAdmin) internal { |
| 144 | + if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); |
| 145 | + crossDomainAdmin = _newCrossDomainAdmin; |
| 146 | + emit SetXDomainAdmin(_newCrossDomainAdmin); |
| 147 | + } |
| 148 | + |
| 149 | + // Reserve storage slots for future versions of this base contract to add state variables without |
| 150 | + // affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables |
| 151 | + // are added. This is at bottom of contract to make sure it's always at the end of storage. |
| 152 | + uint256[1000] private __gap; |
| 153 | +} |
0 commit comments