diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 8f44a70958a7..fe85d95b7644 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -116,8 +116,8 @@ "sourceCodeHash": "0x4b806cc85cead74c8df34ab08f4b6c6a95a1a387a335ec8a7cb2de4ea4e1cf41" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xdd16dbc0ccbbac53ec2d4273f06334960a907a9f20b7c40300833227ee31d0de", - "sourceCodeHash": "0x910d43a17800df64dbc104f69ef1f900ca761cec4949c01d1c1126fde5268349" + "initCodeHash": "0x5158735abf860249f9449ab8e1cb60f4183c701d08ff9a9a3066575d5e26617a", + "sourceCodeHash": "0x9cc730bedbd1adc6422fc186910d4f47d124701a7a17fa33cf2ad3626ce2e684" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0x2e6551705e493bacba8cffe22e564d5c401ae5bb02577a5424e0d32784e13e74", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 6eb57764a8cb..754a2ff135a9 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -629,6 +629,11 @@ "name": "TotalSupplyOverflow", "type": "error" }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, { "inputs": [], "name": "ZeroAddress", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 731657e900fc..0551984959de 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -6,17 +6,10 @@ import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol"; -/// @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 this -/// OptimismSuperchainERC20. -error InvalidCrossDomainSender(); - /// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. error OnlyBridge(); @@ -31,10 +24,13 @@ error ZeroAddress(); /// token, turning it fungible and interoperable across the superchain. Likewise, it also enables the inverse /// conversion path. /// Moreover, it builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain binding. -contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable, ERC165 { - /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. - address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - +contract OptimismSuperchainERC20 is + IOptimismSuperchainERC20Extension, + SuperchainERC20, + ISemver, + Initializable, + ERC165 +{ /// @notice Address of the StandardBridge Predeploy. address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; @@ -48,16 +44,10 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS struct OptimismSuperchainERC20Metadata { /// @notice Address of the corresponding version of this token on the remote chain. address remoteToken; - /// @notice Name of the token - string name; - /// @notice Symbol of the token - string symbol; - /// @notice Decimals of the token - uint8 decimals; } /// @notice Returns the storage for the OptimismSuperchainERC20Metadata. - function _getMetadataStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) { + function _getStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) { assembly { _storage.slot := OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT } @@ -92,11 +82,10 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS external initializer { - OptimismSuperchainERC20Metadata storage _storage = _getMetadataStorage(); + _setMetadataStorage(_name, _symbol, _decimals); + + OptimismSuperchainERC20Metadata storage _storage = _getStorage(); _storage.remoteToken = _remoteToken; - _storage.name = _name; - _storage.symbol = _symbol; - _storage.decimals = _decimals; } /// @notice Allows the L2StandardBridge to mint tokens. @@ -121,64 +110,9 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS emit Burn(_from, _amount); } - /// @notice Sends tokens to some target address on another chain. - /// @param _to Address to send tokens to. - /// @param _amount Amount of tokens to send. - /// @param _chainId Chain ID of the destination chain. - function sendERC20(address _to, uint256 _amount, uint256 _chainId) external { - if (_to == address(0)) revert ZeroAddress(); - - _burn(msg.sender, _amount); - - bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); - IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); - - emit SendERC20(msg.sender, _to, _amount, _chainId); - } - - /// @notice Relays tokens received from another chain. - /// @param _from Address of the msg.sender of sendERC20 on the source chain. - /// @param _to Address to relay tokens to. - /// @param _amount Amount of tokens to relay. - function relayERC20(address _from, address _to, uint256 _amount) external { - if (_to == address(0)) revert ZeroAddress(); - - if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger(); - - if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { - revert InvalidCrossDomainSender(); - } - - uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); - - _mint(_to, _amount); - - emit RelayERC20(_from, _to, _amount, source); - } - /// @notice Returns the address of the corresponding version of this token on the remote chain. function remoteToken() public view override returns (address) { - return _getMetadataStorage().remoteToken; - } - - /// @notice Returns the name of the token. - function name() public view virtual override returns (string memory) { - return _getMetadataStorage().name; - } - - /// @notice Returns the symbol of the token. - function symbol() public view virtual override returns (string memory) { - return _getMetadataStorage().symbol; - } - - /// @notice Returns the number of decimals used to get its user representation. - /// For example, if `decimals` equals `2`, a balance of `505` tokens should - /// be displayed to a user as `5.05` (`505 / 10 ** 2`). - /// NOTE: This information is only used for _display_ purposes: it in - /// no way affects any of the arithmetic of the contract, including - /// {IERC20-balanceOf} and {IERC20-transfer}. - function decimals() public view override returns (uint8) { - return _getMetadataStorage().decimals; + return _getStorage().remoteToken; } /// @notice ERC165 interface check function. diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol new file mode 100644 index 000000000000..53c572a58e6d --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @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 this SuperchainERC20. +error InvalidCrossDomainSender(); + +/// @notice Thrown when attempting to mint or burn tokens and the account is the zero address. +error ZeroAddress(); + +/// @title SuperchainERC20 +/// @notice SuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token +/// bridging to make it fungible across the Superchain. It builds on top of the L2ToL2CrossDomainMessenger for +/// both replay protection and domain binding. +abstract contract SuperchainERC20 is ISuperchainERC20Extensions, ERC20 { + /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + /// @notice Storage slot that the SuperchainERC20Metadata struct is stored at. + /// keccak256(abi.encode(uint256(keccak256("superchainERC20.metadata")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 internal constant SUPERCHAIN_ERC20_METADATA_SLOT = + 0xd17d6ca6a839692cc315581e57453e7dbbeba09485cfb8c48daa1d1181778600; + + /// @notice Storage struct for the SuperchainERC20 metadata. + /// @custom:storage-location erc7201:superchainERC20.metadata + struct SuperchainERC20Metadata { + /// @notice Name of the token + string name; + /// @notice Symbol of the token + string symbol; + /// @notice Decimals of the token + uint8 decimals; + } + + /// @notice Returns the storage for the SuperchainERC20Metadata. + function _getMetadataStorage() private pure returns (SuperchainERC20Metadata storage _storage) { + assembly { + _storage.slot := SUPERCHAIN_ERC20_METADATA_SLOT + } + } + + /// @notice Sets the storage for the SuperchainERC20Metadata. + /// @param _name Name of the token. + /// @param _symbol Symbol of the token. + /// @param _decimals Decimals of the token. + function _setMetadataStorage(string memory _name, string memory _symbol, uint8 _decimals) internal { + SuperchainERC20Metadata storage _storage = _getMetadataStorage(); + _storage.name = _name; + _storage.symbol = _symbol; + _storage.decimals = _decimals; + } + + /// @notice Sends tokens to some target address on another chain. + /// @param _to Address to send tokens to. + /// @param _amount Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. + function sendERC20(address _to, uint256 _amount, uint256 _chainId) external virtual { + if (_to == address(0)) revert ZeroAddress(); + + _burn(msg.sender, _amount); + + bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); + IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); + + emit SendERC20(msg.sender, _to, _amount, _chainId); + } + + /// @notice Relays tokens received from another chain. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _from, address _to, uint256 _amount) external virtual { + if (_to == address(0)) revert ZeroAddress(); + + if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger(); + + if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { + revert InvalidCrossDomainSender(); + } + + uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); + + _mint(_to, _amount); + + emit RelayERC20(_from, _to, _amount, source); + } + + /// @notice Returns the name of the token. + function name() public view virtual override returns (string memory) { + return _getMetadataStorage().name; + } + + /// @notice Returns the symbol of the token. + function symbol() public view virtual override returns (string memory) { + return _getMetadataStorage().symbol; + } + + /// @notice Returns the number of decimals used to get its user representation. + /// For example, if `decimals` equals `2`, a balance of `505` tokens should + /// be displayed to a user as `5.05` (`505 / 10 ** 2`). + /// NOTE: This information is only used for _display_ purposes: it in + /// no way affects any of the arithmetic of the contract, including + /// {IERC20-balanceOf} and {IERC20-transfer}. + function decimals() public view virtual override returns (uint8) { + return _getMetadataStorage().decimals; + } +} diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 0e82aca948a2..12b5b88e30c1 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -14,13 +14,17 @@ import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165. // Target contract import { - OptimismSuperchainERC20, - IOptimismSuperchainERC20Extension, + OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, OnlyBridge +} from "src/L2/OptimismSuperchainERC20.sol"; + +import { CallerNotL2ToL2CrossDomainMessenger, InvalidCrossDomainSender, - OnlyBridge, + SuperchainERC20, + ISuperchainERC20Extensions, ZeroAddress -} from "src/L2/OptimismSuperchainERC20.sol"; +} from "src/L2/SuperchainERC20.sol"; + import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test @@ -127,11 +131,11 @@ contract OptimismSuperchainERC20Test is Test { uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); // Look for the emit of the `Transfer` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); // Look for the emit of the `Mint` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IOptimismSuperchainERC20Extension.Mint(_to, _amount); // Call the `mint` function with the bridge caller @@ -180,11 +184,11 @@ contract OptimismSuperchainERC20Test is Test { uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); // Look for the emit of the `Transfer` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); // Look for the emit of the `Burn` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IOptimismSuperchainERC20Extension.Burn(_from, _amount); // Call the `burn` function with the bridge caller @@ -222,11 +226,11 @@ contract OptimismSuperchainERC20Test is Test { uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); // Look for the emit of the `Transfer` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); // Look for the emit of the `SendERC20` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); // Mock the call over the `sendMessage` function and expect it to be called properly @@ -330,11 +334,11 @@ contract OptimismSuperchainERC20Test is Test { uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); // Look for the emit of the `Transfer` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); // Look for the emit of the `RelayERC20` event - vm.expectEmit(true, true, true, true, address(superchainERC20)); + vm.expectEmit(address(superchainERC20)); emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); // Call the `relayERC20` function with the messenger caller diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol new file mode 100644 index 000000000000..128d0577e66a --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test } from "forge-std/Test.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; + +// Target contract +import { + CallerNotL2ToL2CrossDomainMessenger, + InvalidCrossDomainSender, + SuperchainERC20, + ISuperchainERC20Extensions, + ZeroAddress +} from "src/L2/SuperchainERC20.sol"; + +/// @notice Mock contract for the SuperchainERC20 contract so tests can mint tokens. +contract SuperchainERC20Mock is SuperchainERC20 { + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + _setMetadataStorage(_name, _symbol, _decimals); + } + + function mint(address _account, uint256 _amount) public { + _mint(_account, _amount); + } +} + +/// @title SuperchainERC20Test +/// @notice Contract for testing the SuperchainERC20 contract. +contract SuperchainERC20Test is Test { + address internal constant ZERO_ADDRESS = address(0); + string internal constant NAME = "SuperchainERC20"; + string internal constant SYMBOL = "SCE"; + uint8 internal constant DECIMALS = 18; + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + SuperchainERC20 public superchainERC20Impl; + SuperchainERC20Mock public superchainERC20; + + /// @notice Sets up the test suite. + function setUp() public { + superchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, DECIMALS); + } + + /// @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 Test that the contract's `constructor` sets the correct values. + function test_constructor_succeeds() public view { + assertEq(superchainERC20.name(), NAME); + assertEq(superchainERC20.symbol(), SYMBOL); + assertEq(superchainERC20.decimals(), DECIMALS); + } + + /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. + function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Call the `sendERC20` function with the zero address + superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); + } + + /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20` + /// event. + function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { + // Ensure `_sender` is not the zero address + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + + // Mint some tokens to the sender so then they can be sent + superchainERC20.mint(_sender, _amount); + + // Get the total supply and balance of `_sender` before the send to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainERC20)); + emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); + + // Look for the emit of the `SendERC20` event + vm.expectEmit(address(superchainERC20)); + emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); + + // Mock the call over the `sendMessage` function and expect it to be called properly + bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector( + IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainERC20), _message + ), + abi.encode("") + ); + + // Call the `sendERC20` function + vm.prank(_sender); + superchainERC20.sendERC20(_to, _amount, _chainId); + + // Check the total supply and balance of `_sender` after the send were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. + function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the messenger + vm.assume(_caller != MESSENGER); + vm.assume(_to != ZERO_ADDRESS); + + // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector + vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector); + + // Call the `relayERC20` function with the non-messenger caller + vm.prank(_caller); + superchainERC20.relayERC20(_caller, _to, _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not + /// the same SuperchainERC20 address. + function testFuzz_relayERC20_notCrossDomainSender_reverts( + address _crossDomainMessageSender, + address _to, + uint256 _amount + ) + public + { + vm.assume(_to != ZERO_ADDRESS); + vm.assume(_crossDomainMessageSender != address(superchainERC20)); + + // Mock the call over the `crossDomainMessageSender` function setting a wrong sender + vm.mockCall( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(_crossDomainMessageSender) + ); + + // Expect the revert with `InvalidCrossDomainSender` selector + vm.expectRevert(InvalidCrossDomainSender.selector); + + // Call the `relayERC20` function with the sender caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the `_to` address is the zero address. + function testFuzz_relayERC20_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Mock the call over the `crossDomainMessageSender` function setting the same address as value + vm.mockCall( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(superchainERC20)) + ); + + // Call the `relayERC20` function with the zero address + vm.prank(MESSENGER); + superchainERC20.relayERC20({ _from: ZERO_ADDRESS, _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. + function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { + vm.assume(_from != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + + // Mock the call over the `crossDomainMessageSender` function setting the same address as value + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(superchainERC20)) + ); + + // Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), + abi.encode(_source) + ); + + // Get the total supply and balance of `_to` before the relay to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayERC20` event + vm.expectEmit(address(superchainERC20)); + emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); + + // Call the `relayERC20` function with the messenger caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_from, _to, _amount); + + // Check the total supply and balance of `_to` after the relay were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + } + + /// @notice Tests the `decimals` function always returns the correct value. + function testFuzz_decimals_succeeds(uint8 _decimals) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, _decimals); + assertEq(_newSuperchainERC20.decimals(), _decimals); + } + + /// @notice Tests the `name` function always returns the correct value. + function testFuzz_name_succeeds(string memory _name) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(_name, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.name(), _name); + } + + /// @notice Tests the `symbol` function always returns the correct value. + function testFuzz_symbol_succeeds(string memory _symbol) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, _symbol, DECIMALS); + assertEq(_newSuperchainERC20.symbol(), _symbol); + } +}