Skip to content

Commit 1248f8e

Browse files
bmzignicholaspaimrice32
authored
feat: generic L2 forwarder base contract (#609)
* feat: add L2 forwarder interface Signed-off-by: bennett <[email protected]> --------- Signed-off-by: bennett <[email protected]> Co-authored-by: nicholaspai <[email protected]> Co-authored-by: Matt Rice <[email protected]>
1 parent a2afefe commit 1248f8e

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @notice Sends cross chain messages and tokens to contracts on a specific L3 network.
6+
* This interface is implemented by forwarder contracts deployed to L2s.
7+
*/
8+
9+
interface ForwarderInterface {
10+
event MessageForwarded(address indexed target, uint256 indexed chainId, bytes message);
11+
12+
event TokensForwarded(
13+
address baseToken,
14+
address remoteToken,
15+
uint256 amount,
16+
uint256 indexed destinationChainId,
17+
address indexed to
18+
);
19+
20+
/**
21+
* @notice Send message to `target` on L3.
22+
* @dev This method is marked payable because relaying the message might require a fee
23+
* to be paid by the sender to forward the message to L3. However, it will not send msg.value
24+
* to the target contract on L3.
25+
* @param target L3 address to send message to.
26+
* @param destinationChainId Chain ID of the L3 network.
27+
* @param message Message to send to `target`.
28+
*/
29+
function relayMessage(
30+
address target,
31+
uint256 destinationChainId,
32+
bytes calldata message
33+
) external payable;
34+
35+
/**
36+
* @notice Send `amount` of `l2Token` to `to` on L3. `l3oken` is the L3 address equivalent of `l2Token`.
37+
* @dev This method is marked payable because relaying the message might require a fee
38+
* to be paid by the sender to forward the message to L2. However, it will not send msg.value
39+
* to the target contract on L2.
40+
* @param l2Token L2 token to bridge.
41+
* @param l3Token L3 token to receive.
42+
* @param amount Amount of `l2Token` to bridge.
43+
* @param destinationChainId Chain ID of the L3 network.
44+
* @param to Bridge recipient.
45+
*/
46+
function relayTokens(
47+
address l2Token,
48+
address l3Token,
49+
uint256 amount,
50+
uint256 destinationChainId,
51+
address to
52+
) external payable;
53+
}

0 commit comments

Comments
 (0)