Skip to content

Commit

Permalink
Merge pull request #167 from ethereum-optimism/harry/superchain_weth_…
Browse files Browse the repository at this point in the history
…wrapper

feat: `SuperchainETHWrapper` contract
  • Loading branch information
tremarkley authored Oct 25, 2024
2 parents fa00666 + c5d49fb commit 1c8ae46
Show file tree
Hide file tree
Showing 13 changed files with 513 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ In a few seconds, you should see the RelayedMessage on chain 902:

```sh
# example
INFO [08-30|14:30:14.698] L2ToL2CrossChainMessenger#RelayedMessage sourceChainID=901 destinationChainID=902 nonce=0 sender=0x4200000000000000000000000000000000000028 target=0x4200000000000000000000000000000000000028
INFO [08-30|14:30:14.698] SuperchainTokenBridge#RelayERC20 token=0x420beeF000000000000000000000000000000001 from=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 amount=1000 source=901
```
**5. Check the balance on chain 902**

Expand Down
8 changes: 8 additions & 0 deletions contracts/script/DeployL2PeripheryContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.25;

import {Script, console} from "forge-std/Script.sol";
import {L2NativeSuperchainERC20} from "../src/L2NativeSuperchainERC20.sol";
import {SuperchainETHWrapper} from "../src/SuperchainETHWrapper.sol";

contract DeployL2PeripheryContracts is Script {
/// @notice Used for tracking the next address to deploy a periphery contract at.
Expand Down Expand Up @@ -31,6 +32,7 @@ contract DeployL2PeripheryContracts is Script {

function run() public broadcast {
deployL2NativeSuperchainERC20();
deploySuperchainETHWrapper();
}

function deployL2NativeSuperchainERC20() public {
Expand All @@ -39,6 +41,12 @@ contract DeployL2PeripheryContracts is Script {
console.log("Deployed L2NativeSuperchainERC20 at address: ", deploymentAddress);
}

function deploySuperchainETHWrapper() public {
address _superchainETHWrapperContract = address(new SuperchainETHWrapper{salt: _salt()}());
address deploymentAddress = deployAtNextDeploymentAddress(_superchainETHWrapperContract.code);
console.log("Deployed SuperchainETHWrapper at address: ", deploymentAddress);
}

function deployAtNextDeploymentAddress(bytes memory newRuntimeBytecode)
internal
returns (address _deploymentAddr)
Expand Down
86 changes: 86 additions & 0 deletions contracts/src/SuperchainETHWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {Unauthorized} from "@contracts-bedrock/libraries/errors/CommonErrors.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import {SafeCall} from "@contracts-bedrock//libraries/SafeCall.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {ISuperchainTokenBridge} from "@contracts-bedrock/L2/interfaces/ISuperchainTokenBridge.sol";
import {ISuperchainWETH} from "@contracts-bedrock/L2/interfaces/ISuperchainWETH.sol";

/**
* @notice This contract has not been audited. It may contain bugs or security vulnerabilities.
* We are not liable for any issues arising from its use. It is strongly advised that this
* contract not be used with actual funds and should only be used for testing on
* testnets or in a controlled development environment.
*/

/**
* @notice Thrown when the relay of SuperchainWETH has not succeeded.
* @dev This error is triggered if the SuperchainWETH relay through the L2ToL2CrossDomainMessenger
* has not completed successfully successful.
*/
error RelaySuperchainWETHNotSuccessful();

/**
* @title SuperchainETHWrapper
* @notice This contract facilitates sending ETH across chains within the Superchain by wrapping
* ETH into SuperchainWETH, relaying the wrapped asset to another chain, and then
* unwrapping it back to ETH on the destination chain.
* @dev The contract integrates with the SuperchainWETH contract for wrapping and unwrapping ETH,
* and uses the L2ToL2CrossDomainMessenger for relaying the wrapped ETH between chains.
*/
contract SuperchainETHWrapper {
/**
* @dev Emitted when ETH is received by the contract.
* @param from The address that sent ETH.
* @param value The amount of ETH received.
*/
event ETHReceived(address indexed from, uint256 value);

// Fallback function to receive ETH
receive() external payable {
emit ETHReceived(msg.sender, msg.value);
}

/**
* @notice Unwraps SuperchainWETH into native ETH and sends it to a specified destination address
* then calls an arbitrary function at the destination..
* @param _relayERC20MsgHash The hash of the relayed ERC20 message.
* @param _dst The destination address on the receiving chain.
* @param _wad The amount of SuperchainWETH to unwrap to ETH.
* @param _calldata The calldata to be passed in the call to the destination address.
*/
function unwrapAndCall(bytes32 _relayERC20MsgHash, address _dst, uint256 _wad, bytes memory _calldata) 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 (messenger.successfulMessages(_relayERC20MsgHash) == false) {
revert RelaySuperchainWETHNotSuccessful();
}

ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).withdraw(_wad);
SafeCall.call(_dst, _wad, _calldata);
}

/**
* @notice Wraps ETH into SuperchainWETH and sends it to another chain.
* @dev This function wraps the sent ETH into SuperchainWETH, computes the relay message hash,
* and relays the message to the destination chain.
* @param _dst The destination address on the receiving chain.
* @param _chainId The ID of the destination chain.
* @param _calldata The calldata for the function to be called on the destination chain after unwrapping.
*/
function sendETH(address _dst, uint256 _chainId, bytes memory _calldata) public payable {
ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).deposit{value: msg.value}();
bytes32 messageHash = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE).sendERC20(
Predeploys.SUPERCHAIN_WETH, address(this), msg.value, _chainId
);
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
_destination: _chainId,
_target: address(this),
_message: abi.encodeCall(this.unwrapAndCall, (messageHash, _dst, msg.value, _calldata))
});
}
}
220 changes: 220 additions & 0 deletions contracts/test/SuperchainETHWrapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {Test} from "forge-std/Test.sol";

