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

design-doc: interoperable ether #25

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions protocol/interoperable-ether.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Purpose

This document exists to align on a design for interoperable `ether`. Without this, user experience would
be greatly hurt and `ether` is commonly used to pay for gas, which is necessary to use a chain.

# Summary

A new `WETH` predeploy is introduced that adds the `SuperchainERC20` functionality. It has a simple migration path from
legacy `WETH` and maintains the ability to have interop with custom gas token chains.

# Problem Statement + Context

The enshrinement of native asset sends between chains was removed as part of the interop design. This greatly reduced
the scope of the project as a native ether send feature could be subject to a double spend attack. Instead, we opt
for using `WETH` to send between chains. The existing `WETH` contract is not proxied meaning that it cannot easily
be upgraded. It also existed from before the times of `SuperchainERC20`, meaning that it has no built in cross chain
sending ability.
tynes marked this conversation as resolved.
Show resolved Hide resolved

With the introduction of custom gas token, the existing `WETH` predeploy now has the semantics of "wrapped native asset",
meaning that the `WETH` predeploy is not guaranteed to be `WETH`. This means one of two things:

- A new `WETH` predeploy is introduced that supports `SuperchainERC20`
Copy link
Contributor

@mds1 mds1 Jun 20, 2024

Choose a reason for hiding this comment

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

So the code currently in contracts-bedrock/src/L2/WETH.sol is renamed to something like WrappedNativeToken.sol and is still deployed at 0x4200000000000000000000000000000000000006, then a new SuperchainERC20-comatible WETH.sol is added and placed at a different address?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes exactly

- Custom gas token chains can never be interoperable

# Proposed Solution

A new `WETH` predeploy is introduced that supports interfaces `ISuperchainERC20`.
This means that the new `WETH` predeploy allows for cross chain transfers.

The main problem with this solution is the fact that it will break liquidity for wrapped `ether` into
2 different contracts. Many applications already exist today that integrate with the existing predeploy.
This will be annoying, but it should be very simple to migrate from the old `WETH` to the new `WETH`.
Its a simple unwrap and wrap, and technically we could build support directly into the new `WETH` contract

Choose a reason for hiding this comment

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

Relevant holders for OP Mainnet, looks like a mix of Aave and bridges: https://optimistic.etherscan.io/token/0x4200000000000000000000000000000000000006#balances

to make the migration extra simple.

An important problem to solve is ensuring that there are no liquidity constraints. If a wrap/unwrap mechanism
is used rather than a mint/burn mechanism, it will result in liquidity constraints. Therefore we prefer a
mint/burn mechanism. We need a solution to the liquidity constraint that specifically happens when a user is
trying to send ether to a remote domain. They have to wrap the ether into weth and then it gets sent between
chains as weth and then we need to unwrap the weth. Its possible that there isn’t enough weth to unwrap on the other side.

We introduce 2 new predeploys

- `SuperchainWETH`
- Implements `SuperchainERC20`
- Guaranteed to be wrapped ether rather than wrapped native asset
- `ETHLiquidity`
- Contains an artificially large pool of `ether` to provide liquidity for cross chain sends
- We “burn” ether by sending it here and “mint” ether by pulling it out
- Only `SuperchainWETH` can interact with this contract
- Only works on non custom gas token chains
- This is similar to the Scroll bridge where in genesis they mint a ton of ether into the contract where it can only be unlocked via a L1 to L2 deposit
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have a docs link where I can learn more? Searched briefly but didn't see 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.

I cant find docs on it but you can see on the block explorer here: https://scrollscan.com/address/0x781e90f1c8fc4611c9b7497c3b47f99ef6969cbc

The balance is absurdly high

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, I was able to find it in the genesis.json of their docs here based on the balance of that account at block 0

- Placed in genesis with a balance equal to `type(uint248).max`

The important invariant is that `ether` cannot be withdrawn from the `SuperchainWETH` contract by entities that do not own it.
This would result in inflating the supply of ether.

### SuperchainWETH

