From cacf39d69972dd9a16966ea04682983653495223 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 28 Oct 2024 15:33:27 -0700 Subject: [PATCH] feat(protoype): enable superchainWETH native transfers --- .../src/L2/SuperchainWETH.sol | 69 ++++++- .../src/L2/interfaces/ISuperchainWETH.sol | 8 + .../test/L2/SuperchainWETH.t.sol | 194 +++++++++++++++++- 3 files changed, 267 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 29e179eba82ce..4a279cb0c8469 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -5,16 +5,18 @@ pragma solidity 0.8.15; import { WETH98 } from "src/universal/WETH98.sol"; // Libraries +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; +import { SafeSend } from "src/universal/SafeSend.sol"; // Interfaces import { ISemver } from "src/universal/interfaces/ISemver.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { IL1Block } from "src/L2/interfaces/IL1Block.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000024 @@ -23,9 +25,26 @@ import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErro /// within the superchain. SuperchainWETH can be converted into native ETH on chains that /// do not use a custom gas token. contract SuperchainWETH is WETH98, IERC7802, ISemver { + /// @notice Thrown when attempting to relay a message and the cross domain message sender is not SuperchainWETH. + error InvalidCrossDomainSender(); + + /// @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 Semantic version. - /// @custom:semver 1.0.0-beta.10 - string public constant version = "1.0.0-beta.10"; + /// @custom:semver 1.0.0-beta.11 + string public constant version = "1.0.0-beta.11"; /// @inheritdoc WETH98 function deposit() public payable override { @@ -98,4 +117,48 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId || _interfaceId == type(IERC165).interfaceId; } + + /// @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) external payable returns (bytes32 msgHash_) { + if (_to == address(0)) revert ZeroAddress(); + + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); + + 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 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 { + 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(); + } + + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + + new SafeSend{ value: _amount }(payable(_to)); + + emit RelayETH(_from, _to, _amount, source); + } } diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol index fa10b237dbb53..4a4bbe35372f3 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol @@ -8,10 +8,18 @@ import { ISemver } from "src/universal/interfaces/ISemver.sol"; interface ISuperchainWETH is IWETH98, IERC7802, ISemver { error Unauthorized(); error NotCustomGasToken(); + error InvalidCrossDomainSender(); + error ZeroAddress(); + + event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); + + event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); function balanceOf(address src) external view returns (uint256); function withdraw(uint256 _amount) external; function supportsInterface(bytes4 _interfaceId) external view returns (bool); + function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_); + function relayETH(address _from, address _to, uint256 _amount) external; function __constructor__() external; } diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e342a53b6026b..eaf764dbdc175 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -6,7 +6,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; // Interfaces @@ -14,6 +14,7 @@ import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; /// @title SuperchainWETH_Test /// @notice Contract for testing the SuperchainWETH contract. @@ -33,6 +34,10 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Emitted when a crosschain transfer burns tokens. event CrosschainBurn(address indexed from, uint256 amount); + event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); + + event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); + address internal constant ZERO_ADDRESS = address(0); /// @notice Test setup. @@ -475,4 +480,191 @@ contract SuperchainWETH_Test is CommonTest { vm.assume(_interfaceId != type(IERC20).interfaceId); assertFalse(superchainWeth.supportsInterface(_interfaceId)); } + + + /// @notice Tests the `sendETH` function reverts when the address `_to` is zero. + function testFuzz_sendETH_zeroAddressTo_reverts(address _sender, uint256 _amount, uint256 _chainId) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + vm.deal(_sender, _amount); + vm.prank(_sender); + // Call the `sendETH` function with the zero address as `_to` + superchainWeth.sendETH{ value: _amount }(ZERO_ADDRESS, _chainId); + } + + /// @notice Tests the `sendETH` function burns the sender ETH, sends the message, and emits the `SendETH` + /// event. + function testFuzz_sendETH_fromNonCustomGasTokenChain_succeeds( + address _sender, + address _to, + uint256 _amount, + uint256 _chainId, + bytes32 _msgHash + ) + external + { + // Assume + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + + // Get the total balance of `_sender` before the send to compare later on the assertions + uint256 _senderBalanceBefore = _sender.balance; + + // Look for the emit of the `SendETH` event + vm.expectEmit(address(superchainWeth)); + emit SendETH(_sender, _to, _amount, _chainId); + + // Expect the call to the `burn` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 1); + + // Mock the call over the `sendMessage` function and expect it to be called properly + bytes memory _message = + abi.encodeCall(superchainWeth.relayETH, (_sender, _to, _amount)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall( + IL2ToL2CrossDomainMessenger.sendMessage, (_chainId, address(superchainWeth), _message) + ), + abi.encode(_msgHash) + ); + + // Call the `sendETH` function + vm.prank(_sender); + bytes32 _returnedMsgHash = superchainWeth.sendETH{ value: _amount }(_to, _chainId); + + // Check the message hash was generated correctly + assertEq(_msgHash, _returnedMsgHash); + + // Check the total supply and balance of `_sender` after the send were updated correctly + assertEq(_sender.balance, _senderBalanceBefore - _amount); + } + + /// @notice Tests the `sendETH` function reverts when called on a custom gas token chain. + function testFuzz_sendETH_fromCustomGasTokenChain_fails( + address _sender, + address _to, + uint256 _amount, + uint256 _chainId + ) + external + { + // Assume + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Call the `sendETH` function + vm.prank(_sender); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.sendETH{ value: _amount }(_to, _chainId); + } + + /// @notice Tests the `relayETH` function reverts when the caller is not the L2ToL2CrossDomainMessenger. + function testFuzz_relayETH_notMessenger_reverts( + address _caller, + address _to, + uint256 _amount + ) + public + { + // Ensure the caller is not the messenger + vm.assume(_caller != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(Unauthorized.selector); + + // Call the `relayETH` function with the non-messenger caller + vm.prank(_caller); + superchainWeth.relayETH(_caller, _to, _amount); + } + + /// @notice Tests the `relayETH` function reverts when the `crossDomainMessageSender` that sent the message is not + /// the same SuperchainWETH. + function testFuzz_relayETH_notCrossDomainSender_reverts( + address _crossDomainMessageSender, + uint256 _source, + address _to, + uint256 _amount + ) + public + { + vm.assume(_crossDomainMessageSender != address(superchainWeth)); + + // Mock the call over the `crossDomainMessageContext` function setting a wrong sender + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), + abi.encode(_crossDomainMessageSender, _source) + ); + + // Expect the revert with `InvalidCrossDomainSender` selector + vm.expectRevert(ISuperchainWETH.InvalidCrossDomainSender.selector); + + // Call the `relayETH` function with the sender caller + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainWeth.relayETH(_crossDomainMessageSender, _to, _amount); + } + + /// @notice Tests the `relayETH` function reverts when called on a custom gas token chain. + function testFuzz_relayETH_fromCustomGasTokenChain_fails(address _from, address _to, uint256 _amount, uint256 _source) public { + // Assume + vm.assume(_to != ZERO_ADDRESS); + + // Arrange + vm.deal(address(superchainWeth), _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), + abi.encode(address(superchainWeth), _source) + ); + + // Act + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.relayETH(_from, _to, _amount); + } + + /// @notice Tests the `relayETH` function relays the proper amount of ETH and emits the `RelayETH` event. + function testFuzz_relayETH_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { + // Assume + vm.assume(_to != ZERO_ADDRESS); + assumePayable(_to); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(address(superchainWeth), _amount); + vm.deal(Predeploys.ETH_LIQUIDITY, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), + abi.encode(address(superchainWeth), _source) + ); + + uint256 _toBalanceBefore = _to.balance; + + // Look for the emit of the `RelayETH` event + vm.expectEmit(address(superchainWeth)); + emit RelayETH(_from, _to, _amount, _source); + + // Expect the call to the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 1); + + // Call the `RelayETH` function with the messenger caller + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainWeth.relayETH(_from, _to, _amount); + + assertEq(_to.balance, _toBalanceBefore + _amount); + } }