Skip to content

Commit

Permalink
feat: SuperchainWETHWrapper contract
Browse files Browse the repository at this point in the history
  • Loading branch information
tremarkley committed Oct 23, 2024
1 parent a907454 commit 812cacd
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 0 deletions.
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
76 changes: 76 additions & 0 deletions contracts/src/SuperchainETHWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 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 LogReceived(address from, uint256 value);

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

/**
* @notice Unwraps SuperchainWETH into native ETH and sends it to a specified destination address.
* @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.
*/
function unwrap(bytes32 _relayERC20MsgHash, address _dst, uint256 _wad) 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, hex"");
}

/**
* @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.
*/
function sendETH(address _dst, uint256 _chainId) 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.unwrap, (messageHash, _dst, msg.value))
});
}
}
213 changes: 213 additions & 0 deletions contracts/test/SuperchainETHWrapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// 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;

/// @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);
}

/// @notice Tests the `sendETH` function deposits the sender's tokens, calls
/// SuperchainWETH.sendERC20, and sends an encoded call to
/// SuperchainETHWrapper.unwrap through L2ToL2CrossDomainMessenger.
function testFuzz_sendETH_succeeds(
address _sender,
address _to,
uint256 _amount,
uint256 _chainId,
bytes32 messageHash
) 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.unwrap, (messageHash, _to, _amount));
_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);
}

/**
* @notice Tests the successful execution of the `unwrap` function.
* @dev This test mocks the `crossDomainMessageSender` and `successfulMessages` function calls
* to simulate the proper cross-domain message behavior.
* @param _to Address receiving the unwrapped ETH.
* @param _amount Amount of ETH to be unwrapped and sent.
* @param _relayERC20MsgHash Hash of the relayed message.
*/
function testFuzz_unwrap_succeeds(address _to, uint256 _amount, bytes32 _relayERC20MsgHash) public {
_amount = bound(_amount, 0, type(uint248).max - 1);
// Ensure that the target contract is not a Forge contract.
assumeNotForgeAddress(_to);
// Ensure that the target call is payable if value is sent
assumePayable(_to);
uint256 prevBalance = _to.balance;

_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);

vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainETHWrapper.unwrap(_relayERC20MsgHash, _to, _amount);
assertEq(_to.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)
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.unwrap(_relayERC20MsgHash, _to, _amount);
}

/**
* @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
) public {
vm.assume(_sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);

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

/**
* @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
) 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.unwrap(_relayERC20MsgHash, _to, _amount);
}
}
Loading

0 comments on commit 812cacd

Please sign in to comment.