- Users can send ether between chains as this contract will handle the wrapping and unwrapping, this only works when
the source and destination are not custom gas token
- Users can send ether from a non custom gas token chain to a custom gas token chain and it will end up as weth
- Users can send weth from a custom gas token chain to a non custom gas token chain and it will end up as ether
- Users can send weth between custom gas token chains

```solidity
contract SuperchainWETH is WETH98, ISuperchainERC20 {
L1Block internal l1Block = L1Block(Predeploys.L1_BLOCK);
L2ToL2CrossDomainMessenger messenger = L2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSSDOMAIN_MESSENGER);
ETHLiquidity liquidity = ETHLiquidity(Predeploys.ETH_LIQUIDITY);

function deposit() public payable override {
if (l1block.isCustomGasToken()) revert IsCustomGasToken();
super.deposit();
}

function withdraw(uint256 wad) public override {
if (l1block.isCustomGasToken()) revert IsCustomGasToken();
super.withdraw(wad);
}

function sendETHTo(address to, uint256 chainId) public payable {
if (l1block.isCustomGasToken()) revert IsCustomGasToken();

liquidity.burn{ value: msg.value }();

sendMessage({
_destination: chainId,
_message: abi.encodeCall(SuperchainWETH.relayETH, (to, msg.value))
});
}

function sendERC20(uint256 wad, uint256 chainId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

From the user perspective, we are calling a WETH contract to move Ether (either natively, or in wrapped form between chains). Therefore I think it would be nice if we can simplify the interface to remove unify the sendETHTo and sendERC20To method into a single sendWETHTo method. Similar for relayETH vs. relayERC20. This method would be have conditionals of course

function sendWETHTo(address to, uint256 wad, uint256 chainId) external payable {
  if (l1block.isCustomGasToken()) {
    require(msg.value == 0);
  } else {
    require(msg.value == wad);
  }
}

I'd also be ok with adding an overload that has no wad input for chains where the gas token is ETH. But having the (W)ETH holder worry about the chain's gas token with different named methods feels confusing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this could certainly simplify things, this is a good suggestion. Ideally there is a smaller interface

sendERC20To(msg.sender, wad, chainID);
}

function sendERC20To(address to, uint256 wad, uint256 chainId) external {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is named sendERC20To instead of sendWETHTo to avoid users confusing.fat-fingering it with sendETHTo, right or there's any other reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The interface is meant to satisfy SuperchainERC20 which is why it has this naming, per ethereum-optimism/specs#71

require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;

if (l1Block.isCustomGasToken() == false) {
liquidity.burn{ value: wad }();
}

sendMessage({
_destination: chainId,
_message: abi.encodeCall(SuperchainWETH.relayERC20, (to, wad))
});
}

function sendMessage(uint256 _destination, bytes memory _message) internal {
messenger.sendMessage({
_destination: _destination,
_target: address(this),
_message: _message
});
}

function relayERC20(address to, uint256 amount) external {
if (msg.sender != Predeploys.L2_TO_L2_CROSSDOMAIN_MESSENGER) revert Unauthorized();
if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized();

if (l1Block.isCustomGasToken() == false) {
liquidity.source(amount);
}

balanceOf[to] += amount;
}

function relayETH(address to, uint256 amount) external {
if (l1block.isCustomGasToken()) revert IsCustomGasToken();
if (msg.sender != Predeploys.L2_TO_L2_CROSSDOMAIN_MESSENGER) revert Unauthorized();
if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized();

liquidity.source(amount);

bool success = SafeCall.transfer({ _target: to, _amount: amount });
require(success);
}
}

// Placed in genesis with a balance equal to type(uint248).max
contract ETHLiquidity {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this solution, this predeploy is the only thing that unnerves me a bit. Mostly because Scroll has it contained in a single chain, and here we will have it in many chains that interact with one another. This wouldn't be needed with the approach of allowing the L1StandardBridge to wrap ether into WETH and minting this in the L2s, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a problem where when sending ether between 2 chains, there is no way to "mint" the ether on the remote chain. We can mint as much WETH as we want from within the protocol, but we are unable to unwrap that arbitrarily unless we stick a ton of ether into state that can only be unlocked

address weth internal = Predeploys.SUPERCHAIN_WETH;
L1Block internal l1Block = L1Block(Predeploys.L1_BLOCK);

function burn() external payable {
if (msg.sender != weth) revert Unauthorized();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also have if (l1Block.isCustomGasToken()) revert OnlyEther()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if we need this necessarily, if we want weth.totalSupply() to be more accurate we may need to call burn on ether paying chains

}

function source(uint256 amount) {
Comment on lines +147 to +151
Copy link
Contributor

Choose a reason for hiding this comment

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

naming nit: burn and source don't sound like inverses—maybe lock/unlock or burn/mint? either way, not a blocker and can defer on this until the spec is written

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can have the naming nit convo on the specs PR

if (msg.sender != weth) revert Unauthorized();
if (l1Block.isCustomGasToken()) revert OnlyEther();
require(SafeCall.transfer({ _target: weth, _value: amount }));
}
}
```

