diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol new file mode 100644 index 00000000000..ad4a378a5fd --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { L2StandardBridge } from "./L2StandardBridge.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/// @notice Thrown when the decimals of the tokens are not the same. +error InvalidDecimals(); + +/// @notice Thrown when the legacy address is not found in the OptimismMintableERC20Factory. +error InvalidLegacyAddress(); + +/// @notice Thrown when the SuperchainERC20 address is not found in the SuperchainERC20Factory. +error InvalidSuperchainAddress(); + +/// @notice Thrown when the remote addresses of the tokens are not the same. +error InvalidTokenPair(); + +// TODO: Use OptimismMintableERC20Factory contract instead of interface +interface IOptimismMintableERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Move to a separate file +interface ISuperchainERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Use an existing interface with `mint` and `burn`? +interface MintableAndBurnable is IERC20 { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +/// @custom:proxied +/// @custom:predeploy 0x4200000000000000000000000000000000000010 +/// @title L2StandardBridgeInterop +/// @notice The L2StandardBridgeInterop is an extension of the L2StandardBridge that allows for +/// the conversion of tokens between legacy tokens (OptimismMintableERC20 or StandardL2ERC20) +/// and SuperchainERC20 tokens. +contract L2StandardBridgeInterop is L2StandardBridge { + /// @notice Emitted when a conversion is made. + /// @param from The token being converted from. + /// @param to The token being converted to. + /// @param caller The caller of the conversion. + /// @param amount The amount of tokens being converted. + event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + + /// @notice Converts `amount` of `from` token to `to` token. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + /// @param _amount The amount of tokens being converted. + function convert(address _from, address _to, uint256 _amount) external { + _validatePair(_from, _to); + + MintableAndBurnable(_from).burn(msg.sender, _amount); + MintableAndBurnable(_to).mint(msg.sender, _amount); + + emit Converted(_from, _to, msg.sender, _amount); + } + + /// @notice Validates the pair of tokens. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + function _validatePair(address _from, address _to) internal view { + // 1. Decimals check + if (IERC20Metadata(_from).decimals() != IERC20Metadata(_to).decimals()) revert InvalidDecimals(); + + // Order tokens for factory validation + if (_isOptimismMintableERC20(_from)) { + _validateFactories(_from, _to); + } else { + _validateFactories(_to, _from); + } + } + + /// @notice Validates that the tokens are deployed by the correct factory. + /// @param _legacyAddr The legacy token address (OptimismMintableERC20 or StandardL2ERC20). + /// @param _superAddr The SuperchainERC20 address. + function _validateFactories(address _legacyAddr, address _superAddr) internal view { + // 2. Valid legacy check + address _legacyRemoteToken = + IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); + if (_legacyRemoteToken == address(0)) revert InvalidLegacyAddress(); + + // 3. Valid SuperchainERC20 check + address _superRemoteToken = + ISuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); + if (_superRemoteToken == address(0)) revert InvalidSuperchainAddress(); + + // 4. Same remote address check + if (_legacyRemoteToken != _superRemoteToken) revert InvalidTokenPair(); + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol new file mode 100644 index 00000000000..cacccde97ec --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ISuperchainERC20 } from "src/L2/ISuperchainERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { SafeCall } from "src/libraries/SafeCall.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 RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + +/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this SuperchainERC20. +error MessageSenderNotThisSuperchainERC20(); + +/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. +error CallerNotBridge(); + +/// @custom:proxied +/// @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. This construction builds on top of the L2ToL2CrossDomainMessenger +/// for both replay protection and domain binding. +contract SuperchainERC20 is ISuperchainERC20, ERC20, ISemver { + /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + /// @notice Address of the StandardBridge Predeploy. + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + + /// @notice Decimals of the token + uint8 private immutable DECIMALS; + + /// @notice Address of the corresponding version of this token on the remote chain. + address public immutable REMOTE_TOKEN; + + /// @notice Emitted whenever tokens are minted for an account. + /// @param account Address of the account tokens are being minted for. + /// @param amount Amount of tokens minted. + event Mint(address indexed account, uint256 amount); + + /// @notice Emitted whenever tokens are burned from an account. + /// @param account Address of the account tokens are being burned from. + /// @param amount Amount of tokens burned. + event Burn(address indexed account, uint256 amount); + + /// @notice Emitted whenever tokens are sent to another chain. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens sent. + /// @param chainId Chain ID of the destination chain. + event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + event RelayedERC20(address indexed to, uint256 amount); + + /// @notice A modifier that only allows the bridge to call + modifier onlyBridge() { + if (msg.sender != BRIDGE) revert CallerNotBridge(); + _; + } + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @param _remoteToken Address of the corresponding remote token. + /// @param _name ERC20 name. + /// @param _symbol ERC20 symbol. + /// @param _decimals ERC20 decimals. + constructor( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + ERC20(_name, _symbol) + { + REMOTE_TOKEN = _remoteToken; + DECIMALS = _decimals; + } + + /// @notice Allows the StandardBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint(address _to, uint256 _amount) external virtual onlyBridge { + _mint(_to, _amount); + emit Mint(_to, _amount); + } + + /// @notice Allows the StandardBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn(address _from, uint256 _amount) external virtual onlyBridge { + _burn(_from, _amount); + 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 { + _burn(msg.sender, _amount); + + bytes memory _message = abi.encodeCall(this.relayERC20, (_to, _amount)); + IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); + + emit SentERC20(msg.sender, _to, _amount, _chainId); + } + + /// @notice Relays tokens received from another chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _to, uint256 _amount) external { + if (msg.sender != MESSENGER) revert RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + + if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { + revert MessageSenderNotThisSuperchainERC20(); + } + + _mint(_to, _amount); + + emit RelayedERC20(_to, _amount); + } + + /// @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 DECIMALS; + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol new file mode 100644 index 00000000000..d8ef5b810a9 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; + +/// @title SuperchainERC20Beacon +/// @notice SuperchainERC20Beacon is the beacon proxy for the SuperchainERC20 implementation. +contract SuperchainERC20Beacon is IBeacon { + /// TODO: Replace with real implementation address + /// @notice Address of the SuperchainERC20 implementation. + address internal constant IMPLEMENTATION_ADDRESS = 0x0000000000000000000000000000000000000000; + + /// @inheritdoc IBeacon + function implementation() external pure override returns (address) { + return IMPLEMENTATION_ADDRESS; + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol new file mode 100644 index 00000000000..c102ed0654f --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ISemver } from "src/universal/ISemver.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { CREATE3 } from "@rari-capital/solmate/src/utils/CREATE3.sol"; + +/// @custom:proxied +/// @title SuperchainERC20Factory +/// @notice SuperchainERC20Factory is a factory contract that deploys SuperchainERC20 Beacon Proxies using CREATE3. +contract SuperchainERC20Factory is ISemver { + /// @notice Mapping of the deployed SuperchainERC20 to the remote token address. + mapping(address superchainToken => address remoteToken) public deployments; + + /// @notice Emitted when a SuperchainERC20 is deployed. + /// @param superchainERC20 Address of the SuperchainERC20 deployment. + /// @param remoteToken Address of the remote token. + /// @param name Name of the SuperchainERC20. + /// @param symbol Symbol of the SuperchainERC20. + /// @param decimals Decimals of the SuperchainERC20. + event SuperchainERC20Deployed( + address indexed superchainERC20, address indexed remoteToken, string name, string symbol, uint8 decimals + ); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Deploys a SuperchainERC20 Beacon Proxy using CREATE3. + /// @param _remoteToken Address of the remote token. + /// @param _name Name of the SuperchainERC20. + /// @param _symbol Symbol of the SuperchainERC20. + /// @param _decimals Decimals of the SuperchainERC20. + /// @return _superchainERC20 Address of the SuperchainERC20 deployment. + function deploy( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + returns (address _superchainERC20) + { + // Encode the BeaconProxy creation code with the beacon contract address and metadata + bytes memory _creationCode = abi.encodePacked( + type(BeaconProxy).creationCode, + abi.encode(Predeploys.SUPERCHAIN_ERC20_BEACON, abi.encode(_remoteToken, _name, _symbol, _decimals)) + ); + + // Use CREATE3 for deterministic deployment + bytes32 _salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); + _superchainERC20 = CREATE3.deploy({ salt: _salt, creationCode: _creationCode, value: 0 }); + + // Store SuperchainERC20 and remote token addresses + deployments[_superchainERC20] = _remoteToken; + + emit SuperchainERC20Deployed(_superchainERC20, _remoteToken, _name, _symbol, _decimals); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 0aece54898d..4f27be8a80a 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -95,6 +95,13 @@ library Predeploys { /// @notice Address of the ETHLiquidity predeploy. address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025; + /// @notice Address of the OptimismSuperchainERC20Factory predeploy. + address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026; + + /// TODO: Replace with real predeploy address + /// @notice Address of the SuperchainERC20Beacon predeploy. + address internal constant SUPERCHAIN_ERC20_BEACON = 0x4200000000000000000000000000000000000027; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -121,8 +128,10 @@ library Predeploys { if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox"; if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger"; + if (_addr == SUPERCHAIN_ERC20_BEACON) return "SuperchainERC20Beacon"; if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH"; if (_addr == ETH_LIQUIDITY) return "ETHLiquidity"; + if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; revert("Predeploys: unnamed predeploy"); } @@ -140,7 +149,9 @@ library Predeploys { || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) - || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY); + || (_useInterop && _addr == SUPERCHAIN_ERC20_BEACON) + || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY); } function isPredeployNamespace(address _addr) internal pure returns (bool) { 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 00000000000..e5e4f8828ae --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; + +// Target contract +import { + SuperchainERC20, + CallerNotBridge, + RelayMessageCallerNotL2ToL2CrossDomainMessenger, + MessageSenderNotThisSuperchainERC20, + CallerNotBridge +} from "src/L2/SuperchainERC20.sol"; +import { ISuperchainERC20 } from "src/L2/ISuperchainERC20.sol"; + +/// @title SuperchainERC20Test +/// @dev Contract for testing the SuperchainERC20 contract. +contract SuperchainERC20Test is Test { + address internal constant ZERO_ADDRESS = address(0); + address internal constant REMOTE_TOKEN = address(0x123); + string internal constant NAME = "SuperchainERC20"; + string internal constant SYMBOL = "SCE"; + uint8 internal constant DECIMALS = 18; + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + SuperchainERC20 public superchainERC20; + + /// @dev Sets up the test suite. + function setUp() public { + superchainERC20 = new SuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + } + + /// @dev 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); + } + + /// @dev Test that the bridge's constructor sets the correct values. + function test_constructor_succeeds() public view { + assertEq(superchainERC20.name(), NAME); + assertEq(superchainERC20.symbol(), SYMBOL); + assertEq(superchainERC20.decimals(), DECIMALS); + assertEq(superchainERC20.REMOTE_TOKEN(), REMOTE_TOKEN); + } + + /// @dev Tests the `mint` function reverts when the caller is not the bridge. + function testFuzz_mint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `mint` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.mint(_to, _amount); + } + + /// @dev Tests the `mint` function reverts when the amount is zero. + function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: mint to the zero address" + vm.expectRevert("ERC20: mint to the zero address"); + + // Call the `mint` function with the zero address + vm.prank(BRIDGE); + superchainERC20.mint({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `mint` succeeds and emits the `Mint` event. + function testFuzz_mint_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + + // Get the total supply and balance of `_to` before the mint 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(true, true, true, true, 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)); + emit SuperchainERC20.Mint(_to, _amount); + + // Call the `mint` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.mint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + } + + /// @dev Tests the `burn` function reverts when the caller is not the bridge. + function testFuzz_burn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `burn` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.burn(_from, _amount); + } + + /// @dev Tests the `burn` function reverts when the amount is zero. + function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: burn from the zero address" + vm.expectRevert("ERC20: burn from the zero address"); + + // Call the `burn` function with the zero address + vm.prank(BRIDGE); + superchainERC20.burn({ _from: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `burn` burns the amount and emits the `Burn` event. + function testFuzz_burn_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); + + // Mint some tokens to `_from` so then they can be burned + vm.prank(BRIDGE); + superchainERC20.mint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, 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)); + emit SuperchainERC20.Burn(_from, _amount); + + // Call the `burn` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.burn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); + } + + /// @dev Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SentERC20` 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); + + // Mint some tokens to the sender so then they can be sent + vm.prank(BRIDGE); + 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(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); + + // Look for the emit of the `SentERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.SentERC20(_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, (_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); + } + + /// @dev 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); + + // Expect the revert with `RelayMessageCallerNotL2ToL2CrossDomainMessenger` selector + vm.expectRevert(RelayMessageCallerNotL2ToL2CrossDomainMessenger.selector); + + // Call the `relayERC20` function with the non-messenger caller + vm.prank(_caller); + superchainERC20.relayERC20(_to, _amount); + } + + /// @dev 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(_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 `MessageSenderNotThisSuperchainERC20` selector + vm.expectRevert(MessageSenderNotThisSuperchainERC20.selector); + + // Call the `relayERC20` function with the sender caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_to, _amount); + } + + /// @dev Tests the `relayERC20` function reverts when the `_to` address is the zero address. + function testFuzz_relayERC20_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: mint to the zero address" + vm.expectRevert("ERC20: mint to the zero address"); + + // 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({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `relayERC20` mints the proper amount and emits the `RelayedERC20` event. + function testFuzz_relayERC20_succeeds(address _to, uint256 _amount) public { + 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)) + ); + + // 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(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayedERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.RelayedERC20(_to, _amount); + + // Call the `relayERC20` function with the messenger caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_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); + } + + /// @dev Tests the `decimals` function always returns the correct value. + function testFuzz_decimals_succeeds(uint8 _decimals) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, _decimals); + assertEq(_newSuperchainERC20.decimals(), _decimals); + } + + /// @dev Tests the `REMOTE_TOKEN` function always returns the correct value. + function testFuzz_remoteToken_succeeds(address _remoteToken) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20(_remoteToken, NAME, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.REMOTE_TOKEN(), _remoteToken); + } +}