import {Unauthorized} from "@contracts-bedrock/libraries/errors/CommonErrors.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {ISuperchainTokenBridge} from "@contracts-bedrock/L2/interfaces/ISuperchainTokenBridge.sol";
import {ISuperchainWETH} from "@contracts-bedrock/L2/interfaces/ISuperchainWETH.sol";
import {IWETH} from "@contracts-bedrock/universal/interfaces/IWETH.sol";
import {SuperchainWETH} from "@contracts-bedrock/L2/SuperchainWETH.sol";

import {SuperchainETHWrapper, RelaySuperchainWETHNotSuccessful} from "../src/SuperchainETHWrapper.sol";

/// @title SuperchainETHWrapper Happy Path Tests
/// @notice This contract contains the tests for successful paths in SuperchainETHWrapper.
contract SuperchainETHWrapper_HappyPath_Test is Test {
SuperchainETHWrapper public superchainETHWrapper;
address bob;

/// @notice Helper function to setup a mock and expect a call to it.
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
vm.mockCall(_receiver, _calldata, _returned);
vm.expectCall(_receiver, _calldata);
}

/// @notice Helper function to setup a mock and expect a call to it.
function _mockAndExpect(address _receiver, uint256 _msgValue, bytes memory _calldata, bytes memory _returned)
internal
{
vm.mockCall(_receiver, _msgValue, _calldata, _returned);
vm.expectCall(_receiver, _msgValue, _calldata);
}

/// @notice Sets up the test suite.
function setUp() public {
superchainETHWrapper = new SuperchainETHWrapper();
SuperchainWETH superchainWETH = new SuperchainWETH();
vm.etch(Predeploys.SUPERCHAIN_WETH, address(superchainWETH).code);
bob = makeAddr("bob");
}

/// @notice Tests the `sendETH` function deposits the sender's tokens, calls
/// SuperchainWETH.sendERC20, and sends an encoded call to
/// SuperchainETHWrapper.unwrapAndCallAndCall through L2ToL2CrossDomainMessenger.
function testFuzz_sendETH_succeeds(
address _sender,
address _to,
uint256 _amount,
uint256 _chainId,
bytes32 messageHash,
bytes memory _calldata
) public {
vm.assume(_chainId != block.chainid);
_amount = bound(_amount, 0, type(uint248).max - 1);
vm.deal(_sender, _amount);
_mockAndExpect(
Predeploys.SUPERCHAIN_WETH, _amount, abi.encodeWithSelector(IWETH.deposit.selector), abi.encode("")
);
_mockAndExpect(
Predeploys.SUPERCHAIN_TOKEN_BRIDGE,
abi.encodeCall(
ISuperchainTokenBridge.sendERC20,
(Predeploys.SUPERCHAIN_WETH, address(superchainETHWrapper), _amount, _chainId)
),
abi.encode(messageHash)
);
bytes memory _message =
abi.encodeCall(superchainETHWrapper.unwrapAndCall, (messageHash, _to, _amount, _calldata));
_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeWithSelector(
IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainETHWrapper), _message
),
abi.encode("")
);

vm.prank(_sender);
superchainETHWrapper.sendETH{value: _amount}(_to, _chainId, _calldata);
}

/**
* @notice Tests the successful execution of the `unwrapAndCall` function.
* @dev This test mocks the `crossDomainMessageSender` and `successfulMessages` function calls
* to simulate the proper cross-domain message behavior.
* @param _amount Amount of ETH to be unwrapped and sent.
* @param _relayERC20MsgHash Hash of the relayed message.
*/
function testFuzz_unwrapAndCall_succeeds(uint256 _amount, bytes32 _relayERC20MsgHash, bytes memory _calldata)
public
{
_amount = bound(_amount, 0, type(uint248).max - 1);

_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainETHWrapper))
);
_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
abi.encode(true)
);
_mockAndExpect(
Predeploys.SUPERCHAIN_WETH,
abi.encodeCall(ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).withdraw, (_amount)),
abi.encode("")
);
// Simulates the withdrawal being sent to the SuperchainETHWrapper contract.
vm.deal(address(superchainETHWrapper), _amount);

uint256 prevBalance = bob.balance;
vm.expectCall(bob, _amount, _calldata);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, bob, _amount, _calldata);
assertEq(bob.balance - prevBalance, _amount);
}
}

/// @title SuperchainETHWrapper Revert Tests
/// @notice This contract contains tests to check that certain conditions result in expected
/// reverts.
contract SuperchainETHWrapperRevertTests is Test {
SuperchainETHWrapper public superchainETHWrapper;

/// @notice Helper function to setup a mock and expect a call to it.
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
vm.mockCall(_receiver, _calldata, _returned);
vm.expectCall(_receiver, _calldata);
}

/// @notice Sets up the test suite.
function setUp() public {
superchainETHWrapper = new SuperchainETHWrapper();
}

/**
* @notice Tests that the `unwrap` function reverts when the message is unrelayed.
* @dev Mocks the cross-domain message sender and sets `successfulMessages` to return `false`,
* triggering a revert when trying to call `unwrap`.
* @param _to Address receiving the unwrapped ETH.
* @param _amount Amount of ETH to be unwrapped.
* @param _relayERC20MsgHash Hash of the relayed message.
*/
function testFuzz_unwrap_fromUnrelayedMsgHash_reverts(
address _to,
uint256 _amount,
bytes32 _relayERC20MsgHash,
bytes memory _calldata
) public {
_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainETHWrapper))
);
_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
abi.encode(false)
);

vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
vm.expectRevert(RelaySuperchainWETHNotSuccessful.selector);
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
}

/**
* @notice Tests that the `unwrap` function reverts when the sender is not the expected messenger.
* @dev Mocks an invalid sender (not the messenger) to ensure the function reverts with the
* `Unauthorized` error.
* @param _sender Address that tries to call `unwrap` but is not the messenger.
* @param _to Address receiving the unwrapped ETH.
* @param _amount Amount of ETH to be unwrapped.
* @param _relayERC20MsgHash Hash of the relayed message.
*/
function testFuzz_unwrap_nonMessengerSender_reverts(
address _sender,
address _to,
uint256 _amount,
bytes32 _relayERC20MsgHash,
bytes memory _calldata
) public {
vm.assume(_sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);

vm.prank(_sender);
vm.expectRevert(Unauthorized.selector);
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
}

/**
* @notice Tests that the `unwrap` function reverts when the cross-domain message sender is
* not the SuperchainETHWrapper contract.
* @dev Mocks a wrong cross-domain message sender and ensures the function reverts with the
* `Unauthorized` error.
* @param _sender Address that tries to call `unwrap` but is not the correct message sender.
* @param _to Address receiving the unwrapped ETH.
* @param _amount Amount of ETH to be unwrapped.
* @param _relayERC20MsgHash Hash of the relayed message.
*/
function testFuzz_unwrap_wrongCrossDomainMessageSender_reverts(
address _sender,
address _to,
uint256 _amount,
bytes32 _relayERC20MsgHash,
bytes memory _calldata
) public {
vm.assume(_sender != address(superchainETHWrapper));

_mockAndExpect(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(_sender)
);

vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
vm.expectRevert(Unauthorized.selector);
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
}
}
Loading

0 comments on commit 1c8ae46

Please sign in to comment.