Commit 574aba77 authored by gotzenx's avatar gotzenx Committed by GitHub

feat: SuperchainWETH redesign (#12514)

* 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: default avataragusduha <agusnduha@gmail.com>

* fix: remove superchain erc20 base (#70)

* refactor: update isuperchainweth (#71)


---------
Co-authored-by: default avataragusduha <agusnduha@gmail.com>

* 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: default avatar0xDiscotech <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: default avatar0xng <ng@defi.sucks>
Co-authored-by: default avatar0xParticle <particle@defi.sucks>
Co-authored-by: default avatargotzenx <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: default avatarDisco <131301107+0xDiscotech@users.noreply.github.com>
Co-authored-by: default avatar0xng <ng@defi.sucks>
Co-authored-by: default avatar0xParticle <particle@defi.sucks>
Co-authored-by: default avatargotzenx <78360669+gotzenx@users.noreply.github.com>

* fix: SuperchainWETH redesign fixes (#110)

* fix: superchainWETH redesign fixes

* fix: withdraw arg

* fix: fix revert in SuperchainWETH tests (#112)

---------
Co-authored-by: default avatarAgusDuha <81362284+agusduha@users.noreply.github.com>
Co-authored-by: default avatarDisco <131301107+0xDiscotech@users.noreply.github.com>
Co-authored-by: default avatar0xng <ng@defi.sucks>
Co-authored-by: default avatar0xParticle <particle@defi.sucks>
Co-authored-by: default avataragusduha <agusnduha@gmail.com>
parent 76beff3d
......@@ -132,8 +132,8 @@
"sourceCodeHash": "0x4f539e9d9096d31e861982b8f751fa2d7de0849590523375cf92e175294d1036"
},
"src/L2/SuperchainWETH.sol": {
"initCodeHash": "0x50f6ea9bfe650fcf792e98e44b1bf66c036fd0e6d4b753da680253d7d8609816",
"sourceCodeHash": "0x82d03262decf52d5954d40bca8703f96a0f3ba7accf6c1d75292856c2f34cf8f"
"initCodeHash": "0x09c7efed7d6c8ae5981f6e7a75c7b8c675f73d679265d15a010844ad9b41fa9b",
"sourceCodeHash": "0x8d7612a71deaadfb324c4136673df96019211292ff54494fa4b7724e2e5dd22a"
},
"src/L2/WETH.sol": {
"initCodeHash": "0xfb253765520690623f177941c2cd9eba23e4c6d15063bccdd5e98081329d8956",
......
......@@ -75,82 +75,72 @@
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
"inputs": [
{
"inputs": [],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
"internalType": "address",
"name": "_from",
"type": "address"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"stateMutability": "view",
"name": "crosschainBurn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "dst",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"name": "_amount",
"type": "uint256"
}
],
"name": "relayERC20",
"name": "crosschainMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "address",
"name": "dst",
"type": "address"
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
"inputs": [],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"name": "sendERC20",
"outputs": [],
"stateMutability": "nonpayable",
"stateMutability": "view",
"type": "function"
},
{
......@@ -249,7 +239,7 @@
"inputs": [
{
"internalType": "uint256",
"name": "wad",
"name": "_amount",
"type": "uint256"
}
],
......@@ -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
......@@ -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,10 @@ 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 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 {
......@@ -32,68 +32,56 @@ contract SuperchainWETH is WETH98, ISuperchainWETH, ISemver {
}
/// @inheritdoc WETH98
function withdraw(uint256 wad) public override {
function withdraw(uint256 _amount) public override {
if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
super.withdraw(wad);
super.withdraw(_amount);
}
/// @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 }();
/// @notice Mints WETH to an address.
/// @param _to The address to mint WETH to.
/// @param _amount The amount of WETH to mint.
function _mint(address _to, uint256 _amount) internal {
balanceOf[_to] += _amount;
emit Transfer(address(0), _to, _amount);
}
// 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))
});
// Emit event.
emit SendERC20(msg.sender, dst, wad, chainId);
/// @notice Burns WETH from an address.
/// @param _from The address to burn WETH from.
/// @param _amount The amount of WETH to burn.
function _burn(address _from, uint256 _amount) internal {
balanceOf[_from] -= _amount;
emit Transfer(_from, address(0), _amount);
}
/// @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 {
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
_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();
emit CrosschainMinted(_to, _amount);
}
// Mint to user's balance.
_mint(dst, 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 {
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
// Emit event.
emit RelayERC20(from, dst, wad, source);
}
_burn(_from, _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);
// Burn to ETHLiquidity contract.
if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _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);
emit CrosschainBurnt(_from, _amount);
}
}
......@@ -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 { }
......@@ -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,253 @@ 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));
/// @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);
// Expect the revert with `Unauthorized` selector
vm.expectRevert(ISuperchainWETH.Unauthorized.selector);
// Call the `mint` function with the non-bridge caller
vm.prank(_caller);
superchainWeth.crosschainMint(_to, _amount);
}
/// @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);
// Arrange
vm.deal(_caller, _amount);
// 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(ZERO_ADDRESS, _to, _amount);
// Look for the emit of the `CrosschainMinted` event
vm.expectEmit(address(superchainWeth));
emit CrosschainMinted(_to, _amount);
// 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 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);
// Get the balance of `_to` before the mint to compare later on the assertions
uint256 _toBalanceBefore = superchainWeth.balanceOf(_to);
// Look for the emit of the `Transfer` event
vm.expectEmit(address(superchainWeth));
emit Transfer(ZERO_ADDRESS, _to, _amount);
// Look for the emit of the `CrosschainMinted` event
vm.expectEmit(address(superchainWeth));
emit CrosschainMinted(_to, _amount);
// Mock the `isCustomGasToken` function to return false
_mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Expect to not call the `mint` function in the `ETHLiquidity` contract
vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0);
// 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.balanceOf(_to), _toBalanceBefore + _amount);
assertEq(superchainWeth.balanceOf(Predeploys.ETH_LIQUIDITY), 0);
assertEq(superchainWeth.totalSupply(), 0);
assertEq(address(superchainWeth).balance, 0);
}
/// @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 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);
// 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 Transfer(_caller, address(0), _amount);
emit Transfer(_from, ZERO_ADDRESS, _amount);
// Look for the emit of the `CrosschainBurnt` event
vm.expectEmit(address(superchainWeth));
emit SendERC20(_caller, _recipient, _amount, _chainId);
emit CrosschainBurnt(_from, _amount);
// Mock the `isCustomGasToken` function to return false
_mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false));
// Expect the call to the `burn` function in the `ETHLiquidity` contract
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);
// Assert
assertEq(_caller.balance, 0);
assertEq(superchainWeth.balanceOf(_caller), 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.totalSupply(), _totalSupplyBefore - _amount);
assertEq(superchainWeth.balanceOf(_from), _fromBalanceBefore - _amount);
assertEq(address(superchainWeth).balance, 0);
}
/// @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 `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
vm.deal(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Mock the `isCustomGasToken` function to return false
_mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
// Mint some tokens to `_from` so then they can be burned
vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE);
superchainWeth.crosschainMint(_from, _amount);
// 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(alice, address(0), _amount);
emit Transfer(_from, ZERO_ADDRESS, _amount);
// Look for the emit of the `CrosschainBurnt` event
vm.expectEmit(address(superchainWeth));
emit SendERC20(alice, bob, _amount, _chainId);
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);
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);
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), 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);
}
/// @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 {
/// @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(_chainId != block.chainid);
vm.assume(_from != ZERO_ADDRESS);
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
vm.prank(alice);
vm.deal(_from, _amount);
vm.prank(_from);
superchainWeth.deposit{ value: _amount }();
// Act
vm.expectRevert();
superchainWeth.sendERC20(bob, _amount + 1, _chainId);
superchainWeth.crosschainBurn(_from, _amount + 1);
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), _amount);
assertEq(_from.balance, 0);
assertEq(superchainWeth.balanceOf(_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));
_amount = bound(_amount, 0, type(uint248).max - 1);
/// @notice Test that the internal mint function reverts to protect against accidentally changing the visibility.
function testFuzz_calling_internal_mint_function_reverts(address _caller, address _to, uint256 _amount) public {
// 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)
);
bytes memory _calldata = abi.encodeWithSignature("_mint(address,uint256)", _to, _amount);
vm.expectRevert(bytes(""));
// Act
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);
vm.prank(_caller);
(bool success,) = address(superchainWeth).call(_calldata);
// Assert
assertEq(address(superchainWeth).balance, _amount);
assertEq(superchainWeth.balanceOf(bob), _amount);
assertFalse(success);
}
/// @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);
/// @notice Test that the mint function reverts to protect against accidentally changing the visibility.
function testFuzz_calling_mint_function_reverts(address _caller, address _to, uint256 _amount) public {
// 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));
bytes memory _calldata = abi.encodeWithSignature("mint(address,uint256)", _to, _amount);
vm.expectRevert(bytes(""));
// 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);
vm.prank(_caller);
(bool success,) = address(superchainWeth).call(_calldata);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), _amount);
assertFalse(success);
}
/// @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
_amount = bound(_amount, 0, type(uint248).max - 1);
/// @notice Test that the internal burn function reverts to protect against accidentally changing the visibility.
function testFuzz_calling_internal_burn_function_reverts(address _caller, address _from, uint256 _amount) public {
// Arrange
// Nothing to arrange.
bytes memory _calldata = abi.encodeWithSignature("_burn(address,uint256)", _from, _amount);
vm.expectRevert(bytes(""));
// Act
vm.expectRevert(ISuperchainWETH.CallerNotL2ToL2CrossDomainMessenger.selector);
vm.prank(alice);
superchainWeth.relayERC20(_sender, bob, _amount);
vm.prank(_caller);
(bool success,) = address(superchainWeth).call(_calldata);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), 0);
assertFalse(success);
}
/// @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 {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
/// @notice Test that the burn function reverts to protect against accidentally changing the visibility.
function testFuzz_calling_burn_function_reverts(address _caller, address _from, uint256 _amount) public {
// Arrange
vm.mockCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(alice))
);
bytes memory _calldata = abi.encodeWithSignature("burn(address,uint256)", _from, _amount);
vm.expectRevert(bytes(""));
// Act
vm.expectRevert(ISuperchainWETH.InvalidCrossDomainSender.selector);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(_sender, bob, _amount);
vm.prank(_caller);
(bool success,) = address(superchainWeth).call(_calldata);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), 0);
assertFalse(success);
}
}
......@@ -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);
}
......
......@@ -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 =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment