From 2b08a9bcbf9ecbb4d6eb884617e0d084ccbe664b 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/6] 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 --- .../abi/OptimismSuperchainERC20.json | 5 + .../snapshots/abi/SuperchainERC20.json | 478 ++++++++++++++++++ .../storageLayout/SuperchainERC20.json | 1 + .../src/L2/OptimismSuperchainERC20.sol | 95 +--- .../src/L2/SuperchainERC20.sol | 123 +++++ .../test/L2/OptimismSuperchainERC20.t.sol | 18 +- .../test/L2/SuperchainERC20.t.sol | 231 +++++++++ .../test/vendor/InitializableOZv5.t.sol | 1 + 8 files changed, 862 insertions(+), 90 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/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 731657e900fc..05d6c214dcba 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -6,17 +6,11 @@ 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 { 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 +25,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 +45,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 +65,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS string public constant version = "1.0.0-beta.2"; /// @notice Constructs the OptimismSuperchainERC20 contract. - constructor() { + constructor() SuperchainERC20("", "", 18) { _disableInitializers(); } @@ -92,11 +83,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 +111,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 0fc7d88d0b7c..868f46f0c6b7 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -19,8 +19,6 @@ import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy import { OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, - CallerNotL2ToL2CrossDomainMessenger, - InvalidCrossDomainSender, OnlyBridge, ZeroAddress } from "src/L2/OptimismSuperchainERC20.sol"; @@ -152,11 +150,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 @@ -205,11 +203,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 @@ -247,11 +245,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 @@ -355,11 +353,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 b8b4a4302dbf9d927f8fac3792f1106ca69164c7 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 29 Aug 2024 18:15:05 -0300 Subject: [PATCH 2/6] 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 From d87a38ebfff0d8e243dce84d9d83a7eae37b7c84 Mon Sep 17 00:00:00 2001 From: 0xng Date: Thu, 12 Sep 2024 15:04:07 -0300 Subject: [PATCH 3/6] feat: making baseline abstract --- packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismSuperchainERC20.json | 5 - .../snapshots/abi/SuperchainERC20.json | 478 ------------------ .../storageLayout/SuperchainERC20.json | 1 - .../src/L2/OptimismSuperchainERC20.sol | 48 +- .../src/L2/SuperchainERC20.sol | 74 +-- .../src/libraries/errors/CommonErrors.sol | 3 + .../test/L2/OptimismSuperchainERC20.t.sol | 30 +- .../test/L2/SuperchainERC20.t.sol | 67 ++- 9 files changed, 90 insertions(+), 620 deletions(-) delete mode 100644 packages/contracts-bedrock/snapshots/abi/SuperchainERC20.json delete mode 100644 packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 01a17e96adb2..983384f8be96 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": "0x4fd71b5352b78d51d39625b6defa77a75be53067b32f3cba86bd17a46917adf9", + "sourceCodeHash": "0xa684393658f8e0c5cc25fb476cb49b92a366714ea8643db495116a55e068ff68" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 754a2ff135a9..6eb57764a8cb 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -629,11 +629,6 @@ "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 deleted file mode 100644 index 5f54dce299b5..000000000000 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainERC20.json +++ /dev/null @@ -1,478 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 0637a088a01e..000000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainERC20.json +++ /dev/null @@ -1 +0,0 @@ -[] \ 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 05d6c214dcba..08f06b34a104 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -2,21 +2,26 @@ pragma solidity 0.8.25; import { IOptimismSuperchainERC20Extension } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; -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 { ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; +import { ERC20 } from "@solady/tokens/ERC20.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(); -/// @notice Thrown when attempting to mint or burn tokens and the account is the zero address. -error ZeroAddress(); - /// @custom:proxied true /// @title OptimismSuperchainERC20 /// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token @@ -45,6 +50,12 @@ contract OptimismSuperchainERC20 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. @@ -65,7 +76,7 @@ contract OptimismSuperchainERC20 is string public constant version = "1.0.0-beta.2"; /// @notice Constructs the OptimismSuperchainERC20 contract. - constructor() SuperchainERC20("", "", 18) { + constructor() { _disableInitializers(); } @@ -83,10 +94,11 @@ contract OptimismSuperchainERC20 is external initializer { - _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. @@ -116,6 +128,26 @@ contract OptimismSuperchainERC20 is return _getStorage().remoteToken; } + /// @notice Returns the name of the token. + function name() public view virtual override returns (string memory) { + return _getStorage().name; + } + + /// @notice Returns the symbol of the token. + function symbol() public view virtual override returns (string memory) { + return _getStorage().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 _getStorage().decimals; + } + /// @notice ERC165 interface check function. /// @param _interfaceId Interface ID to check. /// @return Whether or not the interface is supported by this contract. diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 864be0c1e890..3e8d7f0b913a 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { ISuperchainERC20Extensions } from "src/L2/ISuperchainERC20.sol"; +import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; import { ERC20 } from "@solady/tokens/ERC20.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not /// L2ToL2CrossDomainMessenger. @@ -13,59 +14,14 @@ 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 { +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 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. @@ -86,8 +42,6 @@ contract SuperchainERC20 is ISuperchainERC20Extensions, ERC20 { /// @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)) { @@ -100,24 +54,4 @@ contract SuperchainERC20 is ISuperchainERC20Extensions, ERC20 { 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/src/libraries/errors/CommonErrors.sol b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol index eee6cc699489..30ce96972a19 100644 --- a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol +++ b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol @@ -12,3 +12,6 @@ error NotCustomGasToken(); /// @notice Error for when a transfer via call fails. error TransferFailed(); + +/// @notice Thrown when attempting to perform an operation and the account is the zero address. +error ZeroAddress(); diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 868f46f0c6b7..58e3ab056cc8 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -17,11 +17,16 @@ import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy // Target contract import { - OptimismSuperchainERC20, - IOptimismSuperchainERC20Extension, - OnlyBridge, - ZeroAddress + OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, OnlyBridge } from "src/L2/OptimismSuperchainERC20.sol"; + +// SuperchainERC20 Errors +import { + ZeroAddress, + CallerNotL2ToL2CrossDomainMessenger, + InvalidCrossDomainSender +} from "src/L2/OptimismSuperchainERC20.sol"; + import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test @@ -312,23 +317,6 @@ contract OptimismSuperchainERC20Test is Test { 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); diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 143a0d2001a6..34ebaf889aba 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -7,7 +7,7 @@ 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"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; // Target contract import { @@ -20,27 +20,41 @@ import { /// @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) - { } + string private _name; + string private _symbol; + uint8 private _decimals; + + constructor(string memory __name, string memory __symbol, uint8 __decimals) { + _name = __name; + _symbol = __symbol; + _decimals = __decimals; + } function mint(address _account, uint256 _amount) public { _mint(_account, _amount); } -} + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} /// @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; + address internal ZERO_ADDRESS; SuperchainERC20 public superchainERC20Impl; SuperchainERC20Mock public superchainERC20; @@ -154,23 +168,6 @@ contract SuperchainERC20Test is Test { 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); @@ -211,21 +208,21 @@ contract SuperchainERC20Test is Test { 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); + 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 SuperchainERC20(NAME, _symbol, DECIMALS); + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, _symbol, DECIMALS); assertEq(_newSuperchainERC20.symbol(), _symbol); } + + /// @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); + } } From 9bde150af7dfbc445ac3e149f716a96f6f010d5c Mon Sep 17 00:00:00 2001 From: 0xng Date: Fri, 13 Sep 2024 16:37:40 -0300 Subject: [PATCH 4/6] fix: interfaces to comply with the new interface checker --- packages/contracts-bedrock/semver-lock.json | 2 +- .../src/L2/OptimismSuperchainERC20.sol | 9 -- .../src/L2/SuperchainERC20.sol | 12 +-- .../interfaces/IOptimismSuperchainERC20.sol | 10 +- .../src/L2/interfaces/ISuperchainERC20.sol | 21 +++- .../dependency/interfaces/IERC20Solady.sol | 95 +++++++++++++++++++ .../src/libraries/errors/CommonErrors.sol | 3 - .../test/L2/OptimismSuperchainERC20.t.sol | 20 ++-- .../test/L2/SuperchainERC20.t.sol | 17 ++-- 9 files changed, 134 insertions(+), 55 deletions(-) create mode 100644 packages/contracts-bedrock/src/dependency/interfaces/IERC20Solady.sol diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 983384f8be96..b81823154f32 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -117,7 +117,7 @@ }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0x4fd71b5352b78d51d39625b6defa77a75be53067b32f3cba86bd17a46917adf9", - "sourceCodeHash": "0xa684393658f8e0c5cc25fb476cb49b92a366714ea8643db495116a55e068ff68" + "sourceCodeHash": "0xad3934ea533544b3c130c80be26201354af85f9166cb2ce54d96e5e383ebb5c1" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 08f06b34a104..81cef632bfbe 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -5,20 +5,11 @@ import { IOptimismSuperchainERC20Extension } from "src/L2/interfaces/IOptimismSu import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { ERC20 } from "@solady/tokens/ERC20.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(); diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 3e8d7f0b913a..e20b375ff891 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -1,24 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { ISuperchainERC20Extensions, ISuperchainERC20Errors } 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"; -import { ZeroAddress } from "src/libraries/errors/CommonErrors.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(); /// @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 { +abstract contract SuperchainERC20 is ISuperchainERC20Extensions, ISuperchainERC20Errors, ERC20 { /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; diff --git a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol index 9b9594e75d78..f060d7ecd92d 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISuperchainERC20Extensions } from "./ISuperchainERC20.sol"; +import { IERC20Solady } from "../../dependency/interfaces/IERC20Solady.sol"; +import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "./ISuperchainERC20.sol"; /// @title IOptimismSuperchainERC20Extension /// @notice This interface is available on the OptimismSuperchainERC20 contract. /// We declare it as a separate interface so that it can be used in /// custom implementations of SuperchainERC20. -interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions { +interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions, ISuperchainERC20Errors { /// @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. @@ -34,5 +34,5 @@ interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions { } /// @title IOptimismSuperchainERC20 -/// @notice Combines the ERC20 interface with the OptimismSuperchainERC20Extension interface. -interface IOptimismSuperchainERC20 is IERC20, IOptimismSuperchainERC20Extension { } +/// @notice Combines Solady's ERC20 interface with the OptimismSuperchainERC20Extension interface. +interface IOptimismSuperchainERC20 is IERC20Solady, IOptimismSuperchainERC20Extension { } diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol index 76488cdf32ea..251446d8b0b5 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Solady } from "../../dependency/interfaces/IERC20Solady.sol"; /// @title ISuperchainERC20Extensions /// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20. @@ -35,6 +35,21 @@ interface ISuperchainERC20Extensions { function relayERC20(address _from, address _to, uint256 _amount) external; } +/// @title ISuperchainERC20Errors +/// @notice Interface containing the errors added in the SuperchainERC20 implementation. +interface ISuperchainERC20Errors { + /// @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 perform an operation and the account is the zero address. + error ZeroAddress(); +} + /// @title ISuperchainERC20 -/// @notice Combines the ERC20 interface with the SuperchainERC20Extensions interface. -interface ISuperchainERC20 is IERC20, ISuperchainERC20Extensions { } +/// @notice Combines Solady's ERC20 interface with the SuperchainERC20Extensions interface. +interface ISuperchainERC20 is IERC20Solady, ISuperchainERC20Extensions, ISuperchainERC20Errors { } diff --git a/packages/contracts-bedrock/src/dependency/interfaces/IERC20Solady.sol b/packages/contracts-bedrock/src/dependency/interfaces/IERC20Solady.sol new file mode 100644 index 000000000000..1e696ad23ac3 --- /dev/null +++ b/packages/contracts-bedrock/src/dependency/interfaces/IERC20Solady.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20Solady { + /// @dev The total supply has overflowed. + error TotalSupplyOverflow(); + + /// @dev The allowance has overflowed. + error AllowanceOverflow(); + + /// @dev The allowance has underflowed. + error AllowanceUnderflow(); + + /// @dev Insufficient balance. + error InsufficientBalance(); + + /// @dev Insufficient allowance. + error InsufficientAllowance(); + + /// @dev The permit is invalid. + error InvalidPermit(); + + /// @dev The permit has expired. + error PermitExpired(); + + /// @dev Emitted when `amount` tokens is transferred from `from` to `to`. + event Transfer(address indexed from, address indexed to, uint256 amount); + + /// @dev Emitted when `amount` tokens is approved by `owner` to be used by `spender`. + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /// @dev Returns the name of the token. + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + function decimals() external view returns (uint8); + + /// @dev Returns the amount of tokens in existence. + function totalSupply() external view returns (uint256 result); + + /// @dev Returns the amount of tokens owned by `owner` + function balanceOf(address owner) external view returns (uint256 result); + + /// @dev Returns the amount of tokens that `spender` can spend on behalf of `owner`. + function allowance(address owner, address spender) external view returns (uint256 result); + + /// @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + /// + /// Emits a {Approval} event. + function approve(address spender, uint256 amount) external returns (bool); + + /// @dev Transfer `amount` tokens from the caller to `to`. + /// + /// Requirements: + /// - `from` must at least have `amount`. + /// + /// Emits a {Transfer} event. + function transfer(address to, uint256 amount) external returns (bool); + + /// @dev Transfers `amount` tokens from `from` to `to`. + /// + /// Note: Does not update the allowance if it is the maximum uint256 value. + /// + /// Requirements: + /// - `from` must at least have `amount`. + /// - The caller must have at least `amount` of allowance to transfer the tokens of `from`. + /// + /// Emits a {Transfer} event. + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + /// @dev Returns the current nonce for `owner`. + /// This value is used to compute the signature for EIP-2612 permit. + function nonces(address owner) external view returns (uint256 result); + + /// @dev Sets `value` as the allowance of `spender` over the tokens of `owner`, + /// authorized by a signed approval by `owner`. + /// + /// Emits a {Approval} event. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external; + + /// @dev Returns the EIP-712 domain separator for the EIP-2612 permit. + function DOMAIN_SEPARATOR() external view returns (bytes32 result); +} diff --git a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol index 30ce96972a19..eee6cc699489 100644 --- a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol +++ b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol @@ -12,6 +12,3 @@ error NotCustomGasToken(); /// @notice Error for when a transfer via call fails. error TransferFailed(); - -/// @notice Thrown when attempting to perform an operation and the account is the zero address. -error ZeroAddress(); diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 58e3ab056cc8..b8de1468421b 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -20,14 +20,8 @@ import { OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, OnlyBridge } from "src/L2/OptimismSuperchainERC20.sol"; -// SuperchainERC20 Errors -import { - ZeroAddress, - CallerNotL2ToL2CrossDomainMessenger, - InvalidCrossDomainSender -} from "src/L2/OptimismSuperchainERC20.sol"; - -import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; +// SuperchainERC20 Interfaces +import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test /// @notice Contract for testing the OptimismSuperchainERC20 contract. @@ -138,7 +132,7 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Tests the `mint` function reverts when the amount is zero. function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ZeroAddress.selector); + vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); // Call the `mint` function with the zero address vm.prank(BRIDGE); @@ -187,7 +181,7 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Tests the `burn` function reverts when the amount is zero. function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ZeroAddress.selector); + vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); // Call the `burn` function with the zero address vm.prank(BRIDGE); @@ -227,7 +221,7 @@ contract OptimismSuperchainERC20Test is Test { /// @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); + vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); // Call the `sendERC20` function with the zero address vm.prank(BRIDGE); @@ -283,7 +277,7 @@ contract OptimismSuperchainERC20Test is Test { vm.assume(_to != ZERO_ADDRESS); // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector - vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector); + vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector); // Call the `relayERC20` function with the non-messenger caller vm.prank(_caller); @@ -310,7 +304,7 @@ contract OptimismSuperchainERC20Test is Test { ); // Expect the revert with `InvalidCrossDomainSender` selector - vm.expectRevert(InvalidCrossDomainSender.selector); + vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector); // Call the `relayERC20` function with the sender caller vm.prank(MESSENGER); diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 34ebaf889aba..66b20c1b911a 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -10,13 +10,8 @@ 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"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; +import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; /// @notice Mock contract for the SuperchainERC20 contract so tests can mint tokens. contract SuperchainERC20Mock is SuperchainERC20 { @@ -50,11 +45,11 @@ contract SuperchainERC20Mock is SuperchainERC20 { /// @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; - address internal ZERO_ADDRESS; SuperchainERC20 public superchainERC20Impl; SuperchainERC20Mock public superchainERC20; @@ -80,7 +75,7 @@ contract SuperchainERC20Test is Test { /// @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); + vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); // Call the `sendERC20` function with the zero address superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); @@ -134,7 +129,7 @@ contract SuperchainERC20Test is Test { vm.assume(_to != ZERO_ADDRESS); // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector - vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector); + vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector); // Call the `relayERC20` function with the non-messenger caller vm.prank(_caller); @@ -161,7 +156,7 @@ contract SuperchainERC20Test is Test { ); // Expect the revert with `InvalidCrossDomainSender` selector - vm.expectRevert(InvalidCrossDomainSender.selector); + vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector); // Call the `relayERC20` function with the sender caller vm.prank(MESSENGER); From e5efecdcc38a426dcfd2efaed4fc1961ece402cb Mon Sep 17 00:00:00 2001 From: 0xng Date: Fri, 13 Sep 2024 16:50:13 -0300 Subject: [PATCH 5/6] fix: import paths and empty line --- .../src/L2/interfaces/IOptimismSuperchainERC20.sol | 2 +- .../contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol | 2 +- packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol index f060d7ecd92d..91a51e0fb543 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IERC20Solady } from "../../dependency/interfaces/IERC20Solady.sol"; +import { IERC20Solady } from "src/dependency/interfaces/IERC20Solady.sol"; import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "./ISuperchainERC20.sol"; /// @title IOptimismSuperchainERC20Extension diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol index 251446d8b0b5..78f7ae7fc1d7 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IERC20Solady } from "../../dependency/interfaces/IERC20Solady.sol"; +import { IERC20Solady } from "src/dependency/interfaces/IERC20Solady.sol"; /// @title ISuperchainERC20Extensions /// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20. diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 66b20c1b911a..fb4580eb213a 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -43,7 +43,6 @@ contract SuperchainERC20Mock is SuperchainERC20 { } /// @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"; From 9ae653b239aca5e2544a7c284a175c9b02768c14 Mon Sep 17 00:00:00 2001 From: 0xng Date: Fri, 13 Sep 2024 20:38:19 -0300 Subject: [PATCH 6/6] fix: lint line --- packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index fb4580eb213a..66b20c1b911a 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -43,6 +43,7 @@ contract SuperchainERC20Mock is SuperchainERC20 { } /// @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";