Skip to content

Commit

Permalink
feat(protoype): enable superchainWETH native transfers
Browse files Browse the repository at this point in the history
  • Loading branch information
tremarkley committed Nov 19, 2024
1 parent f94151b commit cacf39d
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 4 deletions.
69 changes: 66 additions & 3 deletions packages/contracts-bedrock/src/L2/SuperchainWETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
194 changes: 193 additions & 1 deletion packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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
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.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}

0 comments on commit cacf39d

Please sign in to comment.