Skip to content

Commit 49a6f25

Browse files
committed
interop: interoperable ether transfers
1 parent cb74ef8 commit 49a6f25

File tree

1 file changed

+244
-0
lines changed

1 file changed

+244
-0
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Purpose
2+
3+
This document exists to align on a design for simplifying the process for sending `ETH` between two interoperable chains.
4+
5+
# Summary
6+
7+
New functionality is introduced to the `ETHLiquidity` contract that enables it to facilitate cross-chain `ETH` transfers to a specified recipient.
8+
9+
# Problem Statement + Context
10+
11+
Currently, L2-to-L2 `ETH` transfers between two interoperable chains require four separate transactions:
12+
13+
1. Wrap `ETH` to `SuperchainWETH`.
14+
2. Call `SuperchainTokenBridge#SendERC20` to burn `SuperchainWETH` on source chain and relay a message to the destination chain that mints `SuperchainWETH` to the recipient.
15+
3. Execute the message on the destination chain, triggering `SuperchainTokenBridge#RelayERC20` to mint `SuperchainWETH` to the recipient.
16+
4. Unwrap the received `SuperchainWETH` to `ETH`.
17+
18+
The goal is to reduce the transaction count from four to two, enabling users to send `ETH` to a destination chain directly:
19+
20+
1. Burn `ETH` on source chain and relay a message to destination chain that mints `ETH` to recipient on destination chain.
21+
2. Execute the relayed message on the destination chain that mints `ETH` to the recipient.
22+
23+
# Proposed Solution
24+
25+
Introduce `SendETH` and `RelayETH` functions to the `ETHLiquidity` contract. By adding these functions directly to `ETHLiquidity`, the contract retains its original purpose of centralizing the minting and burning of `ETH` for cross-chain transfers, ensuring that all liquidity management occurs within a single, dedicated contract.
26+
27+
### `SendETH` function
28+
29+
The `SendETH` function combines the first two transactions as follows:
30+
31+
1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent.
32+
2. Sends a message to the destination chain encoding a call to `RelayETH`.
33+
34+
### `RelayETH` function
35+
36+
The `RelayETH` function combines the last two transactions as follows:
37+
38+
1. Mints the specified `_amount` of `ETH` from the `ETHLiquidity` contract.
39+
2. Sends the minted `ETH` to the specified recipient.
40+
41+
### Contract changes
42+
43+
Update the `burn` function to be callable by the `ETHLiquidity` contract:
44+
45+
```solidity
46+
/// @notice Allows an address to lock ETH liquidity into this contract.
47+
function burn() external payable {
48+
if (msg.sender != Predeploys.SUPERCHAIN_WETH && msg.sender != address(this)) revert Unauthorized();
49+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
50+
emit LiquidityBurned(msg.sender, msg.value);
51+
}
52+
```
53+
54+
Update the `mint` function to have a `_recipient` parameter and to be callable by the `ETHLiquidity` contract:
55+
56+
```solidity
57+
/// @notice Allows an address to unlock ETH liquidity from this contract.
58+
/// @param _recipient Address to send ETH to.
59+
/// @param _amount The amount of liquidity to unlock.
60+
function mint(address _recipient, uint256 _amount) external {
61+
if (msg.sender != Predeploys.SUPERCHAIN_WETH && msg.sender != address(this)) revert Unauthorized();
62+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
63+
new SafeSend{ value: _amount }(payable(_recipient));
64+
emit LiquidityMinted(_recipient, _amount);
65+
}
66+
```
67+
68+
Add `SendETH` and `RelayETH` function to `ETHLiquidity`:
69+
```solidity
70+
/// @notice Sends ETH to some target address on another chain.
71+
/// @param _to Address to send ETH to.
72+
/// @param _chainId Chain ID of the destination chain.
73+
function sendETH(address _to, uint256 _chainId) public payable {
74+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
75+
revert NotCustomGasToken();
76+
}
77+
78+
// Burn to ETHLiquidity contract.
79+
this.burn{ value: msg.value }();
80+
81+
// Send message to other chain.
82+
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
83+
_destination: _chainId,
84+
_target: address(this),
85+
_message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value))
86+
});
87+
emit SendETH(msg.sender, _to, msg.value, _chainId);
88+
}
89+
90+
/// @notice Relays ETH received from another chain.
91+
/// @param _from Address of the msg.sender of sendETH on the source chain.
92+
/// @param _to Address to relay ETH to.
93+
/// @param _amount Amount of ETH to relay.
94+
function relayETH(address _from, address _to, uint256 _amount) external {
95+
IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
96+
if (msg.sender != address(messenger)) revert Unauthorized();
97+
if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized();
98+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
99+
revert NotCustomGasToken();
100+
}
101+
102+
this.mint(_to, _amount);
103+
emit RelayETH(_from, _to, _amount, messenger.crossDomainMessageSource());
104+
}
105+
```
106+
107+
# Considerations
108+
109+
## Custom Gas Token Chains
110+
111+
To simplify the solution, custom gas token chains will not be supported and must follow the original four-transaction flow, which includes wrapping and unwrapping `SuperchainWETH`.
112+
113+
# Open Questions
114+
- **Long-term improvements**: Could this functionality eventually extend to custom gas token chains?
115+
- **Rollbacks**: How would rollbacks be handled in this implementation?
116+
117+
# Alternatives Considered
118+
119+
## Integrate `ETH` transfer into `SuperchainWETH`
120+
121+
Add two new functions to the `SuperchainWETH` contract: `SendETH` and `RelayETH`.
122+
123+
### `SendETH`
124+
125+
The `SendETH` function combines the first two transactions as follows:
126+
127+
1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent.
128+
2. Sends a message to the destination chain encoding a call to `RelayETH`.
129+
130+
### `RelayETH`
131+
132+
The `RelayETH` function combines the last two transactions as follows:
133+
134+
1. Mints the specified `_amount` of `ETH` from the `ETHLiquidity` contract.
135+
2. Sends the minted `ETH` to the specified recipient.
136+
137+
### Contract changes
138+
139+
```solidity
140+
/// @notice Sends ETH to some target address on another chain.
141+
/// @param _to Address to send tokens to.
142+
/// @param _chainId Chain ID of the destination chain.
143+
function sendETH(address _to, uint256 _chainId) public payable {
144+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
145+
revert NotCustomGasToken();
146+
}
147+
148+
// Burn to ETHLiquidity contract.
149+
IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }();
150+
151+
// Send message to other chain.
152+
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
153+
_destination: _chainId,
154+
_target: address(this),
155+
_message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value))
156+
});
157+
158+
// Emit event.
159+
emit SendETH(msg.sender, _to, msg.value, _chainId);
160+
}
161+
162+
/// @notice Relays ETH received from another chain.
163+
/// @param _from Address of the msg.sender of sendETH on the source chain.
164+
/// @param _to Address to relay tokens to.
165+
/// @param _amount Amount of tokens to relay.
166+
function relayETH(address _from, address _to, uint256 _amount) external {
167+
// Receive message from other chain.
168+
IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
169+
if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger();
170+
if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();
171+
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
172+
revert NotCustomGasToken();
173+
}
174+
175+
// Mint from ETHLiquidity contract.
176+
IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount);
177+
178+
// Get source chain ID.
179+
uint256 source = messenger.crossDomainMessageSource();
180+
181+
new SafeSend{ value: _amount }(payable(_to));
182+
183+
// Emit event.
184+
emit RelayETH(_from, _to, _amount, source);
185+
}
186+
```
187+
188+
### Advantages
189+
190+
Since the `SuperchainWETH` contract already has permissions to mint and burn `ETH` with the `ETHLiquidity` contract no changes to the `ETHLiquidity` contract are necessary. Any security risks with this change are limited to `SuperchainWETH` and do not affect other `SuperchainERC20` tokens.
191+
192+
### Downsides
193+
194+
This approach introduces an additional entry point for asset transfers outside of `SuperchainTokenBridge`. While SuperchainERC20 tokens that implement custom bridging will already have alternative entry points, adding another for native `ETH` transfers could lead to confusion and diverge from a clean separation of concerns. Additionally, by having `SuperchainWETH` serve as both a `SuperchainERC20` and a bridge for native `ETH`, there is an increased risk of ambiguity around its dual function, which could impact usability and clarity for developers.
195+
196+
## Integrate `ETH` transfer into `SuperchainTokenBridge`
197+
198+
One alternative is to add two new functions to the `SuperchainTokenBridge` contract: `SendETH` and `RelayETH`.
199+
200+
```solidity
201+
/// @notice Sends ETH to a target address on another chain.
202+
/// @param _to Address to send ETH to.
203+
/// @param _amount Amount of ETH to send.
204+
/// @param _chainId Chain ID of the destination chain.
205+
/// @return msgHash_ Hash of the message sent.
206+
function sendETH(address _to, uint256 _amount, uint256 _chainId) external payable returns (bytes32 msgHash_) {
207+
if (_to == address(0)) revert ZeroAddress();
208+
209+
ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).deposit{ value: msg.value }();
210+
ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).crosschainBurn(msg.sender, msg.value);
211+
212+
bytes memory message = abi.encodeCall(this.relayETH, (msg.sender, _to, _amount));
213+
msgHash_ = IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), message);
214+
215+
emit SendERC20(Predeploys.SUPERCHAIN_WETH, msg.sender, _to, _amount, _chainId);
216+
}
217+
218+
/// @notice Relays ETH received from another chain.
219+
/// @param _from Address of the msg.sender of sendETH on the source chain.
220+
/// @param _to Address to relay ETH to.
221+
/// @param _amount Amount of ETH to relay.
222+
function relayETH(address _from, address _to, uint256 _amount) external {
223+
if (msg.sender != MESSENGER) revert Unauthorized();
224+
225+
(address crossDomainMessageSender, uint256 source) =
226+
IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageContext();
227+
if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender();
228+
229+
ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).crosschainMint(address(this), _amount);
230+
ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).withdraw(_amount);
231+
232+
new SafeSend{ value: _amount }(payable(_to));
233+
234+
emit RelayERC20(Predeploys.SUPERCHAIN_WETH, _from, _to, _amount, source);
235+
}
236+
```
237+
238+
### Advantages
239+
240+
The advantage of this solution is that `SuperchainTokenBridge`would handle both `ETH` transfers and `SuperchainERC20` transfers, simplifying developer integrations.
241+
242+
### Downsides
243+
244+
This solution creates changes to a highly sensitive contract. `SuperchainTokenBridge` has permissions to `mint` and `burn` standard `SuperchainERC20` tokens, so updates must be treated with extreme caution.

0 commit comments

Comments
 (0)