Skip to content

Commit

Permalink
feat: SuperchainWETHWrapper contract
Browse files Browse the repository at this point in the history
  • Loading branch information
tremarkley committed Sep 25, 2024
1 parent 71d039e commit 09291d7
Show file tree
Hide file tree
Showing 22 changed files with 327 additions and 66 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ lib

/main
dist/

cache
2 changes: 1 addition & 1 deletion contracts/lib/optimism
Submodule optimism updated 505 files
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
92 changes: 92 additions & 0 deletions contracts/src/SuperchainETHWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 {ISuperchainERC20Extensions} from "@contracts-bedrock/L2/interfaces/ISuperchainERC20.sol";
import {IWETH} from "@contracts-bedrock/universal/interfaces/IWETH.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.
* @dev This function is called after receiving a message from another chain. It checks the message relay status, unwraps WETH to ETH,
* and calls the destination address with the unwrapped ETH and the provided calldata.
* @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 Data to be executed on the destination address.
*/
function unwrapAndCall(bytes32 _relayERC20MsgHash, address _dst, uint256 _wad, bytes memory _calldata) external {
// Receive message from other chain.
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();
}

IWETH(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 Data to be executed on the destination address.
*/
function sendETH(address _dst, uint256 _chainId, bytes memory _calldata) public payable {
IWETH(Predeploys.SUPERCHAIN_WETH).deposit{value: msg.value}();

IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
bytes32 relayERC20MessageHash = keccak256(
abi.encode(
_chainId,
block.chainid,
messenger.messageNonce(),
Predeploys.SUPERCHAIN_WETH,
Predeploys.SUPERCHAIN_WETH,
abi.encodeCall(
ISuperchainERC20Extensions(Predeploys.SUPERCHAIN_WETH).relayERC20,
(address(this), address(this), msg.value)
)
)
);
ISuperchainERC20Extensions(Predeploys.SUPERCHAIN_WETH).sendERC20(address(this), msg.value, _chainId);
messenger.sendMessage({
_destination: _chainId,
_target: address(this),
_message: abi.encodeCall(this.unwrapAndCall, (relayERC20MessageHash, _dst, msg.value, _calldata))
});
}
}
121 changes: 121 additions & 0 deletions contracts/test/SuperchainETHWrapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

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

import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {ISuperchainERC20Extensions} from "@contracts-bedrock/L2/interfaces/ISuperchainERC20.sol";
import {IWETH} from "@contracts-bedrock/universal/interfaces/IWETH.sol";

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

contract SuperchainETHWrapperTest is Test {
address internal constant SUPERCHAIN_WETH = Predeploys.SUPERCHAIN_WETH;
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;

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();
}

/// @notice Tests the `sendETH` function deposits the sender's tokens, calls
/// SuperchainWETH.sendERC20, and sends an encoded call to
/// SuperchainETHWrapper.unwrapAndCall through L2ToL2CrossDomainMessenger.
function testFuzz_sendETH_succeeds(
address _sender,
address _to,
uint256 _amount,
uint256 _nonce,
uint256 _chainId,
bytes memory _calldata
) public {
vm.assume(_chainId != block.chainid);
_amount = bound(_amount, 0, type(uint248).max - 1);

vm.deal(_sender, _amount);
_mockAndExpect(SUPERCHAIN_WETH, _amount, abi.encodeCall(IWETH.deposit, ()), abi.encode(""));
_mockAndExpect(
SUPERCHAIN_WETH,
abi.encodeCall(ISuperchainERC20Extensions.sendERC20, (address(superchainETHWrapper), _amount, _chainId)),
abi.encode("")
);
_mockAndExpect(MESSENGER, abi.encodeCall(IL2ToL2CrossDomainMessenger.messageNonce, ()), abi.encode(_nonce));
bytes32 expectedRelayERC20MessageHash = keccak256(
abi.encode(
_chainId,
block.chainid,
_nonce,
Predeploys.SUPERCHAIN_WETH,
Predeploys.SUPERCHAIN_WETH,
abi.encodeCall(
ISuperchainERC20Extensions(Predeploys.SUPERCHAIN_WETH).relayERC20,
(address(superchainETHWrapper), address(superchainETHWrapper), _amount)
)
)
);
bytes memory _message =
abi.encodeCall(superchainETHWrapper.unwrapAndCall, (expectedRelayERC20MessageHash, _to, _amount, _calldata));
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(
IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainETHWrapper), _message
),
abi.encode("")
);

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

function testFuzz_unwrapAndCall_succeeds(
address _sender,
address _to,
uint256 _amount,
uint256 _nonce,
uint256 _chainId,
bytes32 _relayERC20MsgHash,
bytes memory _calldata
) 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
if (_amount > 0) assumePayable(_to);

// Mock the call over the `crossDomainMessageSender` function setting the same address as value
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainETHWrapper))
);
_mockAndExpect(
MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
abi.encode(true)
);
_mockAndExpect(SUPERCHAIN_WETH, abi.encodeCall(IWETH.withdraw, (_amount)), abi.encode(""));
_mockAndExpect(_to, _amount, _calldata, abi.encode(true));

vm.prank(MESSENGER);
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
}
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading

0 comments on commit 09291d7

Please sign in to comment.