Skip to content

Commit 1c8ae46

Browse files
authored
Merge pull request #167 from ethereum-optimism/harry/superchain_weth_wrapper
feat: `SuperchainETHWrapper` contract
2 parents fa00666 + c5d49fb commit 1c8ae46

13 files changed

+513
-15
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ In a few seconds, you should see the RelayedMessage on chain 902:
148148

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

contracts/script/DeployL2PeripheryContracts.s.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.25;
33

44
import {Script, console} from "forge-std/Script.sol";
55
import {L2NativeSuperchainERC20} from "../src/L2NativeSuperchainERC20.sol";
6+
import {SuperchainETHWrapper} from "../src/SuperchainETHWrapper.sol";
67

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

3233
function run() public broadcast {
3334
deployL2NativeSuperchainERC20();
35+
deploySuperchainETHWrapper();
3436
}
3537

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

44+
function deploySuperchainETHWrapper() public {
45+
address _superchainETHWrapperContract = address(new SuperchainETHWrapper{salt: _salt()}());
46+
address deploymentAddress = deployAtNextDeploymentAddress(_superchainETHWrapperContract.code);
47+
console.log("Deployed SuperchainETHWrapper at address: ", deploymentAddress);
48+
}
49+
4250
function deployAtNextDeploymentAddress(bytes memory newRuntimeBytecode)
4351
internal
4452
returns (address _deploymentAddr)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.15;
3+
4+
import {Unauthorized} from "@contracts-bedrock/libraries/errors/CommonErrors.sol";
5+
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
6+
import {SafeCall} from "@contracts-bedrock//libraries/SafeCall.sol";
7+
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
8+
import {ISuperchainTokenBridge} from "@contracts-bedrock/L2/interfaces/ISuperchainTokenBridge.sol";
9+
import {ISuperchainWETH} from "@contracts-bedrock/L2/interfaces/ISuperchainWETH.sol";
10+
11+
/**
12+
* @notice This contract has not been audited. It may contain bugs or security vulnerabilities.
13+
* We are not liable for any issues arising from its use. It is strongly advised that this
14+
* contract not be used with actual funds and should only be used for testing on
15+
* testnets or in a controlled development environment.
16+
*/
17+
18+
/**
19+
* @notice Thrown when the relay of SuperchainWETH has not succeeded.
20+
* @dev This error is triggered if the SuperchainWETH relay through the L2ToL2CrossDomainMessenger
21+
* has not completed successfully successful.
22+
*/
23+
error RelaySuperchainWETHNotSuccessful();
24+
25+
/**
26+
* @title SuperchainETHWrapper
27+
* @notice This contract facilitates sending ETH across chains within the Superchain by wrapping
28+
* ETH into SuperchainWETH, relaying the wrapped asset to another chain, and then
29+
* unwrapping it back to ETH on the destination chain.
30+
* @dev The contract integrates with the SuperchainWETH contract for wrapping and unwrapping ETH,
31+
* and uses the L2ToL2CrossDomainMessenger for relaying the wrapped ETH between chains.
32+
*/
33+
contract SuperchainETHWrapper {
34+
/**
35+
* @dev Emitted when ETH is received by the contract.
36+
* @param from The address that sent ETH.
37+
* @param value The amount of ETH received.
38+
*/
39+
event ETHReceived(address indexed from, uint256 value);
40+
41+
// Fallback function to receive ETH
42+
receive() external payable {
43+
emit ETHReceived(msg.sender, msg.value);
44+
}
45+
46+
/**
47+
* @notice Unwraps SuperchainWETH into native ETH and sends it to a specified destination address
48+
* then calls an arbitrary function at the destination..
49+
* @param _relayERC20MsgHash The hash of the relayed ERC20 message.
50+
* @param _dst The destination address on the receiving chain.
51+
* @param _wad The amount of SuperchainWETH to unwrap to ETH.
52+
* @param _calldata The calldata to be passed in the call to the destination address.
53+
*/
54+
function unwrapAndCall(bytes32 _relayERC20MsgHash, address _dst, uint256 _wad, bytes memory _calldata) external {
55+
IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
56+
if (msg.sender != address(messenger)) revert Unauthorized();
57+
if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized();
58+
59+
if (messenger.successfulMessages(_relayERC20MsgHash) == false) {
60+
revert RelaySuperchainWETHNotSuccessful();
61+
}
62+
63+
ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).withdraw(_wad);
64+
SafeCall.call(_dst, _wad, _calldata);
65+
}
66+
67+
/**
68+
* @notice Wraps ETH into SuperchainWETH and sends it to another chain.
69+
* @dev This function wraps the sent ETH into SuperchainWETH, computes the relay message hash,
70+
* and relays the message to the destination chain.
71+
* @param _dst The destination address on the receiving chain.
72+
* @param _chainId The ID of the destination chain.
73+
* @param _calldata The calldata for the function to be called on the destination chain after unwrapping.
74+
*/
75+
function sendETH(address _dst, uint256 _chainId, bytes memory _calldata) public payable {
76+
ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).deposit{value: msg.value}();
77+
bytes32 messageHash = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE).sendERC20(
78+
Predeploys.SUPERCHAIN_WETH, address(this), msg.value, _chainId
79+
);
80+
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
81+
_destination: _chainId,
82+
_target: address(this),
83+
_message: abi.encodeCall(this.unwrapAndCall, (messageHash, _dst, msg.value, _calldata))
84+
});
85+
}
86+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.15;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
6+
import {Unauthorized} from "@contracts-bedrock/libraries/errors/CommonErrors.sol";
7+
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
8+
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
9+
import {ISuperchainTokenBridge} from "@contracts-bedrock/L2/interfaces/ISuperchainTokenBridge.sol";
10+
import {ISuperchainWETH} from "@contracts-bedrock/L2/interfaces/ISuperchainWETH.sol";
11+
import {IWETH} from "@contracts-bedrock/universal/interfaces/IWETH.sol";
12+
import {SuperchainWETH} from "@contracts-bedrock/L2/SuperchainWETH.sol";
13+
14+
import {SuperchainETHWrapper, RelaySuperchainWETHNotSuccessful} from "../src/SuperchainETHWrapper.sol";
15+
16+
/// @title SuperchainETHWrapper Happy Path Tests
17+
/// @notice This contract contains the tests for successful paths in SuperchainETHWrapper.
18+
contract SuperchainETHWrapper_HappyPath_Test is Test {
19+
SuperchainETHWrapper public superchainETHWrapper;
20+
address bob;
21+
22+
/// @notice Helper function to setup a mock and expect a call to it.
23+
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
24+
vm.mockCall(_receiver, _calldata, _returned);
25+
vm.expectCall(_receiver, _calldata);
26+
}
27+
28+
/// @notice Helper function to setup a mock and expect a call to it.
29+
function _mockAndExpect(address _receiver, uint256 _msgValue, bytes memory _calldata, bytes memory _returned)
30+
internal
31+
{
32+
vm.mockCall(_receiver, _msgValue, _calldata, _returned);
33+
vm.expectCall(_receiver, _msgValue, _calldata);
34+
}
35+
36+
/// @notice Sets up the test suite.
37+
function setUp() public {
38+
superchainETHWrapper = new SuperchainETHWrapper();
39+
SuperchainWETH superchainWETH = new SuperchainWETH();
40+
vm.etch(Predeploys.SUPERCHAIN_WETH, address(superchainWETH).code);
41+
bob = makeAddr("bob");
42+
}
43+
44+
/// @notice Tests the `sendETH` function deposits the sender's tokens, calls
45+
/// SuperchainWETH.sendERC20, and sends an encoded call to
46+
/// SuperchainETHWrapper.unwrapAndCallAndCall through L2ToL2CrossDomainMessenger.
47+
function testFuzz_sendETH_succeeds(
48+
address _sender,
49+
address _to,
50+
uint256 _amount,
51+
uint256 _chainId,
52+
bytes32 messageHash,
53+
bytes memory _calldata
54+
) public {
55+
vm.assume(_chainId != block.chainid);
56+
_amount = bound(_amount, 0, type(uint248).max - 1);
57+
vm.deal(_sender, _amount);
58+
_mockAndExpect(
59+
Predeploys.SUPERCHAIN_WETH, _amount, abi.encodeWithSelector(IWETH.deposit.selector), abi.encode("")
60+
);
61+
_mockAndExpect(
62+
Predeploys.SUPERCHAIN_TOKEN_BRIDGE,
63+
abi.encodeCall(
64+
ISuperchainTokenBridge.sendERC20,
65+
(Predeploys.SUPERCHAIN_WETH, address(superchainETHWrapper), _amount, _chainId)
66+
),
67+
abi.encode(messageHash)
68+
);
69+
bytes memory _message =
70+
abi.encodeCall(superchainETHWrapper.unwrapAndCall, (messageHash, _to, _amount, _calldata));
71+
_mockAndExpect(
72+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
73+
abi.encodeWithSelector(
74+
IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainETHWrapper), _message
75+
),
76+
abi.encode("")
77+
);
78+
79+
vm.prank(_sender);
80+
superchainETHWrapper.sendETH{value: _amount}(_to, _chainId, _calldata);
81+
}
82+
83+
/**
84+
* @notice Tests the successful execution of the `unwrapAndCall` function.
85+
* @dev This test mocks the `crossDomainMessageSender` and `successfulMessages` function calls
86+
* to simulate the proper cross-domain message behavior.
87+
* @param _amount Amount of ETH to be unwrapped and sent.
88+
* @param _relayERC20MsgHash Hash of the relayed message.
89+
*/
90+
function testFuzz_unwrapAndCall_succeeds(uint256 _amount, bytes32 _relayERC20MsgHash, bytes memory _calldata)
91+
public
92+
{
93+
_amount = bound(_amount, 0, type(uint248).max - 1);
94+
95+
_mockAndExpect(
96+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
97+
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
98+
abi.encode(address(superchainETHWrapper))
99+
);
100+
_mockAndExpect(
101+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
102+
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
103+
abi.encode(true)
104+
);
105+
_mockAndExpect(
106+
Predeploys.SUPERCHAIN_WETH,
107+
abi.encodeCall(ISuperchainWETH(Predeploys.SUPERCHAIN_WETH).withdraw, (_amount)),
108+
abi.encode("")
109+
);
110+
// Simulates the withdrawal being sent to the SuperchainETHWrapper contract.
111+
vm.deal(address(superchainETHWrapper), _amount);
112+
113+
uint256 prevBalance = bob.balance;
114+
vm.expectCall(bob, _amount, _calldata);
115+
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
116+
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, bob, _amount, _calldata);
117+
assertEq(bob.balance - prevBalance, _amount);
118+
}
119+
}
120+
121+
/// @title SuperchainETHWrapper Revert Tests
122+
/// @notice This contract contains tests to check that certain conditions result in expected
123+
/// reverts.
124+
contract SuperchainETHWrapperRevertTests is Test {
125+
SuperchainETHWrapper public superchainETHWrapper;
126+
127+
/// @notice Helper function to setup a mock and expect a call to it.
128+
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
129+
vm.mockCall(_receiver, _calldata, _returned);
130+
vm.expectCall(_receiver, _calldata);
131+
}
132+
133+
/// @notice Sets up the test suite.
134+
function setUp() public {
135+
superchainETHWrapper = new SuperchainETHWrapper();
136+
}
137+
138+
/**
139+
* @notice Tests that the `unwrap` function reverts when the message is unrelayed.
140+
* @dev Mocks the cross-domain message sender and sets `successfulMessages` to return `false`,
141+
* triggering a revert when trying to call `unwrap`.
142+
* @param _to Address receiving the unwrapped ETH.
143+
* @param _amount Amount of ETH to be unwrapped.
144+
* @param _relayERC20MsgHash Hash of the relayed message.
145+
*/
146+
function testFuzz_unwrap_fromUnrelayedMsgHash_reverts(
147+
address _to,
148+
uint256 _amount,
149+
bytes32 _relayERC20MsgHash,
150+
bytes memory _calldata
151+
) public {
152+
_mockAndExpect(
153+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
154+
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
155+
abi.encode(address(superchainETHWrapper))
156+
);
157+
_mockAndExpect(
158+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
159+
abi.encodeCall(IL2ToL2CrossDomainMessenger.successfulMessages, (_relayERC20MsgHash)),
160+
abi.encode(false)
161+
);
162+
163+
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
164+
vm.expectRevert(RelaySuperchainWETHNotSuccessful.selector);
165+
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
166+
}
167+
168+
/**
169+
* @notice Tests that the `unwrap` function reverts when the sender is not the expected messenger.
170+
* @dev Mocks an invalid sender (not the messenger) to ensure the function reverts with the
171+
* `Unauthorized` error.
172+
* @param _sender Address that tries to call `unwrap` but is not the messenger.
173+
* @param _to Address receiving the unwrapped ETH.
174+
* @param _amount Amount of ETH to be unwrapped.
175+
* @param _relayERC20MsgHash Hash of the relayed message.
176+
*/
177+
function testFuzz_unwrap_nonMessengerSender_reverts(
178+
address _sender,
179+
address _to,
180+
uint256 _amount,
181+
bytes32 _relayERC20MsgHash,
182+
bytes memory _calldata
183+
) public {
184+
vm.assume(_sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
185+
186+
vm.prank(_sender);
187+
vm.expectRevert(Unauthorized.selector);
188+
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
189+
}
190+
191+
/**
192+
* @notice Tests that the `unwrap` function reverts when the cross-domain message sender is
193+
* not the SuperchainETHWrapper contract.
194+
* @dev Mocks a wrong cross-domain message sender and ensures the function reverts with the
195+
* `Unauthorized` error.
196+
* @param _sender Address that tries to call `unwrap` but is not the correct message sender.
197+
* @param _to Address receiving the unwrapped ETH.
198+
* @param _amount Amount of ETH to be unwrapped.
199+
* @param _relayERC20MsgHash Hash of the relayed message.
200+
*/
201+
function testFuzz_unwrap_wrongCrossDomainMessageSender_reverts(
202+
address _sender,
203+
address _to,
204+
uint256 _amount,
205+
bytes32 _relayERC20MsgHash,
206+
bytes memory _calldata
207+
) public {
208+
vm.assume(_sender != address(superchainETHWrapper));
209+
210+
_mockAndExpect(
211+
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
212+
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
213+
abi.encode(_sender)
214+
);
215+
216+
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
217+
vm.expectRevert(Unauthorized.selector);
218+
superchainETHWrapper.unwrapAndCall(_relayERC20MsgHash, _to, _amount, _calldata);
219+
}
220+
}

0 commit comments

Comments
 (0)