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 4, 2024
1 parent 1e73ae1 commit 12e0c76
Show file tree
Hide file tree
Showing 31 changed files with 455 additions and 93 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
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
90 changes: 90 additions & 0 deletions contracts/src/SuperchainETHWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.
* @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 {
// 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, 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 {
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.unwrap, (relayERC20MessageHash, _dst, msg.value))
});
}
}
217 changes: 217 additions & 0 deletions contracts/test/SuperchainETHWrapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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 {ISuperchainERC20Extensions} from "@contracts-bedrock/L2/interfaces/ISuperchainERC20.sol";
import {IWETH} from "@contracts-bedrock/universal/interfaces/IWETH.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 {
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.unwrap through L2ToL2CrossDomainMessenger.
function testFuzz_sendETH_succeeds(address _sender, address _to, uint256 _amount, uint256 _nonce, uint256 _chainId)
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.unwrap, (expectedRelayERC20MessageHash, _to, _amount));
_mockAndExpect(
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);

_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(""));
// Simulates the withdrawal being sent to the SuperchainETHWrapper contract.
vm.deal(address(superchainETHWrapper), _amount);

vm.prank(MESSENGER);
uint256 prevBalance = _to.balance;
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 {
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 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(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainETHWrapper))
);
_mockAndExpect(
MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
abi.encode(false)
);

vm.prank(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 != 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(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(_sender)
);

vm.prank(MESSENGER);
vm.expectRevert(Unauthorized.selector);
superchainETHWrapper.unwrap(_relayERC20MsgHash, _to, _amount);
}
}
4 changes: 2 additions & 2 deletions genesis/generated/addresses/901-addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"DisputeGameFactory": "0xdc2ba9096Cc439Ef483dAf69673ac77919fF6421",
"DisputeGameFactoryProxy": "0x5d9c1a294e8b70d9504df0887d641b1727764098",
"FastPreimageOracle": "0x1B74808A25D47f703C2926ef0205b0f1D55c1843",
"FaultDisputeGame_0": "0x21940D47eD2c4005C1dFdad64796a7A38b1E16FE",
"FaultDisputeGame_0": "0x0b1E38930c1d964df56aF4Fa6F3033987751b800",
"FaultDisputeGame_254": "0xca6D21fC0D4cF352d60D1743d82d96dAa3eF973b",
"FaultDisputeGame_255": "0x1322C11Ce52D479eed803A0fE9E269dA9765E183",
"L1CrossDomainMessenger": "0x15910ccc0CA0038a620ae48ee0FfD1a2fE0Aaa7B",
Expand All @@ -27,7 +27,7 @@
"OptimismPortalInterop": "0x4a25B5073bc405f09D685Ebb8fC8F8449df4b34b",
"OptimismPortalProxy": "0x4C60D24B4deaa79B1C96b3149A310f86B0562d79",
"PermissionedDelayedWETHProxy": "0xCE7D14a3bF59e53a0959C5a21Fc1ee04Ea77524E",
"PermissionedDisputeGame": "0xD5C0833C1BEa3BcfD32BfE884ACaCa8e158c8c1A",
"PermissionedDisputeGame": "0xf2B37fc86294312c0871EB2EEDF1B6a48Bb06110",
"PreimageOracle": "0x2951989302E3274d72B521b4926106190b9470fd",
"ProtocolVersions": "0xa99F1ab91821747b76Ec0cDFA38368DF4Ba06E84",
"ProtocolVersionsProxy": "0xbA128bC42D4f3D21f23D78fa068b2d1B0dfC08F9",
Expand Down
4 changes: 2 additions & 2 deletions genesis/generated/addresses/902-addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"DisputeGameFactory": "0xdc2ba9096Cc439Ef483dAf69673ac77919fF6421",
"DisputeGameFactoryProxy": "0x9c491CC02E63D896802aCE7A997F4eb922aB945a",
"FastPreimageOracle": "0x1B74808A25D47f703C2926ef0205b0f1D55c1843",
"FaultDisputeGame_0": "0xff29eDF79F6438066e09CB34d2533A58dA4CDB28",
"FaultDisputeGame_0": "0x379Ac1A9e22FfE4d51e971fcF06DBb79aA7479ee",
"FaultDisputeGame_254": "0x8876164A42627323A2664C4fee1ff0B59458Fe76",
"FaultDisputeGame_255": "0x90543C7E601715f4e15907719d6a5D46CAFf558e",
"L1CrossDomainMessenger": "0x15910ccc0CA0038a620ae48ee0FfD1a2fE0Aaa7B",
Expand All @@ -27,7 +27,7 @@
"OptimismPortalInterop": "0x4a25B5073bc405f09D685Ebb8fC8F8449df4b34b",
"OptimismPortalProxy": "0x07170174721388b75ce70a33A1C90685f4F16a71",
"PermissionedDelayedWETHProxy": "0x28cd29082110aB186bA13E6fec042b649D1bDB9E",
"PermissionedDisputeGame": "0xF8867774985c730Cc2215be26a241E86040B0a18",
"PermissionedDisputeGame": "0xC6230B470dEA6D9AdF03DE0A43C539123991d389",
"PreimageOracle": "0x2951989302E3274d72B521b4926106190b9470fd",
"ProtocolVersions": "0xa99F1ab91821747b76Ec0cDFA38368DF4Ba06E84",
"ProtocolVersionsProxy": "0xaEC7bc92E07FB07A49130196fD3B337FA4eB416b",
Expand Down
4 changes: 2 additions & 2 deletions genesis/generated/addresses/903-addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"DisputeGameFactory": "0xdc2ba9096Cc439Ef483dAf69673ac77919fF6421",
"DisputeGameFactoryProxy": "0xf0C414f2a27c0c0DF24b2946ef45EE4e11D87AF7",
"FastPreimageOracle": "0x1B74808A25D47f703C2926ef0205b0f1D55c1843",
"FaultDisputeGame_0": "0x14e5De20c62872b37a93F01275A63781384476de",
"FaultDisputeGame_0": "0xE8Aa13A17E566197f15960af73d346d2ef088700",
"FaultDisputeGame_254": "0xb19ae8e9287ada3500DAa43b23DE4F08bAc6163D",
"FaultDisputeGame_255": "0x860842142A1AA2EC762B226E10E42cf9C0ca31e9",
"L1CrossDomainMessenger": "0x15910ccc0CA0038a620ae48ee0FfD1a2fE0Aaa7B",
Expand All @@ -27,7 +27,7 @@
"OptimismPortalInterop": "0x4a25B5073bc405f09D685Ebb8fC8F8449df4b34b",
"OptimismPortalProxy": "0x8094BF146C539184DD77503C52c61EEefE61941e",
"PermissionedDelayedWETHProxy": "0x2a484836182650421C3bD479fa29b50262374bC7",
"PermissionedDisputeGame": "0x3Fad5221a8b1EBA883AB64fc7B449C9261ce6Ec6",
"PermissionedDisputeGame": "0xEfe60848d7209C1f40871CbC11b8f489E60D39D2",
"PreimageOracle": "0x2951989302E3274d72B521b4926106190b9470fd",
"ProtocolVersions": "0xa99F1ab91821747b76Ec0cDFA38368DF4Ba06E84",
"ProtocolVersionsProxy": "0x645CfCaCB134E2B80B7DEf99590420292D270846",
Expand Down
Loading

0 comments on commit 12e0c76

Please sign in to comment.