From 79dd15cfde3a407561fcb65b7b546c8dd93b298f Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:52:04 -0300 Subject: [PATCH] feat: SuperchainWETH redesign (#101) * feat: add superchain erc20 bridge (#61) * feat: add superchain erc20 bridge * fix: interfaces and versions * refactor: optimism superchain erc20 redesign (#62) * refactor: use oz upgradeable erc20 as dependency * chore: update interfaces * fix: tests based on changes * refactor: remove op as dependency * feat: add check for supererc20 bridge on modifier * chore: update tests and interfaces * chore: update stack vars name on test * chore: remove empty gitmodules file * chore: update superchain weth errors * test: add superchain erc20 bridge tests (#65) * test: add superchain erc20 bridge tests * test: add optimism superchain erc20 beacon tests * test: remove unnecessary test * test: tests fixes * test: tests fixes * chore: update missing bridge on natspec (#69) * chore: update missing bridge on natspec * fix: natspecs --------- Co-authored-by: agusduha * fix: remove superchain erc20 base (#70) * refactor: update isuperchainweth (#71) --------- Co-authored-by: agusduha * feat: rename mint/burn and add SuperchainERC20 (#74) * refactor: rename mint and burn functions on superchain erc20 * chore: rename optimism superchain erc20 to superchain erc20 * feat: create optimism superchain erc20 contract * chore: update natspec and errors * fix: superchain erc20 tests * refactor: make superchain erc20 abstract * refactor: move storage and erc20 metadata functions to implementation * chore: update interfaces * chore: update superchain erc20 events * fix: tests * fix: natspecs * fix: add semmver lock and snapshots * fix: remove unused imports * fix: natspecs --------- Co-authored-by: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> * fix: refactor zero check (#76) * fix: pre pr * fix: semver natspec check failure (#79) * fix: semver natspec check failure * fix: ignore mock contracts in semver natspec script * fix: error message * feat: add crosschain erc20 interface (#80) * feat: add crosschain erc20 interface * fix: refactor interfaces * fix: superchain bridge natspec (#83) * fix: superchain weth natspec (#84) Co-authored-by: 0xng Co-authored-by: 0xParticle Co-authored-by: gotzenx <78360669+gotzenx@users.noreply.github.com> * fix: stop inheriting superchain interfaces (#85) * fix: stop inheriting superchain interfaces * fix: move events and erros into the implementation * fix: make superchainERC20 inherits from crosschainERC20 * fix: superchain bridge rename (#86) * fix: fee vault compiler error (#87) * fix: remove unused imports * fix: refactor common errors (#90) * fix: refactor common errors * fix: remove unused version * fix: reuse unauthorized error (#92) * fix: superchain erc20 factory conflicts * fix: rename crosschain functions (#94) * feat: superweth redesign * fix: pr fixes * fix: fixes post merge --------- Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> Co-authored-by: 0xng Co-authored-by: 0xParticle Co-authored-by: gotzenx <78360669+gotzenx@users.noreply.github.com> --- packages/contracts-bedrock/semver-lock.json | 4 +- .../snapshots/abi/SuperchainWETH.json | 129 +++---- .../src/L2/SuperchainWETH.sol | 95 +++-- .../src/L2/interfaces/ISuperchainWETH.sol | 41 +- .../test/L2/SuperchainWETH.t.sol | 353 ++++++++---------- .../test/invariants/SuperchainWETH.t.sol | 92 +---- .../contracts-bedrock/test/setup/Setup.sol | 4 +- 7 files changed, 262 insertions(+), 456 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 9488435c1ca3..ad1fd45be34f 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -132,8 +132,8 @@ "sourceCodeHash": "0xaf2458e48dcadcafa8940cde7368549eae2280eef91247600d864ddac20f5d82" }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x50f6ea9bfe650fcf792e98e44b1bf66c036fd0e6d4b753da680253d7d8609816", - "sourceCodeHash": "0x82d03262decf52d5954d40bca8703f96a0f3ba7accf6c1d75292856c2f34cf8f" + "initCodeHash": "0x76a6baa0823ca75f32a94ba49592f07129e762f9459be305b95b12475d8a5fce", + "sourceCodeHash": "0xbb6196751e8d1f0c7281bd824f0cb024dda726d65701def59cc3cb6d2bcbe6b3" }, "src/L2/WETH.sol": { "initCodeHash": "0xfb253765520690623f177941c2cd9eba23e4c6d15063bccdd5e98081329d8956", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index b0b86ea7c8ce..1a8a06b1eec9 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -74,6 +74,42 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "crosschainBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "crosschainMint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "decimals", @@ -107,52 +143,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "dst", - "type": "address" - }, - { - "internalType": "uint256", - "name": "wad", - "type": "uint256" - } - ], - "name": "relayERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "dst", - "type": "address" - }, - { - "internalType": "uint256", - "name": "wad", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - } - ], - "name": "sendERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "symbol", @@ -289,28 +279,22 @@ { "indexed": true, "internalType": "address", - "name": "dst", + "name": "from", "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "wad", + "name": "amount", "type": "uint256" } ], - "name": "Deposit", + "name": "CrosschainBurnt", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, { "indexed": true, "internalType": "address", @@ -322,15 +306,9 @@ "internalType": "uint256", "name": "amount", "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "source", - "type": "uint256" } ], - "name": "RelayERC20", + "name": "CrosschainMinted", "type": "event" }, { @@ -339,29 +317,17 @@ { "indexed": true, "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", + "name": "dst", "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "destination", + "name": "wad", "type": "uint256" } ], - "name": "SendERC20", + "name": "Deposit", "type": "event" }, { @@ -410,17 +376,12 @@ }, { "inputs": [], - "name": "CallerNotL2ToL2CrossDomainMessenger", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidCrossDomainSender", + "name": "NotCustomGasToken", "type": "error" }, { "inputs": [], - "name": "NotCustomGasToken", + "name": "Unauthorized", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 3706a511cdeb..543580078da2 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -9,10 +9,10 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "src/universal/interfaces/ISemver.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { IL1Block } from "src/L2/interfaces/IL1Block.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; -import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; +import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000024 @@ -20,10 +20,16 @@ import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; /// @notice SuperchainWETH is a version of WETH that can be freely transfrered between chains /// within the superchain. SuperchainWETH can be converted into native ETH on chains that /// do not use a custom gas token. -contract SuperchainWETH is WETH98, ISuperchainWETH, ISemver { +contract SuperchainWETH is WETH98, ICrosschainERC20, ISemver { + /// @notice A modifier that only allows the SuperchainTokenBridge to call + modifier onlySuperchainTokenBridge() { + if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized(); + _; + } + /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.7 + string public constant version = "1.0.0-beta.7"; /// @inheritdoc WETH98 function deposit() public payable override { @@ -37,63 +43,48 @@ contract SuperchainWETH is WETH98, ISuperchainWETH, ISemver { super.withdraw(wad); } - /// @inheritdoc ISuperchainWETH - function sendERC20(address dst, uint256 wad, uint256 chainId) public { - // Burn from user's balance. - _burn(msg.sender, wad); - - // Burn to ETHLiquidity contract. - if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { - IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: wad }(); - } - - // Send message to other chain. - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ - _destination: chainId, - _target: address(this), - _message: abi.encodeCall(this.relayERC20, (msg.sender, dst, wad)) - }); + /// @notice Mints WETH to an address. + /// @param _guy The address to mint WETH to. + /// @param _wad The amount of WETH to mint. + function _mint(address _guy, uint256 _wad) internal { + balanceOf[_guy] += _wad; + emit Transfer(address(0), _guy, _wad); + } - // Emit event. - emit SendERC20(msg.sender, dst, wad, chainId); + /// @notice Burns WETH from an address. + /// @param _guy The address to burn WETH from. + /// @param _wad The amount of WETH to burn. + function _burn(address _guy, uint256 _wad) internal { + require(balanceOf[_guy] >= _wad); + balanceOf[_guy] -= _wad; + emit Transfer(_guy, address(0), _wad); } - /// @inheritdoc ISuperchainWETH - function relayERC20(address from, 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 CallerNotL2ToL2CrossDomainMessenger(); - if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); + /// @notice Allows the SuperchainTokenBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external onlySuperchainTokenBridge { + _mint(_to, _amount); // Mint from ETHLiquidity contract. if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(wad); + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); } - // Get source chain ID. - uint256 source = messenger.crossDomainMessageSource(); - - // Mint to user's balance. - _mint(dst, wad); - - // Emit event. - emit RelayERC20(from, dst, wad, source); + emit CrosschainMinted(_to, _amount); } - /// @notice Mints WETH to an address. - /// @param guy The address to mint WETH to. - /// @param wad The amount of WETH to mint. - function _mint(address guy, uint256 wad) internal { - balanceOf[guy] += wad; - emit Transfer(address(0), guy, wad); - } + /// @notice Allows the SuperchainTokenBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external onlySuperchainTokenBridge { + _burn(_from, _amount); - /// @notice Burns WETH from an address. - /// @param guy The address to burn WETH from. - /// @param wad The amount of WETH to burn. - function _burn(address guy, uint256 wad) internal { - require(balanceOf[guy] >= wad); - balanceOf[guy] -= wad; - emit Transfer(guy, address(0), wad); + // Burn to ETHLiquidity contract. + if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _amount }(); + } + + emit CrosschainBurnt(_from, _amount); } } diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol index bccab456f5fd..4c91322c6b12 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol @@ -2,43 +2,12 @@ pragma solidity ^0.8.0; import { IWETH } from "src/universal/interfaces/IWETH.sol"; +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; +import { ISemver } from "src/universal/interfaces/ISemver.sol"; -interface ISuperchainWETH { - /// @notice Thrown when attempting a deposit or withdrawal and the chain uses a custom gas token. +interface ISuperchainWETH is IWETH, ICrosschainERC20, ISemver { + error Unauthorized(); error NotCustomGasToken(); - /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not - /// L2ToL2CrossDomainMessenger. - error CallerNotL2ToL2CrossDomainMessenger(); - - /// @notice Thrown when attempting to relay a message and the cross domain message sender is not `address(this)` - error InvalidCrossDomainSender(); - - /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param from Address of the msg.sender of sendERC20 on the source chain. - /// @param to Address of the recipient. - /// @param amount Amount of tokens relayed. - /// @param source Chain ID of the source chain. - event RelayERC20(address indexed from, address indexed to, uint256 amount, uint256 source); - - /// @notice Emitted when tokens are sent from one chain to another. - /// @param from Address of the sender. - /// @param to Address of the recipient. - /// @param amount Number of tokens sent. - /// @param destination Chain ID of the destination chain. - event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); - - /// @notice Sends tokens to some target address on another chain. - /// @param _dst Address to send tokens to. - /// @param _wad Amount of tokens to send. - /// @param _chainId Chain ID of the destination chain. - function sendERC20(address _dst, uint256 _wad, uint256 _chainId) external; - - /// @notice Relays tokens received from another chain. - /// @param _from Address of the msg.sender of sendERC20 on the source chain. - /// @param _dst Address to relay tokens to. - /// @param _wad Amount of tokens to relay. - function relayERC20(address _from, address _dst, uint256 _wad) external; + function __constructor__() external; } - -interface ISuperchainWETHERC20 is IWETH, ISuperchainWETH { } diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index c9c523201c6a..bee12c00b568 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -9,7 +9,6 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; // Interfaces -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; @@ -25,11 +24,13 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Emitted when a withdrawal is made. event Withdrawal(address indexed src, uint256 wad); - /// @notice Emitted when an ERC20 is sent. - event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId); + /// @notice Emitted when a crosschain transfer mints tokens. + event CrosschainMinted(address indexed to, uint256 amount); - /// @notice Emitted when an ERC20 send is relayed. - event RelayERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _source); + /// @notice Emitted when a crosschain transfer burns tokens. + event CrosschainBurnt(address indexed from, uint256 amount); + + address internal constant ZERO_ADDRESS = address(0); /// @notice Test setup. function setUp() public virtual override { @@ -37,6 +38,12 @@ contract SuperchainWETH_Test is CommonTest { super.setUp(); } + /// @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 Tests that the deposit function can be called on a non-custom gas token chain. /// @param _amount The amount of WETH to send. function testFuzz_deposit_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public { @@ -45,6 +52,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(alice, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -65,7 +73,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(address(alice), _amount); - vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); // Act vm.prank(alice); @@ -87,6 +95,7 @@ contract SuperchainWETH_Test is CommonTest { vm.deal(alice, _amount); vm.prank(alice); superchainWeth.deposit{ value: _amount }(); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -109,7 +118,7 @@ contract SuperchainWETH_Test is CommonTest { vm.deal(alice, _amount); vm.prank(alice); superchainWeth.deposit{ value: _amount }(); - vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); // Act vm.prank(alice); @@ -121,237 +130,197 @@ contract SuperchainWETH_Test is CommonTest { assertEq(superchainWeth.balanceOf(alice), _amount); } - /// @notice Tests that the sendERC20 function always succeeds when called with a sufficient - /// balance no matter the sender, amount, recipient, or chain ID. - /// @param _amount The amount of WETH to send. - /// @param _caller The address of the caller. - /// @param _recipient The address of the recipient. - /// @param _chainId The chain ID to send the WETH to. - function testFuzz_sendERC20_sufficientBalance_succeeds( - uint256 _amount, - address _caller, - address _recipient, - uint256 _chainId - ) - public - { - // Assume - vm.assume(_chainId != block.chainid); - vm.assume(_caller != address(ethLiquidity)); - vm.assume(_caller != address(superchainWeth)); - _amount = bound(_amount, 0, type(uint248).max - 1); + /// @notice Tests the `crosschainMint` function reverts when the caller is not the `SuperchainTokenBridge`. + function testFuzz_crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != Predeploys.SUPERCHAIN_TOKEN_BRIDGE); - // Arrange - vm.deal(_caller, _amount); + // Expect the revert with `Unauthorized` selector + vm.expectRevert(ISuperchainWETH.Unauthorized.selector); + + // Call the `mint` function with the non-bridge caller vm.prank(_caller); - superchainWeth.deposit{ value: _amount }(); + superchainWeth.crosschainMint(_to, _amount); + } - // Act + /// @notice Tests the `crosschainMint` with non custom gas token succeeds and emits the `CrosschainMinted` event. + function testFuzz_crosschainMint_fromBridgeNonCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Get the total supply and balance of `_to` before the mint to compare later on the assertions + uint256 _totalSupplyBefore = superchainWeth.totalSupply(); + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); + + // Look for the emit of the `Transfer` event vm.expectEmit(address(superchainWeth)); - emit Transfer(_caller, address(0), _amount); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `CrosschainMinted` event vm.expectEmit(address(superchainWeth)); - emit SendERC20(_caller, _recipient, _amount, _chainId); - vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 1); - vm.expectCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall( - IL2ToL2CrossDomainMessenger.sendMessage, - ( - _chainId, - address(superchainWeth), - abi.encodeCall(superchainWeth.relayERC20, (_caller, _recipient, _amount)) - ) - ), - 1 - ); - vm.prank(_caller); - superchainWeth.sendERC20(_recipient, _amount, _chainId); + emit CrosschainMinted(_to, _amount); - // Assert - assertEq(_caller.balance, 0); - assertEq(superchainWeth.balanceOf(_caller), 0); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + + // Expect the call to the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 1); + + // Call the `mint` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.balanceOf(Predeploys.ETH_LIQUIDITY), 0); + assertEq(address(superchainWeth).balance, _amount); } - /// @notice Tests that the sendERC20 function can be called with a sufficient balance on a - /// custom gas token chain. Also tests that the proper calls are made and the proper - /// events are emitted but ETH is not burned via the ETHLiquidity contract. - /// @param _amount The amount of WETH to send. - /// @param _chainId The chain ID to send the WETH to. - function testFuzz_sendERC20_sufficientFromCustomGasTokenChain_succeeds(uint256 _amount, uint256 _chainId) public { - // Assume - vm.assume(_chainId != block.chainid); + /// @notice Tests the `crosschainMint` with custom gas token succeeds and emits the `CrosschainMinted` event. + function testFuzz_crosschainMint_fromBridgeCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); - // Arrange - vm.deal(alice, _amount); - vm.prank(alice); - superchainWeth.deposit{ value: _amount }(); - vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + // Get the balance of `_to` before the mint to compare later on the assertions + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); - // Act + // Look for the emit of the `Transfer` event vm.expectEmit(address(superchainWeth)); - emit Transfer(alice, address(0), _amount); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `CrosschainMinted` event vm.expectEmit(address(superchainWeth)); - emit SendERC20(alice, bob, _amount, _chainId); - vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 0); - vm.expectCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall( - IL2ToL2CrossDomainMessenger.sendMessage, - (_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (alice, bob, _amount))) - ), - 1 - ); - vm.prank(alice); - superchainWeth.sendERC20(bob, _amount, _chainId); + emit CrosschainMinted(_to, _amount); - // Assert - assertEq(alice.balance, 0); - assertEq(superchainWeth.balanceOf(alice), 0); - } + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); - /// @notice Tests that the sendERC20 function reverts when called with insufficient balance. - /// @param _amount The amount of WETH to send. - /// @param _chainId The chain ID to send the WETH to. - function testFuzz_sendERC20_insufficientBalance_fails(uint256 _amount, uint256 _chainId) public { - // Assume - vm.assume(_chainId != block.chainid); - _amount = bound(_amount, 0, type(uint248).max - 1); + // Expect to not call the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); - // Arrange - vm.deal(alice, _amount); - vm.prank(alice); - superchainWeth.deposit{ value: _amount }(); + // Call the `mint` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_to, _amount); - // Act - vm.expectRevert(); - superchainWeth.sendERC20(bob, _amount + 1, _chainId); + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.balanceOf(Predeploys.ETH_LIQUIDITY), 0); + assertEq(superchainWeth.totalSupply(), 0); + assertEq(address(superchainWeth).balance, 0); + } - // Assert - assertEq(alice.balance, 0); - assertEq(superchainWeth.balanceOf(alice), _amount); + /// @notice Tests the `crosschainBurn` function reverts when the caller is not the `SuperchainTokenBridge`. + function testFuzz_crosschainBurn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(ISuperchainWETH.Unauthorized.selector); + + // Call the `burn` function with the non-bridge caller + vm.prank(_caller); + superchainWeth.crosschainBurn(_from, _amount); } - /// @notice Tests that the relayERC20 function can be called from the - /// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the - /// SuperchainWETH contract. - /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessenger_succeeds(address _sender, uint256 _amount, uint256 _chainId) public { - // Assume - vm.assume(_chainId != block.chainid); - vm.assume(_sender != address(ethLiquidity)); - vm.assume(_sender != address(superchainWeth)); + /// @notice Tests the `crosschainBurn` with non custom gas token burns the amount and emits the `CrosschainBurnt` + /// event. + function testFuzz_crosschainBurn_fromBridgeNonCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); - // Arrange - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), - abi.encode(address(superchainWeth)) - ); - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), - abi.encode(_chainId) - ); + // Deposit some tokens to `_from` so then they can be burned + vm.deal(_from, _amount); + vm.prank(_from); + superchainWeth.deposit{ value: _amount }(); - // Act + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainWeth.totalSupply(); + uint256 _fromBalanceBefore = superchainWeth.balanceOf(_from); + + // Look for the emit of the `Transfer` event vm.expectEmit(address(superchainWeth)); - emit RelayERC20(_sender, bob, _amount, _chainId); - vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 1); - vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(_sender, bob, _amount); + emit Transfer(_from, ZERO_ADDRESS, _amount); - // Assert - assertEq(address(superchainWeth).balance, _amount); - assertEq(superchainWeth.balanceOf(bob), _amount); - } + // Look for the emit of the `CrosschainBurnt` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainBurnt(_from, _amount); - /// @notice Tests that the relayERC20 function can be called from the - /// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the - /// SuperchainWETH contract, even when the chain is a custom gas token chain. Shows - /// that ETH is not minted in this case but the SuperchainWETH balance is updated. - /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessengerCustomGasTokenChain_succeeds( - address _sender, - uint256 _amount, - uint256 _chainId - ) - public - { - // Assume - vm.assume(_chainId != block.chainid); - vm.assume(_sender != address(ethLiquidity)); - vm.assume(_sender != address(superchainWeth)); - _amount = bound(_amount, 0, type(uint248).max - 1); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); - // Arrange - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), - abi.encode(address(superchainWeth)) - ); - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), - abi.encode(_chainId) - ); - vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + // Expect the call to the `burn` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 1); - // Act - vm.expectEmit(address(superchainWeth)); - emit RelayERC20(_sender, bob, _amount, _chainId); - vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); - vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(_sender, bob, _amount); + // Call the `burn` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainBurn(_from, _amount); - // Assert + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainWeth.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainWeth.balanceOf(_from), _fromBalanceBefore - _amount); assertEq(address(superchainWeth).balance, 0); - assertEq(superchainWeth.balanceOf(bob), _amount); } - /// @notice Tests that the relayERC20 function reverts when not called from the - /// L2ToL2CrossDomainMessenger. - /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_notFromMessenger_fails(address _sender, uint256 _amount) public { - // Assume + /// @notice Tests the `crosschainBurn` with custom gas token burns the amount and emits the `CrosschainBurnt` + /// event. + function testFuzz_crosschainBurn_fromBridgeCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); - // Arrange - // Nothing to arrange. + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); - // Act - vm.expectRevert(ISuperchainWETH.CallerNotL2ToL2CrossDomainMessenger.selector); - vm.prank(alice); - superchainWeth.relayERC20(_sender, bob, _amount); + // Mint some tokens to `_from` so then they can be burned + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_from, _amount); - // Assert + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainWeth.totalSupply(); + uint256 _fromBalanceBefore = superchainWeth.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(_from, ZERO_ADDRESS, _amount); + + // Look for the emit of the `CrosschainBurnt` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainBurnt(_from, _amount); + + // Expect to not call the `burn` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 0); + + // Call the `burn` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainBurn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainWeth.balanceOf(_from), _fromBalanceBefore - _amount); + assertEq(superchainWeth.totalSupply(), _totalSupplyBefore); assertEq(address(superchainWeth).balance, 0); - assertEq(superchainWeth.balanceOf(bob), 0); } - /// @notice Tests that the relayERC20 function reverts when called from the - /// L2ToL2CrossDomainMessenger but the crossDomainMessageSender is not the - /// SuperchainWETH contract. - /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessengerNotFromSuperchainWETH_fails(address _sender, uint256 _amount) public { + /// @notice Tests that the `crosschainBurn` function reverts when called with insufficient balance. + function testFuzz_crosschainBurn_insufficientBalance_fails(address _from, uint256 _amount) public { // Assume + vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); // Arrange - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), - abi.encode(address(alice)) - ); + vm.deal(_from, _amount); + vm.prank(_from); + superchainWeth.deposit{ value: _amount }(); // Act - vm.expectRevert(ISuperchainWETH.InvalidCrossDomainSender.selector); - vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(_sender, bob, _amount); + vm.expectRevert(); + superchainWeth.crosschainBurn(_from, _amount + 1); // Assert - assertEq(address(superchainWeth).balance, 0); - assertEq(superchainWeth.balanceOf(bob), 0); + assertEq(_from.balance, 0); + assertEq(superchainWeth.balanceOf(_from), _amount); } } diff --git a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol index e9b8a3be828a..24ffc0a57963 100644 --- a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol @@ -6,22 +6,12 @@ import { StdUtils } from "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; - // Interfaces -import { ISuperchainWETHERC20 } from "src/L2/interfaces/ISuperchainWETH.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; +import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; /// @title SuperchainWETH_User /// @notice Actor contract that interacts with the SuperchainWETH contract. contract SuperchainWETH_User is StdUtils { - /// @notice Cross domain message data. - struct MessageData { - bytes32 id; - uint256 amount; - } - /// @notice Flag to indicate if the test has failed. bool public failed = false; @@ -29,18 +19,12 @@ contract SuperchainWETH_User is StdUtils { Vm internal vm; /// @notice The SuperchainWETH contract. - ISuperchainWETHERC20 internal weth; - - /// @notice Mapping of sent messages. - mapping(bytes32 => bool) internal sent; - - /// @notice Array of unrelayed messages. - MessageData[] internal unrelayed; + ISuperchainWETH internal weth; /// @param _vm The Vm contract. /// @param _weth The SuperchainWETH contract. /// @param _balance The initial balance of the contract. - constructor(Vm _vm, ISuperchainWETHERC20 _weth, uint256 _balance) { + constructor(Vm _vm, ISuperchainWETH _weth, uint256 _balance) { vm = _vm; weth = _weth; vm.deal(address(this), _balance); @@ -76,72 +60,6 @@ contract SuperchainWETH_User is StdUtils { failed = true; } } - - /// @notice Send ERC20 tokens to another chain. - /// @param _amount The amount of ERC20 tokens to send. - /// @param _chainId The chain ID to send the tokens to. - /// @param _messageId The message ID. - function sendERC20(uint256 _amount, uint256 _chainId, bytes32 _messageId) public { - // Make sure we aren't reusing a message ID. - if (sent[_messageId]) { - return; - } - - // Bound send amount to our WETH balance. - _amount = bound(_amount, 0, weth.balanceOf(address(this))); - - // Prevent receiving chain ID from being the same as the current chain ID. - _chainId = _chainId == block.chainid ? _chainId + 1 : _chainId; - - // Send the amount. - try weth.sendERC20(address(this), _amount, _chainId) { - // Success. - } catch { - failed = true; - } - - // Mark message as sent. - sent[_messageId] = true; - unrelayed.push(MessageData({ id: _messageId, amount: _amount })); - } - - /// @notice Relay a message from another chain. - function relayMessage(uint256 _source) public { - // Make sure there are unrelayed messages. - if (unrelayed.length == 0) { - return; - } - - // Grab the latest unrelayed message. - MessageData memory message = unrelayed[unrelayed.length - 1]; - - // Simulate the cross-domain message. - // Make sure the cross-domain message sender is set to this contract. - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), - abi.encode(address(weth)) - ); - - // Simulate the cross-domain message source to any chain. - vm.mockCall( - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), - abi.encode(_source) - ); - - // Prank the relayERC20 function. - // Balance will just go back to our own account. - vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - try weth.relayERC20(address(this), address(this), message.amount) { - // Success. - } catch { - failed = true; - } - - // Remove the message from the unrelayed list. - unrelayed.pop(); - } } /// @title SuperchainWETH_SendSucceeds_Invariant @@ -167,11 +85,9 @@ contract SuperchainWETH_SendSucceeds_Invariant is CommonTest { targetContract(address(actor)); // Set the target selectors. - bytes4[] memory selectors = new bytes4[](4); + bytes4[] memory selectors = new bytes4[](2); selectors[0] = actor.deposit.selector; selectors[1] = actor.withdraw.selector; - selectors[2] = actor.sendERC20.selector; - selectors[3] = actor.relayMessage.selector; FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors }); targetSelector(selector); } diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 7e5cebd7f803..10ee252218fc 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -43,7 +43,7 @@ import { ISequencerFeeVault } from "src/L2/interfaces/ISequencerFeeVault.sol"; import { IL1FeeVault } from "src/L2/interfaces/IL1FeeVault.sol"; import { IGasPriceOracle } from "src/L2/interfaces/IGasPriceOracle.sol"; import { IL1Block } from "src/L2/interfaces/IL1Block.sol"; -import { ISuperchainWETHERC20 } from "src/L2/interfaces/ISuperchainWETH.sol"; +import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; import { IWETH } from "src/universal/interfaces/IWETH.sol"; import { IGovernanceToken } from "src/governance/interfaces/IGovernanceToken.sol"; @@ -106,7 +106,7 @@ contract Setup { IGovernanceToken governanceToken = IGovernanceToken(Predeploys.GOVERNANCE_TOKEN); ILegacyMessagePasser legacyMessagePasser = ILegacyMessagePasser(Predeploys.LEGACY_MESSAGE_PASSER); IWETH weth = IWETH(payable(Predeploys.WETH)); - ISuperchainWETHERC20 superchainWeth = ISuperchainWETHERC20(payable(Predeploys.SUPERCHAIN_WETH)); + ISuperchainWETH superchainWeth = ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)); IETHLiquidity ethLiquidity = IETHLiquidity(Predeploys.ETH_LIQUIDITY); ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory =