Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interop: interoperable ether transfers design doc #146

Merged
merged 15 commits into from
Nov 26, 2024
Merged
190 changes: 104 additions & 86 deletions protocol/interoperable-ether-transfers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
tremarkley marked this conversation as resolved.
Show resolved Hide resolved
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:

Expand All @@ -40,88 +42,80 @@ 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);
}
```
/// @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);

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 {
function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_) {
if (_to == address(0)) revert ZeroAddress();

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({
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))
});

// 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 _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 (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();
Comment on lines +89 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NTH: it'd be cool to have this "am I being called by myself?" abstracted into a "CDM library"


if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
revert NotCustomGasToken();
}

_mint(_to, _amount);
emit RelayETH(_from, _to, _amount, messenger.crossDomainMessageSource());
IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount);

new SafeSend{ value: _amount }(payable(_to));

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.
tremarkley marked this conversation as resolved.
Show resolved Hide resolved

### 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

Expand All @@ -133,18 +127,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.
tremarkley marked this conversation as resolved.
Show resolved Hide resolved

### `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.
Copy link
Contributor

@hamdiallam hamdiallam Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not burning WETH, how about we add these functions to the ETHLiquidity contract?

I think it makes intent a bit clearer and keeps WETH completely separate as just an ERC20? No need to overload the ERC20 contract

Copy link
Contributor Author

@tremarkley tremarkley Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it definitely simplifies things and creates better separation of concerns to leave SuperchainWETH as-is and move this into ETHLiquidity, so I'm onboard with that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. I originally designed it so that there could be only a single useful caller. We need to be very careful with any modifications to it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tynes are you onboard with the suggested solution or do you think it is too risky of a change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely see the concern on additions to ETHLiquidity. But want to note that even if this is in SuperchainWETH, a vulnerability there would also mean a vulnerability in ETHLiquidity since the relay mechanism pulls ETH from this contract

tremarkley marked this conversation as resolved.
Show resolved Hide resolved
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:

Expand All @@ -153,62 +147,88 @@ 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);
}
```
Comment on lines +154 to +190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this outdated?
not sure how this fits with the current design.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is up to date. this design is under the Alternatives Considered section and is meant to illustrate a design that was considered for adding this functionality to the ETHLiquidity contract


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({
_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.
/// @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`

Expand Down Expand Up @@ -253,9 +273,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.
- 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.