From b2c87e94bcbc7c640abce8f4b7bb048f9e340959 Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 29 Oct 2024 12:01:41 -0700 Subject: [PATCH 01/14] interop: interoperable ether transfers --- protocol/interoperable-ether-transfers.md | 261 ++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 protocol/interoperable-ether-transfers.md diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md new file mode 100644 index 00000000..fbc7d4ec --- /dev/null +++ b/protocol/interoperable-ether-transfers.md @@ -0,0 +1,261 @@ +# Purpose + +This document exists to align on a design for simplifying the process for sending `ETH` between two interoperable chains. + +# Summary + +New functionality is introduced to the `ETHLiquidity` contract that enables it to facilitate cross-chain `ETH` transfers to a specified recipient. + +# Problem Statement + Context + +Currently, L2-to-L2 `ETH` transfers between two interoperable chains require four separate transactions: + +1. Wrap `ETH` to `SuperchainWETH`. +2. Call `SuperchainTokenBridge#SendERC20` to burn `SuperchainWETH` on source chain and relay a message to the destination chain that mints `SuperchainWETH` to the recipient. +3. Execute the message on the destination chain, triggering `SuperchainTokenBridge#RelayERC20` to mint `SuperchainWETH` to the recipient. +4. Unwrap the received `SuperchainWETH` to `ETH`. + +The goal is to reduce the transaction count from four to two, enabling users to send `ETH` to a destination chain directly: + +1. Burn `ETH` on source chain and relay a message to destination chain that mints `ETH` to recipient on destination chain. +2. Execute the relayed message on the destination chain that mints `ETH` to the recipient. + +# Proposed Solution + +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. + +### `SendETH` function + +The `SendETH` function combines the first two transactions as follows: + +1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. +2. Sends a message to the destination chain encoding a call to `RelayETH`. + +### `RelayETH` function + +The `RelayETH` function combines the last two transactions as follows: + +1. Mints the specified `_amount` of `ETH` from the `ETHLiquidity` contract. +2. Sends the minted `ETH` to the specified recipient. + +### Contract changes + +Add an internal `_burn` function that will be called by `ETHLiquidity#SendETH` and update the external `burn` function to call `_burn`: + +```solidity +/// @notice Allows an address to lock ETH liquidity into this contract. +function burn() external payable { + if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + _burn(msg.sender, msg.value); +} + +/// @notice Allows an address to lock ETH liquidity into this contract. +/// @param _sender Address that sent the ETH to be locked. +/// @param _amount The amount of liquidity to burn. +function _burn(address _sender, uint256 _amount) internal { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + emit LiquidityBurned(_sender, _amount); +} +``` + +Add an internal `_mint` function that will be called by `ETHLiquidity#RelayETH` and update the external `mint` function to call `_mint`: + +```solidity +/// @notice Allows an address to unlock ETH liquidity from this contract. +/// @param _amount The amount of liquidity to unlock. +function mint(uint256 _amount) external { + if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + _mint(msg.sender, _amount); +} + +/// @notice Allows an address to unlock ETH liquidity from this contract. +/// @param _recipient Address to send the unlocked ETH to. +/// @param _amount The amount of ETH liquidity to unlock. +function _mint(address _recipient, uint256 _amount) internal { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + new SafeSend{ value: _amount }(payable(_recipient)); + emit LiquidityMinted(_recipient, _amount); +} +``` + +Add `SendETH` and `RelayETH` function to `ETHLiquidity`: + +```solidity +/// @notice Sends ETH to some target address on another chain. +/// @param _to Address to send ETH to. +/// @param _chainId Chain ID of the destination chain. +function sendETH(address _to, uint256 _chainId) public payable { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + + _burn(msg.sender, msg.value); + + // Send message to other chain. + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ + _destination: _chainId, + _target: address(this), + _message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value)) + }); + emit SendETH(msg.sender, _to, msg.value, _chainId); +} + +/// @notice Relays ETH received from another chain. +/// @param _from Address of the msg.sender of sendETH on the source chain. +/// @param _to Address to relay ETH to. +/// @param _amount Amount of ETH to relay. +function relayETH(address _from, address _to, uint256 _amount) external { + IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + if (msg.sender != address(messenger)) revert Unauthorized(); + if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + + _mint(_to, _amount); + emit RelayETH(_from, _to, _amount, messenger.crossDomainMessageSource()); +} +``` + +# Considerations + +## SuperchainWETH transfers + +This solution does not eliminate the `SuperchainWETH` contract and `SuperchainWETH` transfers would still go through the `SuperchainTokenBridge`. + +## Custom Gas Token Chains + +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`. + +# Open Questions +- **Long-term improvements**: Could this functionality eventually extend to custom gas token chains? +- **Rollbacks**: How would rollbacks be handled in this implementation? + +# Alternatives Considered + +## Integrate `ETH` transfer into `SuperchainWETH` + +Add two new functions to the `SuperchainWETH` contract: `SendETH` and `RelayETH`. + +### `SendETH` + +The `SendETH` function combines the first two transactions as follows: + +1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. +2. Sends a message to the destination chain encoding a call to `RelayETH`. + +### `RelayETH` + +The `RelayETH` function combines the last two transactions as follows: + +1. Mints the specified `_amount` of `ETH` from the `ETHLiquidity` contract. +2. Sends the minted `ETH` to the specified recipient. + +### Contract changes + +```solidity +/// @notice Sends ETH to some target address on another chain. +/// @param _to Address to send tokens to. +/// @param _chainId Chain ID of the destination chain. +function sendETH(address _to, uint256 _chainId) public payable { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + + // Burn to ETHLiquidity contract. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); + + // Send message to other chain. + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ + _destination: _chainId, + _target: address(this), + _message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value)) + }); + + // Emit event. + emit SendETH(msg.sender, _to, msg.value, _chainId); +} + +/// @notice Relays ETH received from another chain. +/// @param _from Address of the msg.sender of sendETH on the source chain. +/// @param _to Address to relay tokens to. +/// @param _amount Amount of tokens to relay. +function relayETH(address _from, address _to, uint256 _amount) external { + // Receive message from other chain. + IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger(); + if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + + // Mint from ETHLiquidity contract. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + + // Get source chain ID. + uint256 source = messenger.crossDomainMessageSource(); + + new SafeSend{ value: _amount }(payable(_to)); + + // Emit event. + emit RelayETH(_from, _to, _amount, source); +} +``` + +### Advantages + +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. + +### Downsides + +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. + +## Integrate `ETH` transfer into `SuperchainTokenBridge` + +One alternative is to add two new functions to the `SuperchainTokenBridge` contract: `SendETH` and `RelayETH`. + +```solidity +/// @notice Sends ETH to a target address on another chain. +/// @param _to Address to send ETH to. +/// @param _amount Amount of ETH to send. +/// @param _chainId Chain ID of the destination chain. +/// @return msgHash_ Hash of the message sent. +function sendETH(address _to, uint256 _amount, uint256 _chainId) external payable returns (bytes32 msgHash_) { + if (_to == address(0)) revert ZeroAddress(); + + ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).deposit{ value: msg.value }(); + ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).crosschainBurn(msg.sender, msg.value); + + bytes memory message = abi.encodeCall(this.relayETH, (msg.sender, _to, _amount)); + msgHash_ = IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), message); + + emit SendERC20(Predeploys.SUPERCHAIN_WETH, msg.sender, _to, _amount, _chainId); +} + +/// @notice Relays ETH received from another chain. +/// @param _from Address of the msg.sender of sendETH on the source chain. +/// @param _to Address to relay ETH to. +/// @param _amount Amount of ETH to relay. +function relayETH(address _from, address _to, uint256 _amount) external { + if (msg.sender != MESSENGER) revert Unauthorized(); + + (address crossDomainMessageSender, uint256 source) = + IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageContext(); + if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender(); + + ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).crosschainMint(address(this), _amount); + ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)).withdraw(_amount); + + new SafeSend{ value: _amount }(payable(_to)); + + emit RelayERC20(Predeploys.SUPERCHAIN_WETH, _from, _to, _amount, source); +} +``` + +### Advantages + +The advantage of this solution is that `SuperchainTokenBridge`would handle both `ETH` transfers and `SuperchainERC20` transfers, simplifying developer integrations. + +### Downsides + +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. \ No newline at end of file From 657dc6d87b0ff0689d6a6bd8f7693184db35b6e3 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 14:52:11 -0800 Subject: [PATCH 02/14] update proposed solution to superchainweth --- protocol/interoperable-ether-transfers.md | 172 +++++++++++----------- 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index fbc7d4ec..c2aa9b45 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -22,16 +22,18 @@ The goal is to reduce the transaction count from four to two, enabling users to # Proposed Solution -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. +## Integrate `ETH` transfer into `SuperchainWETH` -### `SendETH` function +Add two new functions to the `SuperchainWETH` contract: `SendETH` and `RelayETH`. + +### `SendETH` The `SendETH` function combines the first two transactions as follows: 1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. 2. Sends a message to the destination chain encoding a call to `RelayETH`. -### `RelayETH` function +### `RelayETH` The `RelayETH` function combines the last two transactions as follows: @@ -40,56 +42,17 @@ The `RelayETH` function combines the last two transactions as follows: ### Contract changes -Add an internal `_burn` function that will be called by `ETHLiquidity#SendETH` and update the external `burn` function to call `_burn`: - -```solidity -/// @notice Allows an address to lock ETH liquidity into this contract. -function burn() external payable { - if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); - _burn(msg.sender, msg.value); -} - -/// @notice Allows an address to lock ETH liquidity into this contract. -/// @param _sender Address that sent the ETH to be locked. -/// @param _amount The amount of liquidity to burn. -function _burn(address _sender, uint256 _amount) internal { - if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); - emit LiquidityBurned(_sender, _amount); -} -``` - -Add an internal `_mint` function that will be called by `ETHLiquidity#RelayETH` and update the external `mint` function to call `_mint`: - -```solidity -/// @notice Allows an address to unlock ETH liquidity from this contract. -/// @param _amount The amount of liquidity to unlock. -function mint(uint256 _amount) external { - if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); - _mint(msg.sender, _amount); -} - -/// @notice Allows an address to unlock ETH liquidity from this contract. -/// @param _recipient Address to send the unlocked ETH to. -/// @param _amount The amount of ETH liquidity to unlock. -function _mint(address _recipient, uint256 _amount) internal { - if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); - new SafeSend{ value: _amount }(payable(_recipient)); - emit LiquidityMinted(_recipient, _amount); -} -``` - -Add `SendETH` and `RelayETH` function to `ETHLiquidity`: - ```solidity /// @notice Sends ETH to some target address on another chain. -/// @param _to Address to send ETH to. +/// @param _to Address to send tokens to. /// @param _chainId Chain ID of the destination chain. function sendETH(address _to, uint256 _chainId) public payable { if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } - _burn(msg.sender, msg.value); + // Burn to ETHLiquidity contract. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); // Send message to other chain. IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ @@ -97,31 +60,48 @@ function sendETH(address _to, uint256 _chainId) public payable { _target: address(this), _message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value)) }); + + // Emit event. emit SendETH(msg.sender, _to, msg.value, _chainId); } /// @notice Relays ETH received from another chain. /// @param _from Address of the msg.sender of sendETH on the source chain. -/// @param _to Address to relay ETH to. -/// @param _amount Amount of ETH to relay. +/// @param _to Address to relay tokens to. +/// @param _amount Amount of tokens to relay. function relayETH(address _from, address _to, uint256 _amount) external { + // Receive message from other chain. IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - if (msg.sender != address(messenger)) revert Unauthorized(); - if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized(); + if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger(); + if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } - _mint(_to, _amount); - emit RelayETH(_from, _to, _amount, messenger.crossDomainMessageSource()); + // Mint from ETHLiquidity contract. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + + // Get source chain ID. + uint256 source = messenger.crossDomainMessageSource(); + + new SafeSend{ value: _amount }(payable(_to)); + + // Emit event. + emit RelayETH(_from, _to, _amount, source); } ``` +### Advantages +- 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. + +### Downsides +- 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. + # Considerations ## SuperchainWETH transfers -This solution does not eliminate the `SuperchainWETH` contract and `SuperchainWETH` transfers would still go through the `SuperchainTokenBridge`. +This solution does not eliminate the `SuperchainWETH` ERC20 token. `SuperchainWETH` ERC20 transfers would still go through the `SuperchainTokenBridge`. ## Custom Gas Token Chains @@ -133,18 +113,18 @@ To simplify the solution, custom gas token chains will not be supported and must # Alternatives Considered -## Integrate `ETH` transfer into `SuperchainWETH` +## Integrate ETH transfers into `ETHLiquidity` contract -Add two new functions to the `SuperchainWETH` contract: `SendETH` and `RelayETH`. +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. -### `SendETH` +### `SendETH` function The `SendETH` function combines the first two transactions as follows: 1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. 2. Sends a message to the destination chain encoding a call to `RelayETH`. -### `RelayETH` +### `RelayETH` function The `RelayETH` function combines the last two transactions as follows: @@ -153,17 +133,56 @@ The `RelayETH` function combines the last two transactions as follows: ### Contract changes +Add an internal `_burn` function that will be called by `ETHLiquidity#SendETH` and update the external `burn` function to call `_burn`: + +```solidity +/// @notice Allows an address to lock ETH liquidity into this contract. +function burn() external payable { + if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + _burn(msg.sender, msg.value); +} + +/// @notice Allows an address to lock ETH liquidity into this contract. +/// @param _sender Address that sent the ETH to be locked. +/// @param _amount The amount of liquidity to burn. +function _burn(address _sender, uint256 _amount) internal { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + emit LiquidityBurned(_sender, _amount); +} +``` + +Add an internal `_mint` function that will be called by `ETHLiquidity#RelayETH` and update the external `mint` function to call `_mint`: + +```solidity +/// @notice Allows an address to unlock ETH liquidity from this contract. +/// @param _amount The amount of liquidity to unlock. +function mint(uint256 _amount) external { + if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + _mint(msg.sender, _amount); +} + +/// @notice Allows an address to unlock ETH liquidity from this contract. +/// @param _recipient Address to send the unlocked ETH to. +/// @param _amount The amount of ETH liquidity to unlock. +function _mint(address _recipient, uint256 _amount) internal { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + new SafeSend{ value: _amount }(payable(_recipient)); + emit LiquidityMinted(_recipient, _amount); +} +``` + +Add `SendETH` and `RelayETH` function to `ETHLiquidity`: + ```solidity /// @notice Sends ETH to some target address on another chain. -/// @param _to Address to send tokens to. +/// @param _to Address to send ETH to. /// @param _chainId Chain ID of the destination chain. function sendETH(address _to, uint256 _chainId) public payable { if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } - // Burn to ETHLiquidity contract. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); + _burn(msg.sender, msg.value); // Send message to other chain. IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ @@ -171,44 +190,31 @@ function sendETH(address _to, uint256 _chainId) public payable { _target: address(this), _message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value)) }); - - // Emit event. emit SendETH(msg.sender, _to, msg.value, _chainId); } /// @notice Relays ETH received from another chain. /// @param _from Address of the msg.sender of sendETH on the source chain. -/// @param _to Address to relay tokens to. -/// @param _amount Amount of tokens to relay. +/// @param _to Address to relay ETH to. +/// @param _amount Amount of ETH to relay. function relayETH(address _from, address _to, uint256 _amount) external { - // Receive message from other chain. IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger(); - if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); + if (msg.sender != address(messenger)) revert Unauthorized(); + if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized(); if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } - // Mint from ETHLiquidity contract. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); - - // Get source chain ID. - uint256 source = messenger.crossDomainMessageSource(); - - new SafeSend{ value: _amount }(payable(_to)); - - // Emit event. - emit RelayETH(_from, _to, _amount, source); + _mint(_to, _amount); + emit RelayETH(_from, _to, _amount, messenger.crossDomainMessageSource()); } ``` ### Advantages - -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. +- All ETH liquidity management occurs within a single, dedicated contract. ### Downsides - -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. +- Since `ETHLiquidity` holds so much ether, we have to be really careful with what functionality is added to it. It should be callable in the most minimal amount of ways. The original design of `ETHLiquidity` intended there to be only a single useful caller. We need to be very careful with any modifications to it ## Integrate `ETH` transfer into `SuperchainTokenBridge` @@ -253,9 +259,7 @@ function relayETH(address _from, address _to, uint256 _amount) external { ``` ### Advantages - -The advantage of this solution is that `SuperchainTokenBridge`would handle both `ETH` transfers and `SuperchainERC20` transfers, simplifying developer integrations. +- The advantage of this solution is that `SuperchainTokenBridge`would handle both `ETH` transfers and `SuperchainERC20` transfers, simplifying developer integrations. ### Downsides - -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. \ No newline at end of file +- 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. \ No newline at end of file From d9c3efcbd44ef07a70289b1be6686b8b7fbdfc97 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 14:59:40 -0800 Subject: [PATCH 03/14] add event signatures --- protocol/interoperable-ether-transfers.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index c2aa9b45..9e0c3172 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -43,6 +43,10 @@ The `RelayETH` function combines the last two transactions as follows: ### Contract changes ```solidity +event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); + +event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); + /// @notice Sends ETH to some target address on another chain. /// @param _to Address to send tokens to. /// @param _chainId Chain ID of the destination chain. From 6f892acc5e4147ef954c7edf7822b72fabd8459a Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 15:21:02 -0800 Subject: [PATCH 04/14] add natspec for events --- protocol/interoperable-ether-transfers.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 9e0c3172..50eb077d 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -43,8 +43,18 @@ The `RelayETH` function combines the last two transactions as follows: ### Contract changes ```solidity +/// @notice Emitted when ETH is sent from one chain to another. +/// @param from Address of the sender. +/// @param to Address of the recipient. +/// @param amount Amount of ETH sent. +/// @param destination Chain ID of the destination chain. event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); +/// @notice Emitted whenever ETH is successfully relayed on this chain. +/// @param from Address of the msg.sender of sendETH on the source chain. +/// @param to Address of the recipient. +/// @param amount Amount of ETH relayed. +/// @param source Chain ID of the source chain. event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Sends ETH to some target address on another chain. From 04e3f9eff27167fb911cb3c1754185b95bd4c084 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 15:22:49 -0800 Subject: [PATCH 05/14] nit --- protocol/interoperable-ether-transfers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 50eb077d..f0c30e3d 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -58,7 +58,7 @@ event SendETH(address indexed from, address indexed to, uint256 amount, uint256 event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Sends ETH to some target address on another chain. -/// @param _to Address to send tokens to. +/// @param _to Address to send ETH to. /// @param _chainId Chain ID of the destination chain. function sendETH(address _to, uint256 _chainId) public payable { if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { @@ -80,9 +80,9 @@ function sendETH(address _to, uint256 _chainId) public payable { } /// @notice Relays ETH received from another chain. -/// @param _from Address of the msg.sender of sendETH on the source chain. -/// @param _to Address to relay tokens to. -/// @param _amount Amount of tokens to relay. +/// @param _from Address of the msg.sender of sendETH on the source chain. +/// @param _to Address to relay ETH to. +/// @param _amount Amount of ETH to relay. function relayETH(address _from, address _to, uint256 _amount) external { // Receive message from other chain. IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); From fb460639753a4e04222ee8c791bfe51a54119016 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 15:48:21 -0800 Subject: [PATCH 06/14] update relayETH --- protocol/interoperable-ether-transfers.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index f0c30e3d..9dba1cb0 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -61,6 +61,8 @@ event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 /// @param _to Address to send ETH to. /// @param _chainId Chain ID of the destination chain. function sendETH(address _to, uint256 _chainId) public payable { + if (_to == address(0)) revert ZeroAddress(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } @@ -84,23 +86,21 @@ function sendETH(address _to, uint256 _chainId) public payable { /// @param _to Address to relay ETH to. /// @param _amount Amount of ETH to relay. function relayETH(address _from, address _to, uint256 _amount) external { - // Receive message from other chain. - IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger(); - if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); + if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) revert Unauthorized(); + + (address crossDomainMessageSender, uint256 source) = + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageContext(); + + if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { revert NotCustomGasToken(); } - // Mint from ETHLiquidity contract. IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); - // Get source chain ID. - uint256 source = messenger.crossDomainMessageSource(); - new SafeSend{ value: _amount }(payable(_to)); - // Emit event. emit RelayETH(_from, _to, _amount, source); } ``` From 9948477d7abc80724444ed3e7fadfafa5cbd64a2 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 15:49:18 -0800 Subject: [PATCH 07/14] update sendETH return value --- protocol/interoperable-ether-transfers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 9dba1cb0..d3260f5b 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -60,7 +60,7 @@ event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 /// @notice Sends ETH to some target address on another chain. /// @param _to Address to send ETH to. /// @param _chainId Chain ID of the destination chain. -function sendETH(address _to, uint256 _chainId) public payable { +function sendETH(address _to, uint256 _chainId) public payable returns (bytes32 msgHash_) { if (_to == address(0)) revert ZeroAddress(); if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { @@ -71,7 +71,7 @@ function sendETH(address _to, uint256 _chainId) public payable { IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); // Send message to other chain. - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ + msgHash_ = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ _destination: _chainId, _target: address(this), _message: abi.encodeCall(this.relayETH, (msg.sender, _to, msg.value)) From f77848f39a643d4c7ecd5cba210c817d0ce8883f Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 16:01:07 -0800 Subject: [PATCH 08/14] make sendETH external --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index d3260f5b..097c6ec4 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -60,7 +60,7 @@ event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 /// @notice Sends ETH to some target address on another chain. /// @param _to Address to send ETH to. /// @param _chainId Chain ID of the destination chain. -function sendETH(address _to, uint256 _chainId) public payable returns (bytes32 msgHash_) { +function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_) { if (_to == address(0)) revert ZeroAddress(); if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { From 5178c74249667a6455d8a716fa649453c9a9d976 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Nov 2024 19:08:30 -0800 Subject: [PATCH 09/14] cleanup --- protocol/interoperable-ether-transfers.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 097c6ec4..3b4b88bf 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -4,7 +4,7 @@ This document exists to align on a design for simplifying the process for sendin # Summary -New functionality is introduced to the `ETHLiquidity` contract that enables it to facilitate cross-chain `ETH` transfers to a specified recipient. +New functionality is introduced to the `SuperchainWETH` contract that enables it to facilitate cross-chain `ETH` transfers to a specified recipient. # Problem Statement + Context @@ -17,8 +17,8 @@ Currently, L2-to-L2 `ETH` transfers between two interoperable chains require fou The goal is to reduce the transaction count from four to two, enabling users to send `ETH` to a destination chain directly: -1. Burn `ETH` on source chain and relay a message to destination chain that mints `ETH` to recipient on destination chain. -2. Execute the relayed message on the destination chain that mints `ETH` to the recipient. +1. Burn `ETH` on source chain and relay a message to destination chain that relays `ETH` to recipient on destination chain. +2. Execute the relayed message on the destination chain that relays `ETH` to the recipient. # Proposed Solution @@ -94,7 +94,11 @@ function relayETH(address _from, address _to, uint256 _amount) external { if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender(); if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { - revert NotCustomGasToken(); + _mint(_to, _amount); + + emit RelayETH(_from, _to, _amount, source); + + return; } IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); @@ -119,10 +123,10 @@ This solution does not eliminate the `SuperchainWETH` ERC20 token. `SuperchainWE ## Custom Gas Token Chains -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`. +The `SendETH` function will not support custom gas token chains and will revert if called on a custom gas token chain. If the `RelayETH` function is called on a custom gas token chain, it will mint `SuperchainWETH` to the recipient. # Open Questions -- **Long-term improvements**: Could this functionality eventually extend to custom gas token chains? +- **Long-term improvements**: Could the `SendETH` functionality eventually extend to custom gas token chains? - **Rollbacks**: How would rollbacks be handled in this implementation? # Alternatives Considered From 35f577c4f415b1e67a4a3719ccbb57f6c65d3d70 Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Nov 2024 10:11:14 -0800 Subject: [PATCH 10/14] Update protocol/interoperable-ether-transfers.md Co-authored-by: Skeletor Spaceman <92943766+skeletor-spaceman@users.noreply.github.com> --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 3b4b88bf..10a42c98 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -17,7 +17,7 @@ Currently, L2-to-L2 `ETH` transfers between two interoperable chains require fou The goal is to reduce the transaction count from four to two, enabling users to send `ETH` to a destination chain directly: -1. Burn `ETH` on source chain and relay a message to destination chain that relays `ETH` to recipient on destination chain. +1. Locks `ETH` on source chain and relay a message to destination chain that relays `ETH` to recipient on destination chain. 2. Execute the relayed message on the destination chain that relays `ETH` to the recipient. # Proposed Solution From 9bbc779aa3ef15123d47752c0f8249e11f9650ce Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Nov 2024 10:11:26 -0800 Subject: [PATCH 11/14] Update protocol/interoperable-ether-transfers.md Co-authored-by: Skeletor Spaceman <92943766+skeletor-spaceman@users.noreply.github.com> --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 10a42c98..143b08bd 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -30,7 +30,7 @@ Add two new functions to the `SuperchainWETH` contract: `SendETH` and `RelayETH` The `SendETH` function combines the first two transactions as follows: -1. Burns `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. +1. Locks `ETH` within the `ETHLiquidity` contract equivalent to the `ETH` sent. 2. Sends a message to the destination chain encoding a call to `RelayETH`. ### `RelayETH` From 6e8a7e9439b812a117ffdcb02873432457a04fdc Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Nov 2024 10:11:34 -0800 Subject: [PATCH 12/14] Update protocol/interoperable-ether-transfers.md Co-authored-by: Skeletor Spaceman <92943766+skeletor-spaceman@users.noreply.github.com> --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 143b08bd..2f577279 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -37,7 +37,7 @@ The `SendETH` function combines the first two transactions as follows: The `RelayETH` function combines the last two transactions as follows: -1. Mints the specified `_amount` of `ETH` from the `ETHLiquidity` contract. +1. Withdraws the specified `_amount` of `ETH` from the `ETHLiquidity` contract. 2. Sends the minted `ETH` to the specified recipient. ### Contract changes From d469ea1090c36f478b2c2316793b5461015a5c71 Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Nov 2024 10:11:54 -0800 Subject: [PATCH 13/14] Update protocol/interoperable-ether-transfers.md Co-authored-by: Skeletor Spaceman <92943766+skeletor-spaceman@users.noreply.github.com> --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 2f577279..2a1d09b7 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -110,7 +110,7 @@ function relayETH(address _from, address _to, uint256 _amount) external { ``` ### Advantages -- 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. +- Since the `SuperchainWETH` contract already has permissions to withdraw and lock `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. ### Downsides - 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. From 6f5b008e6af133067402783f4652801099d28e73 Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Nov 2024 10:12:00 -0800 Subject: [PATCH 14/14] Update protocol/interoperable-ether-transfers.md Co-authored-by: Skeletor Spaceman <92943766+skeletor-spaceman@users.noreply.github.com> --- protocol/interoperable-ether-transfers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/interoperable-ether-transfers.md b/protocol/interoperable-ether-transfers.md index 2a1d09b7..e8c421aa 100644 --- a/protocol/interoperable-ether-transfers.md +++ b/protocol/interoperable-ether-transfers.md @@ -133,7 +133,7 @@ The `SendETH` function will not support custom gas token chains and will revert ## Integrate ETH transfers into `ETHLiquidity` contract -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. +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 withdrawal and locking of `ETH` for cross-chain transfers, ensuring that all liquidity management occurs within a single, dedicated contract. ### `SendETH` function