From 0d0245b7d71c3e8d89d1f78356dc104aa6233f10 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:07:38 -0300 Subject: [PATCH 1/2] feat: add superchain erc20 baseline (#37) * feat: add superchain erc20 baseline * feat: make superchain ERC20 simpler * fix: small version fix and tests * test: fix test name --- packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismSuperchainERC20.json | 5 + .../snapshots/abi/SuperchainERC20.json | 478 ++++++++++++++++++ .../storageLayout/SuperchainERC20.json | 1 + .../src/L2/OptimismSuperchainERC20.sol | 98 +--- .../src/L2/SuperchainERC20.sol | 123 +++++ .../test/L2/OptimismSuperchainERC20.t.sol | 19 +- .../test/L2/SuperchainERC20.t.sol | 231 +++++++++ .../test/vendor/InitializableOZv5.t.sol | 1 + 9 files changed, 865 insertions(+), 95 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/SuperchainERC20.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json create mode 100644 packages/contracts-bedrock/src/L2/SuperchainERC20.sol create mode 100644 packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index c9fe50d36c6e..bbb0357ef90b 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -116,8 +116,8 @@ "sourceCodeHash": "0x972564d2e2fab111cd431f3c78d00c7b0f0ae422fa5e9a8bf5b202cdaef89bf9" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xd49214518ea1a30a43fac09f28b2cee9be570894a500cef342762c9820a070b0", - "sourceCodeHash": "0x6943d40010dcbd1d51dc3668d0a154fbb1568ea49ebcf3aa039d65ef6eab321b" + "initCodeHash": "0x782aaff652a602a7cefda3102b37f724a45d009933a70a6ee235d14909d74ab1", + "sourceCodeHash": "0x9282c807007dc670cd10a32cb46c6d913b7635748693eddfaba496ebbcf5b7a5" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", 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/snapshots/abi/SuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/SuperchainERC20.json new file mode 100644 index 000000000000..5f54dce299b5 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainERC20.json @@ -0,0 +1,478 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "result", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "relayERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "sendERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "source", + "type": "uint256" + } + ], + "name": "RelayERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destination", + "type": "uint256" + } + ], + "name": "SendERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "AllowanceOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "AllowanceUnderflow", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotL2ToL2CrossDomainMessenger", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientAllowance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCrossDomainSender", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPermit", + "type": "error" + }, + { + "inputs": [], + "name": "PermitExpired", + "type": "error" + }, + { + "inputs": [], + "name": "TotalSupplyOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 9b0ba5cad8b0..580029505e94 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -2,21 +2,12 @@ pragma solidity 0.8.25; import { IOptimismSuperchainERC20Extension } from "src/L2/IOptimismSuperchainERC20.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; -import { ISemver } from "src/universal/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; +import { ISemver } from "src/universal/ISemver.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 +22,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 +42,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 } @@ -74,7 +62,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS string public constant version = "1.0.0-beta.1"; /// @notice Constructs the OptimismSuperchainERC20 contract. - constructor() { + constructor() SuperchainERC20("", "", 18) { _disableInitializers(); } @@ -92,11 +80,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 +108,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..864be0c1e890 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ISuperchainERC20Extensions } from "src/L2/ISuperchainERC20.sol"; +import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/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. +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 Constructs the SuperchainERC20 contract. + /// @param _name ERC20 name. + /// @param _symbol ERC20 symbol. + /// @param _decimals ERC20 decimals. + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + _setMetadataStorage(_name, _symbol, _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 84580fdd8687..b12d4f725f3c 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -16,11 +16,10 @@ import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165. import { OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, - CallerNotL2ToL2CrossDomainMessenger, - InvalidCrossDomainSender, OnlyBridge, ZeroAddress } from "src/L2/OptimismSuperchainERC20.sol"; +import { CallerNotL2ToL2CrossDomainMessenger, InvalidCrossDomainSender } from "src/L2/SuperchainERC20.sol"; import { ISuperchainERC20Extensions } from "src/L2/ISuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test @@ -127,11 +126,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 +179,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 +221,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 +329,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..143a0d2001a6 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -0,0 +1,231 @@ +// 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/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 + ) + SuperchainERC20(_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 SuperchainERC20(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 SuperchainERC20(_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 SuperchainERC20(NAME, _symbol, DECIMALS); + assertEq(_newSuperchainERC20.symbol(), _symbol); + } +} diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index 0820f987414a..93f8c6693866 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.25; import { Test } from "forge-std/Test.sol"; import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; /// @title InitializerOZv5_Test From c6b41288cad8f877d201279e1d2f4782b4ad1364 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 29 Aug 2024 18:15:05 -0300 Subject: [PATCH 2/2] test: remove unused import --- packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index 93f8c6693866..0820f987414a 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.25; import { Test } from "forge-std/Test.sol"; import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; -import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; /// @title InitializerOZv5_Test