### Longer term nice to haves

- Enable ETH interface on `StandardBridge` for custom gas token chains and have it mint `SuperchainWETH` for deposits
- We can call this out of scope for now, but a future upgrade can make `SuperchainWETH` an `OptimismMintableERC20` to enable this
Comment on lines +161 to +162
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you expand on what "Enable ETH interface" means? What are the benefits/tradeoffs of this nice to have?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The following ABI on the StandardBridge is disabled for custom gas token chains:

  • bridgeETH(uint32,bytes)
  • bridgeETHTo(address,uint32,bytes)
  • receive()

You could imagine enabling these functions and having the ether deposits become WETH on L2 on custom gas token chains automatically


# Open Questions

## Naming

We need to come up with a new name for this "superchain wrapped ether" to differentiate it. Not sure
if it should be called `SWETH` or just stick with `WETH`. Without a different name, it will be confusing,
but then it will create more overhead to get people to understand that its portable `WETH`.
Comment on lines +168 to +170
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good question, applies to generic SuperchainERC20 tokens too


Since the usage of custom gas token is legible from within both L1 and L2, this means that the superchain `WETH`
can block deposits of native asset when its a custom gas token chain.

## `IOptimismMintableERC20` Support

It is also possible to add in support for `IOptimismMintableERC20` so that `WETH` can be deposited directly into
this predeploy. This would solve the problem of having `ETH` and native asset liquidity on the L2, since the chain
Copy link
Contributor

Choose a reason for hiding this comment

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

By "this predeploy" do you mean SuperchainWETH or ETHLiquidity?

operator likely needs to sell the earned native asset into `ether` to pay for DA. This could look like the `L1StandardBridge`
automatically converting `ether` into `WETH` and then depositing it such that it mints the superchain `WETH` on L2.
Without this solution, it means there is no easy way to get fungible `ether` on to a custom gas token chain.

# Alternatives Considered

## Upgrade Existing WETH Predeploy

The existing `WETH` predeploy is not proxied, meaning the only way to upgrade
it is with an irregular state transition. This is not a solution that we should
take often, it adds technical debt that must be implemented in every execution
layer client that supports OP Stack.

Following this decision would be inconsistent with previous decision making where
we decided specifically to remove native ether sends from within the protocol
so that custom gas token chains can be interoperable.

# Risks & Uncertainties

- This adds a lot of bridge risk as its a new way to send `ether` between chains. We need to be sure to think in terms of state machines and invariants.
Copy link
Contributor

Choose a reason for hiding this comment

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

Other risks:

  • Placing type(uint248).max into ETHLiquidity, since we might introduce a bug that lets a disproportionate amount of ETH be withdrawn
  • User confusion: Lots of new concepts, naming questions, etc.
  • Are there any risks or footguns we introduce here by splitting up WETH supply between two predeploys? Want to think on the consequences there more

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are great to add to the failure mode analysis