From 64dc6d40e659673cb5c6ef3e6e564c74f7dacd5f Mon Sep 17 00:00:00 2001 From: agusduha Date: Mon, 5 Aug 2024 11:32:25 -0300 Subject: [PATCH 01/14] feat: add L2 standrad bridge interop contract --- .../src/L2/L2StandardBridgeInterop.sol | 96 +++++++++++++++++++ .../src/libraries/Predeploys.sol | 7 +- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol new file mode 100644 index 000000000000..ad4a378a5fda --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { L2StandardBridge } from "./L2StandardBridge.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/// @notice Thrown when the decimals of the tokens are not the same. +error InvalidDecimals(); + +/// @notice Thrown when the legacy address is not found in the OptimismMintableERC20Factory. +error InvalidLegacyAddress(); + +/// @notice Thrown when the SuperchainERC20 address is not found in the SuperchainERC20Factory. +error InvalidSuperchainAddress(); + +/// @notice Thrown when the remote addresses of the tokens are not the same. +error InvalidTokenPair(); + +// TODO: Use OptimismMintableERC20Factory contract instead of interface +interface IOptimismMintableERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Move to a separate file +interface ISuperchainERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Use an existing interface with `mint` and `burn`? +interface MintableAndBurnable is IERC20 { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +/// @custom:proxied +/// @custom:predeploy 0x4200000000000000000000000000000000000010 +/// @title L2StandardBridgeInterop +/// @notice The L2StandardBridgeInterop is an extension of the L2StandardBridge that allows for +/// the conversion of tokens between legacy tokens (OptimismMintableERC20 or StandardL2ERC20) +/// and SuperchainERC20 tokens. +contract L2StandardBridgeInterop is L2StandardBridge { + /// @notice Emitted when a conversion is made. + /// @param from The token being converted from. + /// @param to The token being converted to. + /// @param caller The caller of the conversion. + /// @param amount The amount of tokens being converted. + event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + + /// @notice Converts `amount` of `from` token to `to` token. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + /// @param _amount The amount of tokens being converted. + function convert(address _from, address _to, uint256 _amount) external { + _validatePair(_from, _to); + + MintableAndBurnable(_from).burn(msg.sender, _amount); + MintableAndBurnable(_to).mint(msg.sender, _amount); + + emit Converted(_from, _to, msg.sender, _amount); + } + + /// @notice Validates the pair of tokens. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + function _validatePair(address _from, address _to) internal view { + // 1. Decimals check + if (IERC20Metadata(_from).decimals() != IERC20Metadata(_to).decimals()) revert InvalidDecimals(); + + // Order tokens for factory validation + if (_isOptimismMintableERC20(_from)) { + _validateFactories(_from, _to); + } else { + _validateFactories(_to, _from); + } + } + + /// @notice Validates that the tokens are deployed by the correct factory. + /// @param _legacyAddr The legacy token address (OptimismMintableERC20 or StandardL2ERC20). + /// @param _superAddr The SuperchainERC20 address. + function _validateFactories(address _legacyAddr, address _superAddr) internal view { + // 2. Valid legacy check + address _legacyRemoteToken = + IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); + if (_legacyRemoteToken == address(0)) revert InvalidLegacyAddress(); + + // 3. Valid SuperchainERC20 check + address _superRemoteToken = + ISuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); + if (_superRemoteToken == address(0)) revert InvalidSuperchainAddress(); + + // 4. Same remote address check + if (_legacyRemoteToken != _superRemoteToken) revert InvalidTokenPair(); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 0aece54898d3..65d019a45711 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -95,6 +95,9 @@ library Predeploys { /// @notice Address of the ETHLiquidity predeploy. address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025; + /// @notice Address of the OptimismSuperchainERC20Factory predeploy. + address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -123,6 +126,7 @@ library Predeploys { if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger"; if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH"; if (_addr == ETH_LIQUIDITY) return "ETHLiquidity"; + if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; revert("Predeploys: unnamed predeploy"); } @@ -140,7 +144,8 @@ library Predeploys { || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) - || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY); + || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY); } function isPredeployNamespace(address _addr) internal pure returns (bool) { From de677a277c1e8967172d85f3b027fca9503c472f Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:23:50 -0300 Subject: [PATCH 02/14] test: add L2 standard bridge interop unit tests (#13) * test: add L2 standard bridge interop unit tests * fix: add tests natspec * fix: unit tests fixes * fix: super to legacy tests failing * fix: mock and expect mint and burn --- .../contracts-bedrock/scripts/Artifacts.s.sol | 2 + .../abi/L2StandardBridgeInterop.json | 694 ++++++++++++++++++ .../L2StandardBridgeInterop.json | 58 ++ .../src/L2/L2StandardBridgeInterop.sol | 2 +- .../src/libraries/Predeploys.sol | 2 +- .../test/L2/L2StandardBridgeInterop.t.sol | 357 +++++++++ .../contracts-bedrock/test/L2Genesis.t.sol | 4 +- .../contracts-bedrock/test/setup/Setup.sol | 4 +- .../test/vendor/Initializable.t.sol | 8 + 9 files changed, 1125 insertions(+), 6 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/L2StandardBridgeInterop.json create mode 100644 packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 4a788608788a..75ccb70379c6 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -114,6 +114,8 @@ abstract contract Artifacts { return payable(Predeploys.L2_TO_L1_MESSAGE_PASSER); } else if (digest == keccak256(bytes("L2StandardBridge"))) { return payable(Predeploys.L2_STANDARD_BRIDGE); + } else if (digest == keccak256(bytes("L2StandardBridgeInterop"))) { + return payable(Predeploys.L2_STANDARD_BRIDGE); } else if (digest == keccak256(bytes("L2ERC721Bridge"))) { return payable(Predeploys.L2_ERC721_BRIDGE); } else if (digest == keccak256(bytes("SequencerFeeWallet"))) { diff --git a/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json b/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json new file mode 100644 index 000000000000..1a4cae148836 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json @@ -0,0 +1,694 @@ +[ + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "MESSENGER", + "outputs": [ + { + "internalType": "contract CrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OTHER_BRIDGE", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "convert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract StandardBridge", + "name": "_otherBridge", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l1TokenBridge", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "contract CrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "otherBridge", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "withdrawTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Converted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "DepositFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeInitiated", + "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": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeFinalized", + "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": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "WithdrawalInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "InvalidDecimals", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidLegacyAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSuperchainAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTokenPair", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L2StandardBridgeInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/L2StandardBridgeInterop.json new file mode 100644 index 000000000000..f5effc6ae799 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/L2StandardBridgeInterop.json @@ -0,0 +1,58 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "30", + "label": "spacer_0_2_30", + "offset": 2, + "slot": "0", + "type": "bytes30" + }, + { + "bytes": "20", + "label": "spacer_1_0_20", + "offset": 0, + "slot": "1", + "type": "address" + }, + { + "bytes": "32", + "label": "deposits", + "offset": 0, + "slot": "2", + "type": "mapping(address => mapping(address => uint256))" + }, + { + "bytes": "20", + "label": "messenger", + "offset": 0, + "slot": "3", + "type": "contract CrossDomainMessenger" + }, + { + "bytes": "20", + "label": "otherBridge", + "offset": 0, + "slot": "4", + "type": "contract StandardBridge" + }, + { + "bytes": "1440", + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "uint256[45]" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index ad4a378a5fda..13059150e366 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { L2StandardBridge } from "./L2StandardBridge.sol"; +import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 65d019a45711..53d5955e3688 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -107,7 +107,7 @@ library Predeploys { if (_addr == WETH) return "WETH"; if (_addr == L2_CROSS_DOMAIN_MESSENGER) return "L2CrossDomainMessenger"; if (_addr == GAS_PRICE_ORACLE) return "GasPriceOracle"; - if (_addr == L2_STANDARD_BRIDGE) return "L2StandardBridge"; + if (_addr == L2_STANDARD_BRIDGE) return "L2StandardBridgeInterop"; if (_addr == SEQUENCER_FEE_WALLET) return "SequencerFeeVault"; if (_addr == OPTIMISM_MINTABLE_ERC20_FACTORY) return "OptimismMintableERC20Factory"; if (_addr == L1_BLOCK_NUMBER) return "L1BlockNumber"; diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol new file mode 100644 index 000000000000..a9966f12d55e --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Target contract is imported by the `Bridge_Initializer` +import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; +import { console2 } from "forge-std/console2.sol"; + +// Target contract dependencies +import { + L2StandardBridgeInterop, + InvalidDecimals, + InvalidLegacyAddress, + InvalidSuperchainAddress, + InvalidTokenPair, + IOptimismMintableERC20Factory, + MintableAndBurnable +} from "src/L2/L2StandardBridgeInterop.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { IOptimismMintableERC20 } from "src/universal/IOptimismMintableERC20.sol"; + +// TODO: Replace Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY with optimismSuperchainERC20Factory +import { Predeploys } from "src/libraries/Predeploys.sol"; + +contract L2StandardBridgeInterop_Test is Bridge_Initializer { + /// @notice Emitted when a conversion is made. + event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + + /// @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 Mock ERC20 decimals + function _mockDecimals(address _token, uint8 _decimals) internal { + _mockAndExpect(_token, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimals)); + } + + /// @notice Mock ERC165 interface + function _mockInterface(address _token, bytes4 _interfaceId, bool _supported) internal { + _mockAndExpect( + _token, abi.encodeWithSelector(IERC165.supportsInterface.selector, _interfaceId), abi.encode(_supported) + ); + } + + /// @notice Mock factory deployment + function _mockDeployments(address _factory, address _token, address _deployed) internal { + _mockAndExpect( + _factory, + abi.encodeWithSelector(IOptimismMintableERC20Factory.deployments.selector, _token), + abi.encode(_deployed) + ); + } +} + +/// @notice Test suite when converting from a legacy token to a SuperchainERC20 token +contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_Test { + /// @notice Set up the test for converting from a legacy token to a SuperchainERC20 token + function _setUpLegacyToSuper(address _from, address _to) internal { + // Assume + vm.assume(_from != console2.CONSOLE_ADDRESS); + vm.assume(_to != console2.CONSOLE_ADDRESS); + + // Mock same decimals + _mockDecimals(_from, 18); + _mockDecimals(_to, 18); + + // Mock `_from` to be a legacy address + _mockInterface(_from, type(IERC165).interfaceId, true); + _mockInterface(_from, type(IOptimismMintableERC20).interfaceId, true); + } + + /// @notice Test that the `convert` function with different decimals reverts + function testFuzz_convert_differentDecimals_reverts( + address _from, + uint8 _decimalsFrom, + address _to, + uint8 _decimalsTo, + uint256 _amount + ) + public + { + // Assume + vm.assume(_from != console2.CONSOLE_ADDRESS); + vm.assume(_to != console2.CONSOLE_ADDRESS); + vm.assume(_decimalsFrom != _decimalsTo); + vm.assume(_from != _to); + + // Arrange + // Mock the tokens to have different decimals + _mockDecimals(_from, _decimalsFrom); + _mockDecimals(_to, _decimalsTo); + + // Expect the revert with `InvalidDecimals` selector + vm.expectRevert(InvalidDecimals.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with an invalid legacy address reverts + function testFuzz_convert_invalidLegacyAddress_reverts(address _from, address _to, uint256 _amount) public { + // Arrange + _setUpLegacyToSuper(_from, _to); + + // Mock the legacy factory to return address(0) + _mockDeployments(address(l2OptimismMintableERC20Factory), _from, address(0)); + + // Expect the revert with `InvalidLegacyAddress` selector + vm.expectRevert(InvalidLegacyAddress.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with an invalid superchain address reverts + function testFuzz_convert_invalidSuperchainAddress_reverts( + address _from, + address _to, + uint256 _amount, + address _remoteToken + ) + public + { + // Assume + vm.assume(_remoteToken != address(0)); + + // Arrange + _setUpLegacyToSuper(_from, _to); + + // Mock the legacy factory to return `_remoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _remoteToken); + + // Mock the superchain factory to return address(0) + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, address(0)); + + // Expect the revert with `InvalidSuperchainAddress` selector + vm.expectRevert(InvalidSuperchainAddress.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with different remote tokens reverts + function testFuzz_convert_differentRemoteAddresses_reverts( + address _from, + address _to, + uint256 _amount, + address _fromRemoteToken, + address _toRemoteToken + ) + public + { + // Assume + vm.assume(_fromRemoteToken != address(0)); + vm.assume(_toRemoteToken != address(0)); + vm.assume(_fromRemoteToken != _toRemoteToken); + + // Arrange + _setUpLegacyToSuper(_from, _to); + + // Mock the legacy factory to return `_fromRemoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _fromRemoteToken); + + // Mock the superchain factory to return `_toRemoteToken` + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, _toRemoteToken); + + // Expect the revert with `InvalidTokenPair` selector + vm.expectRevert(InvalidTokenPair.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function succeeds + function testFuzz_convert_succeeds( + address _caller, + address _from, + address _to, + uint256 _amount, + address _remoteToken + ) + public + { + // Assume + vm.assume(_remoteToken != address(0)); + + // Arrange + _setUpLegacyToSuper(_from, _to); + + // Mock the legacy and superchain factory to return `_remoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _remoteToken); + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, _remoteToken); + + // Expect the `Converted` event to be emitted + vm.expectEmit(true, true, true, true, address(l2StandardBridge)); + emit Converted(_from, _to, _caller, _amount); + + // Mock and expect the `burn` and `mint` functions + _mockAndExpect(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), abi.encode()); + _mockAndExpect(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); + + // Act + vm.prank(_caller); + l2StandardBridge.convert(_from, _to, _amount); + } +} + +/// @notice Test suite when converting from a SuperchainERC20 token to a legacy token +contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_Test { + /// @notice Set up the test for converting from a SuperchainERC20 token to a legacy token + function _setUpSuperToLegacy(address _from, address _to) internal { + // Assume + vm.assume(_from != console2.CONSOLE_ADDRESS); + vm.assume(_to != console2.CONSOLE_ADDRESS); + + // Mock same decimals + _mockDecimals(_from, 18); + _mockDecimals(_to, 18); + } + + /// @notice Test that the `convert` function with different decimals reverts + function testFuzz_convert_differentDecimals_reverts( + address _from, + uint8 _decimalsFrom, + address _to, + uint8 _decimalsTo, + uint256 _amount + ) + public + { + // Assume + vm.assume(_from != console2.CONSOLE_ADDRESS); + vm.assume(_to != console2.CONSOLE_ADDRESS); + vm.assume(_decimalsFrom != _decimalsTo); + vm.assume(_from != _to); + + // Arrange + // Mock the tokens to have different decimals + _mockDecimals(_from, _decimalsFrom); + _mockDecimals(_to, _decimalsTo); + + // Expect the revert with `InvalidDecimals` selector + vm.expectRevert(InvalidDecimals.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with an invalid legacy address reverts + function testFuzz_convert_invalidLegacyAddress_reverts(address _from, address _to, uint256 _amount) public { + // Arrange + _setUpSuperToLegacy(_from, _to); + + // Mock the legacy factory to return address(0) + _mockDeployments(address(l2OptimismMintableERC20Factory), _to, address(0)); + + // Expect the revert with `InvalidLegacyAddress` selector + vm.expectRevert(InvalidLegacyAddress.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with an invalid superchain address reverts + function testFuzz_convert_invalidSuperchainAddress_reverts( + address _from, + address _to, + uint256 _amount, + address _remoteToken + ) + public + { + // Assume + vm.assume(_remoteToken != address(0)); + + // Arrange + _setUpSuperToLegacy(_from, _to); + + // Mock the legacy factory to return `_remoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _remoteToken); + + // Mock the superchain factory to return address(0) + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, address(0)); + + // Expect the revert with `InvalidSuperchainAddress` selector + vm.expectRevert(InvalidSuperchainAddress.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function with different remote tokens reverts + function testFuzz_convert_differentRemoteAddresses_reverts( + address _from, + address _to, + uint256 _amount, + address _fromRemoteToken, + address _toRemoteToken + ) + public + { + // Assume + vm.assume(_fromRemoteToken != address(0)); + vm.assume(_toRemoteToken != address(0)); + vm.assume(_fromRemoteToken != _toRemoteToken); + + // Arrange + _setUpSuperToLegacy(_from, _to); + + // Mock the legacy factory to return `_fromRemoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _fromRemoteToken); + + // Mock the superchain factory to return `_toRemoteToken` + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, _toRemoteToken); + + // Expect the revert with `InvalidTokenPair` selector + vm.expectRevert(InvalidTokenPair.selector); + + // Act + l2StandardBridge.convert(_from, _to, _amount); + } + + /// @notice Test that the `convert` function succeeds + function testFuzz_convert_succeeds( + address _caller, + address _from, + address _to, + uint256 _amount, + address _remoteToken + ) + public + { + // Assume + vm.assume(_remoteToken != address(0)); + + // Arrange + _setUpSuperToLegacy(_from, _to); + + // Mock the legacy and superchain factory to return `_remoteToken` + _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _remoteToken); + _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, _remoteToken); + + // Expect the `Converted` event to be emitted + vm.expectEmit(true, true, true, true, address(l2StandardBridge)); + emit Converted(_from, _to, _caller, _amount); + + // Mock and expect the `burn` and `mint` functions + _mockAndExpect(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), abi.encode()); + _mockAndExpect(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); + + // Act + vm.prank(_caller); + l2StandardBridge.convert(_from, _to, _amount); + } +} diff --git a/packages/contracts-bedrock/test/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2Genesis.t.sol index 2bb969546a52..23c457da6a2f 100644 --- a/packages/contracts-bedrock/test/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/L2Genesis.t.sol @@ -150,8 +150,8 @@ contract L2GenesisTest is Test { // 2 predeploys do not have proxies assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); - // 21 proxies have the implementation set if useInterop is true and 17 if useInterop is false - assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 21 : 17); + // 22 proxies have the implementation set if useInterop is true and 17 if useInterop is false + assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 22 : 17); // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin assertEq( diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 537585d7105d..c19d27be9c27 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -5,7 +5,7 @@ import { console2 as console } from "forge-std/console2.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; import { L2CrossDomainMessenger } from "src/L2/L2CrossDomainMessenger.sol"; -import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; +import { L2StandardBridgeInterop } from "src/L2/L2StandardBridgeInterop.sol"; import { L2ToL1MessagePasser } from "src/L2/L2ToL1MessagePasser.sol"; import { L2ERC721Bridge } from "src/L2/L2ERC721Bridge.sol"; import { BaseFeeVault } from "src/L2/BaseFeeVault.sol"; @@ -83,7 +83,7 @@ contract Setup { L2CrossDomainMessenger l2CrossDomainMessenger = L2CrossDomainMessenger(payable(Predeploys.L2_CROSS_DOMAIN_MESSENGER)); - L2StandardBridge l2StandardBridge = L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)); + L2StandardBridgeInterop l2StandardBridge = L2StandardBridgeInterop(payable(Predeploys.L2_STANDARD_BRIDGE)); L2ToL1MessagePasser l2ToL1MessagePasser = L2ToL1MessagePasser(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)); OptimismMintableERC20Factory l2OptimismMintableERC20Factory = OptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 05fff737bd6e..7e1aef108910 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -285,6 +285,14 @@ contract Initializer_Test is Bridge_Initializer { initializedSlotVal: deploy.loadInitializedSlot("L2StandardBridge") }) ); + // L2StandardBridgeInterop + contracts.push( + InitializeableContract({ + target: address(l2StandardBridge), + initCalldata: abi.encodeCall(l2StandardBridge.initialize, (l1StandardBridge)), + initializedSlotVal: deploy.loadInitializedSlot("L2StandardBridgeInterop") + }) + ); // L1ERC721BridgeImpl contracts.push( InitializeableContract({ From 593efad64ddfa3a26966437edfb091c5c951ba7b Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:04:40 -0300 Subject: [PATCH 03/14] fix: add generic factory interface (#14) * test: add L2 standard bridge interop unit tests * fix: add tests natspec * fix: add generic factory interface --- .../src/L2/IOptimismERC20Factory.sol | 12 ++++++++++++ .../src/L2/L2StandardBridgeInterop.sol | 15 +++------------ .../test/L2/L2StandardBridgeInterop.t.sol | 6 ++---- 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol diff --git a/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol b/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol new file mode 100644 index 000000000000..0286a8f7b34e --- /dev/null +++ b/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title IOptimismERC20Factory +/// @notice Generic interface for IOptimismMintableERC20Factory and ISuperchainERC20Factory. Used to +/// determine if a ERC20 contract is deployed by a factory. +interface IOptimismERC20Factory { + /// @notice Checks if a ERC20 token is deployed by the factory. + /// @param _token The address of the ERC20 token to check the deployment. + /// @return _remoteToken The address of the remote token if it is deployed or `address(0)` if not. + function deployments(address _token) external view returns (address _remoteToken); +} diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index 13059150e366..75f7f854b401 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -5,6 +5,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IOptimismERC20Factory } from "src/L2/IOptimismERC20Factory.sol"; /// @notice Thrown when the decimals of the tokens are not the same. error InvalidDecimals(); @@ -18,16 +19,6 @@ error InvalidSuperchainAddress(); /// @notice Thrown when the remote addresses of the tokens are not the same. error InvalidTokenPair(); -// TODO: Use OptimismMintableERC20Factory contract instead of interface -interface IOptimismMintableERC20Factory { - function deployments(address) external view returns (address); -} - -// TODO: Move to a separate file -interface ISuperchainERC20Factory { - function deployments(address) external view returns (address); -} - // TODO: Use an existing interface with `mint` and `burn`? interface MintableAndBurnable is IERC20 { function mint(address, uint256) external; @@ -82,12 +73,12 @@ contract L2StandardBridgeInterop is L2StandardBridge { function _validateFactories(address _legacyAddr, address _superAddr) internal view { // 2. Valid legacy check address _legacyRemoteToken = - IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); + IOptimismERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); if (_legacyRemoteToken == address(0)) revert InvalidLegacyAddress(); // 3. Valid SuperchainERC20 check address _superRemoteToken = - ISuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); + IOptimismERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); if (_superRemoteToken == address(0)) revert InvalidSuperchainAddress(); // 4. Same remote address check diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index a9966f12d55e..db065764eab3 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -12,7 +12,7 @@ import { InvalidLegacyAddress, InvalidSuperchainAddress, InvalidTokenPair, - IOptimismMintableERC20Factory, + IOptimismERC20Factory, MintableAndBurnable } from "src/L2/L2StandardBridgeInterop.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -47,9 +47,7 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { /// @notice Mock factory deployment function _mockDeployments(address _factory, address _token, address _deployed) internal { _mockAndExpect( - _factory, - abi.encodeWithSelector(IOptimismMintableERC20Factory.deployments.selector, _token), - abi.encode(_deployed) + _factory, abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector, _token), abi.encode(_deployed) ); } } From 014c25d80f55227a3dbd83541ec30faddab5633f Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:40:30 -0300 Subject: [PATCH 04/14] feat: modify OptimismMintableERC20Factory for convert (#17) * test: add L2 standard bridge interop unit tests * fix: add tests natspec * fix: add generic factory interface * feat: modify OptimismMintableERC20Factory for convert * fix: use only a public function for create3 * feat: rollback interop factory, modify legacy one * fix: delete local token return variable --- packages/contracts-bedrock/.gas-snapshot | 8 +- packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismMintableERC20Factory.json | 19 +++ .../OptimismMintableERC20Factory.json | 13 +- .../src/L2/IOptimismERC20Factory.sol | 4 +- .../OptimismMintableERC20Factory.sol | 20 ++- .../OptimismMintableERC20Factory.t.sol | 149 +++++++++++++++--- 7 files changed, 174 insertions(+), 43 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index a97f05678b58..b3ea3b88545e 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,7 +1,7 @@ -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369380) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967520) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 561992) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4074035) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index dc1f1cc9c4c5..ce8add4c9f7a 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -196,8 +196,8 @@ "sourceCodeHash": "0x52737b23e99bf79dd2c23196b3298e80aa41f740efc6adc7916e696833eb546a" }, "src/universal/OptimismMintableERC20Factory.sol": { - "initCodeHash": "0xf6f522681e7ae940cb778db68004f122b25194296a65bba7ad1d792bd593c4a6", - "sourceCodeHash": "0x9b8c73ea139f13028008eedef53a6b07576cd6b08979574e6dde3192656e9268" + "initCodeHash": "0x29a49fc387ad84f82199947e49a0d1960902f63492d974c26afd72372e748648", + "sourceCodeHash": "0xbc6cf74368c244bdea8ed64c501129d0b6d41db421dc91d1de051f7b505a4977" }, "src/universal/OptimismMintableERC721.sol": { "initCodeHash": "0xb400f430acf4d65bee9635e4935a6e1e3a0284fc50aea40ad8b7818dc826f31c", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20Factory.json b/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20Factory.json index 3f2f14fcbe59..d3da8e2de735 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20Factory.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20Factory.json @@ -122,6 +122,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "deployments", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20Factory.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20Factory.json index c39454b5e3eb..94c133d105d2 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20Factory.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20Factory.json @@ -28,10 +28,17 @@ "type": "address" }, { - "bytes": "1568", - "label": "__gap", + "bytes": "32", + "label": "deployments", "offset": 0, "slot": "2", - "type": "uint256[49]" + "type": "mapping(address => address)" + }, + { + "bytes": "1536", + "label": "__gap", + "offset": 0, + "slot": "3", + "type": "uint256[48]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol b/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol index 0286a8f7b34e..5e0040aa83cf 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismERC20Factory.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.0; /// determine if a ERC20 contract is deployed by a factory. interface IOptimismERC20Factory { /// @notice Checks if a ERC20 token is deployed by the factory. - /// @param _token The address of the ERC20 token to check the deployment. + /// @param _localToken The address of the ERC20 token to check the deployment. /// @return _remoteToken The address of the remote token if it is deployed or `address(0)` if not. - function deployments(address _token) external view returns (address _remoteToken); + function deployments(address _localToken) external view returns (address _remoteToken); } diff --git a/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol b/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol index 6e7baf31f1fb..799389542abe 100644 --- a/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol +++ b/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { ISemver } from "src/universal/ISemver.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { IOptimismERC20Factory } from "src/L2/IOptimismERC20Factory.sol"; /// @custom:proxied /// @custom:predeployed 0x4200000000000000000000000000000000000012 @@ -12,7 +13,7 @@ import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable /// contracts on the network it's deployed to. Simplifies the deployment process for users /// who may be less familiar with deploying smart contracts. Designed to be backwards /// compatible with the older StandardL2ERC20Factory contract. -contract OptimismMintableERC20Factory is ISemver, Initializable { +contract OptimismMintableERC20Factory is ISemver, Initializable, IOptimismERC20Factory { /// @custom:spacer OptimismMintableERC20Factory's initializer slot spacing /// @notice Spacer to avoid packing into the initializer slot bytes30 private spacer_0_2_30; @@ -21,10 +22,14 @@ contract OptimismMintableERC20Factory is ISemver, Initializable { /// @custom:network-specific address public bridge; + /// @notice Mapping of local token address to remote token address. + /// This is used to keep track of the token deployments. + mapping(address => address) public deployments; + /// @notice Reserve extra slots in the storage layout for future upgrades. - /// A gap size of 49 was chosen here, so that the first slot used in a child contract - /// would be 1 plus a multiple of 50. - uint256[49] private __gap; + /// A gap size of 48 was chosen here, so that the first slot used in a child contract + /// would be a multiple of 50. + uint256[48] private __gap; /// @custom:legacy /// @notice Emitted whenever a new OptimismMintableERC20 is created. Legacy version of the newer @@ -43,8 +48,8 @@ contract OptimismMintableERC20Factory is ISemver, Initializable { /// the OptimismMintableERC20 token contract since this contract /// is responsible for deploying OptimismMintableERC20 contracts. /// @notice Semantic version. - /// @custom:semver 1.9.0 - string public constant version = "1.9.0"; + /// @custom:semver 1.10.0 + string public constant version = "1.10.0"; /// @notice Constructs the OptimismMintableERC20Factory contract. constructor() { @@ -117,9 +122,12 @@ contract OptimismMintableERC20Factory is ISemver, Initializable { require(_remoteToken != address(0), "OptimismMintableERC20Factory: must provide remote token address"); bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); + address localToken = address(new OptimismMintableERC20{ salt: salt }(bridge, _remoteToken, _name, _symbol, _decimals)); + deployments[localToken] = _remoteToken; + // Emit the old event too for legacy support. emit StandardL2TokenCreated(_remoteToken, localToken); diff --git a/packages/contracts-bedrock/test/universal/OptimismMintableERC20Factory.t.sol b/packages/contracts-bedrock/test/universal/OptimismMintableERC20Factory.t.sol index d1919a5ff555..e0c18ecf655b 100644 --- a/packages/contracts-bedrock/test/universal/OptimismMintableERC20Factory.t.sol +++ b/packages/contracts-bedrock/test/universal/OptimismMintableERC20Factory.t.sol @@ -17,19 +17,20 @@ contract OptimismMintableTokenFactory_Test is Bridge_Initializer { event StandardL2TokenCreated(address indexed remoteToken, address indexed localToken); event OptimismMintableERC20Created(address indexed localToken, address indexed remoteToken, address deployer); - /// @dev Tests that the constructor is initialized correctly. + /// @notice Tests that the constructor is initialized correctly. function test_constructor_succeeds() external { OptimismMintableERC20Factory impl = new OptimismMintableERC20Factory(); assertEq(address(impl.BRIDGE()), address(0)); assertEq(address(impl.bridge()), address(0)); } - /// @dev Tests that the proxy is initialized correctly. + /// @notice Tests that the proxy is initialized correctly. function test_initialize_succeeds() external view { assertEq(address(l1OptimismMintableERC20Factory.BRIDGE()), address(l1StandardBridge)); assertEq(address(l1OptimismMintableERC20Factory.bridge()), address(l1StandardBridge)); } + /// @notice Tests that the upgrade is successful. function test_upgrading_succeeds() external { Proxy proxy = Proxy(deploy.mustGetAddress("OptimismMintableERC20FactoryProxy")); // Check an unused slot before upgrading. @@ -49,60 +50,156 @@ contract OptimismMintableTokenFactory_Test is Bridge_Initializer { assertEq(slot21Expected, slot21After); } - function test_createStandardL2Token_succeeds() external { - address remote = address(4); + /// @notice Test that calling `createStandardL2Token` with valid parameters succeeds. + function test_createStandardL2Token_succeeds( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol + ) + external + { + // Assume + vm.assume(_remoteToken != address(0)); + // Arrange // Defaults to 18 decimals - address local = calculateTokenAddress(remote, "Beep", "BOOP", 18); + address local = _calculateTokenAddress(_remoteToken, _name, _symbol, 18); vm.expectEmit(true, true, true, true); - emit StandardL2TokenCreated(remote, local); + emit StandardL2TokenCreated(_remoteToken, local); vm.expectEmit(true, true, true, true); - emit OptimismMintableERC20Created(local, remote, alice); + emit OptimismMintableERC20Created(local, _remoteToken, _caller); - vm.prank(alice); - address addr = l2OptimismMintableERC20Factory.createStandardL2Token(remote, "Beep", "BOOP"); + // Act + vm.prank(_caller); + address addr = l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); + + // Assert assertTrue(addr == local); assertTrue(OptimismMintableERC20(local).decimals() == 18); + assertEq(l2OptimismMintableERC20Factory.deployments(local), _remoteToken); } - function test_createStandardL2TokenWithDecimals_succeeds() external { - address remote = address(4); - address local = calculateTokenAddress(remote, "Beep", "BOOP", 6); + /// @notice Test that calling `createOptimismMintableERC20WithDecimals` with valid parameters succeeds. + function test_createStandardL2TokenWithDecimals_succeeds( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + { + // Assume + vm.assume(_remoteToken != address(0)); + + // Arrange + address local = _calculateTokenAddress(_remoteToken, _name, _symbol, _decimals); vm.expectEmit(true, true, true, true); - emit StandardL2TokenCreated(remote, local); + emit StandardL2TokenCreated(_remoteToken, local); vm.expectEmit(true, true, true, true); - emit OptimismMintableERC20Created(local, remote, alice); + emit OptimismMintableERC20Created(local, _remoteToken, _caller); - vm.prank(alice); - address addr = l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(remote, "Beep", "BOOP", 6); + // Act + vm.prank(_caller); + address addr = l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals( + _remoteToken, _name, _symbol, _decimals + ); + + // Assert assertTrue(addr == local); + assertTrue(OptimismMintableERC20(local).decimals() == _decimals); + assertEq(l2OptimismMintableERC20Factory.deployments(local), _remoteToken); + } + + /// @notice Test that calling `createStandardL2Token` with the same parameters twice reverts. + function test_createStandardL2Token_sameTwice_reverts( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol + ) + external + { + // Assume + vm.assume(_remoteToken != address(0)); + + vm.prank(_caller); + l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); + + // Arrange + vm.expectRevert(bytes("")); - assertTrue(OptimismMintableERC20(local).decimals() == 6); + // Act + vm.prank(_caller); + l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); } - function test_createStandardL2Token_sameTwice_reverts() external { - address remote = address(4); + /// @notice Test that calling `createStandardL2TokenWithDecimals` with the same parameters twice reverts. + function test_createStandardL2TokenWithDecimals_sameTwice_reverts( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + { + // Assume + vm.assume(_remoteToken != address(0)); - vm.prank(alice); - l2OptimismMintableERC20Factory.createStandardL2Token(remote, "Beep", "BOOP"); + vm.prank(_caller); + l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(_remoteToken, _name, _symbol, _decimals); + // Arrange vm.expectRevert(bytes("")); - vm.prank(alice); - l2OptimismMintableERC20Factory.createStandardL2Token(remote, "Beep", "BOOP"); + // Act + vm.prank(_caller); + l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(_remoteToken, _name, _symbol, _decimals); } - function test_createStandardL2Token_remoteIsZero_reverts() external { + /// @notice Test that calling `createStandardL2Token` with a zero remote token address reverts. + function test_createStandardL2Token_remoteIsZero_reverts( + address _caller, + string memory _name, + string memory _symbol + ) + external + { + // Arrange address remote = address(0); vm.expectRevert("OptimismMintableERC20Factory: must provide remote token address"); - l2OptimismMintableERC20Factory.createStandardL2Token(remote, "Beep", "BOOP"); + + // Act + vm.prank(_caller); + l2OptimismMintableERC20Factory.createStandardL2Token(remote, _name, _symbol); + } + + /// @notice Test that calling `createStandardL2TokenWithDecimals` with a zero remote token address reverts. + function test_createStandardL2TokenWithDecimals_remoteIsZero_reverts( + address _caller, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + { + // Arrange + address remote = address(0); + vm.expectRevert("OptimismMintableERC20Factory: must provide remote token address"); + + // Act + vm.prank(_caller); + l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(remote, _name, _symbol, _decimals); } - function calculateTokenAddress( + /// @notice Precalculates the address of the token contract. + function _calculateTokenAddress( address _remote, string memory _name, string memory _symbol, From 809a71bc419b71678032de4957f900e1663b7419 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 13 Aug 2024 16:38:15 -0300 Subject: [PATCH 05/14] fix: PR fixes --- .../contracts-bedrock/scripts/L2Genesis.s.sol | 10 ++++- packages/contracts-bedrock/semver-lock.json | 8 +++- .../snapshots/abi/L2StandardBridge.json | 2 +- .../abi/L2StandardBridgeInterop.json | 6 +-- .../src/L2/L2StandardBridge.sol | 7 +++- .../src/L2/L2StandardBridgeInterop.sol | 18 +++++--- .../src/libraries/Predeploys.sol | 3 +- .../test/L2/L2StandardBridgeInterop.t.sol | 42 +++++++++++-------- 8 files changed, 63 insertions(+), 33 deletions(-) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index e15459166999..7d4568653936 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -286,7 +286,15 @@ contract L2Genesis is Deployer { /// @notice This predeploy is following the safety invariant #1. function setL2StandardBridge(address payable _l1StandardBridgeProxy) public { - address impl = _setImplementationCode(Predeploys.L2_STANDARD_BRIDGE); + address impl; + if (cfg.useInterop()) { + string memory cname = "L2StandardBridgeInterop"; + impl = Predeploys.predeployToCodeNamespace(Predeploys.L2_STANDARD_BRIDGE); + console.log("Setting %s implementation at: %s", cname, impl); + vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); + } else { + impl = _setImplementationCode(Predeploys.L2_STANDARD_BRIDGE); + } L2StandardBridge(payable(impl)).initialize({ _otherBridge: L1StandardBridge(payable(address(0))) }); diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 79fce55d3de4..507c5d7efaf1 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -100,8 +100,12 @@ "sourceCodeHash": "0xb0806d362ba5cc39acfb09e0606059a2b10a7c1d5bc1f86cd4561fd24f7dcada" }, "src/L2/L2StandardBridge.sol": { - "initCodeHash": "0xb6af582e9edaae531d48559b7cd0ca5813a112361ea19b8cb46292726ad88d40", - "sourceCodeHash": "0xb4a9f333f04008f610eb55fa6ff7e610bab340d53156cb50ec65a575c9576b0e" + "initCodeHash": "0xfbfc7bd101022024b94114c26128c6028c25dec07e8d40fdcfdb180c0ba6ddee", + "sourceCodeHash": "0xb1a5fb22b124e8fa8eb5bae4b8e0770abbdbebe32be00480317cf4aaada28ed3" + }, + "src/L2/L2StandardBridgeInterop.sol": { + "initCodeHash": "0xd7f85eef12b60211104cddbd86d9f458cd31a0ba74f382404799bcf86ef003ba", + "sourceCodeHash": "0x00f415380689a5ee1762e93b032d5c3de2fcddb36b0a068cb5616f7e8001ddc0" }, "src/L2/L2ToL1MessagePasser.sol": { "initCodeHash": "0x08bbede75cd6dfd076903b8f04d24f82fa7881576c135825098778632e37eebc", diff --git a/packages/contracts-bedrock/snapshots/abi/L2StandardBridge.json b/packages/contracts-bedrock/snapshots/abi/L2StandardBridge.json index a73e6038c22a..da71bae10f06 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2StandardBridge.json +++ b/packages/contracts-bedrock/snapshots/abi/L2StandardBridge.json @@ -311,7 +311,7 @@ "type": "string" } ], - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json b/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json index 1a4cae148836..d870c7180a30 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/L2StandardBridgeInterop.json @@ -329,7 +329,7 @@ "type": "string" } ], - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { @@ -678,12 +678,12 @@ }, { "inputs": [], - "name": "InvalidLegacyAddress", + "name": "InvalidLegacyERC20Address", "type": "error" }, { "inputs": [], - "name": "InvalidSuperchainAddress", + "name": "InvalidSuperchainERC20Address", "type": "error" }, { diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol index 1472d0fd9e86..b11314bcc626 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol @@ -52,8 +52,11 @@ contract L2StandardBridge is StandardBridge, ISemver { bytes extraData ); - /// @custom:semver 1.10.0 - string public constant version = "1.10.0"; + /// @notice Semantic version. + /// @custom:semver 1.11.0 + function version() public pure virtual returns (string memory) { + return "1.11.0"; + } /// @notice Constructs the L2StandardBridge contract. constructor() StandardBridge() { diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index 75f7f854b401..818ee0f9a741 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -11,15 +11,17 @@ import { IOptimismERC20Factory } from "src/L2/IOptimismERC20Factory.sol"; error InvalidDecimals(); /// @notice Thrown when the legacy address is not found in the OptimismMintableERC20Factory. -error InvalidLegacyAddress(); +error InvalidLegacyERC20Address(); /// @notice Thrown when the SuperchainERC20 address is not found in the SuperchainERC20Factory. -error InvalidSuperchainAddress(); +error InvalidSuperchainERC20Address(); /// @notice Thrown when the remote addresses of the tokens are not the same. error InvalidTokenPair(); -// TODO: Use an existing interface with `mint` and `burn`? +/// TODO: Define a better naming convention for this interface. +/// @notice Interface for minting and burning tokens in the L2StandardBridge. +/// Used for StandardL2ERC20, OptimismMintableERC20 and OptimismSuperchainERC20. interface MintableAndBurnable is IERC20 { function mint(address, uint256) external; function burn(address, uint256) external; @@ -39,6 +41,12 @@ contract L2StandardBridgeInterop is L2StandardBridge { /// @param amount The amount of tokens being converted. event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + /// @notice Semantic version. + /// @custom:semver +interop + function version() public pure override returns (string memory) { + return string.concat(super.version(), "+interop"); + } + /// @notice Converts `amount` of `from` token to `to` token. /// @param _from The token being converted from. /// @param _to The token being converted to. @@ -74,12 +82,12 @@ contract L2StandardBridgeInterop is L2StandardBridge { // 2. Valid legacy check address _legacyRemoteToken = IOptimismERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); - if (_legacyRemoteToken == address(0)) revert InvalidLegacyAddress(); + if (_legacyRemoteToken == address(0)) revert InvalidLegacyERC20Address(); // 3. Valid SuperchainERC20 check address _superRemoteToken = IOptimismERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); - if (_superRemoteToken == address(0)) revert InvalidSuperchainAddress(); + if (_superRemoteToken == address(0)) revert InvalidSuperchainERC20Address(); // 4. Same remote address check if (_legacyRemoteToken != _superRemoteToken) revert InvalidTokenPair(); diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 53d5955e3688..9dcd7f0a95d0 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -95,6 +95,7 @@ library Predeploys { /// @notice Address of the ETHLiquidity predeploy. address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025; + /// TODO: Add correct predeploy address for OptimismSuperchainERC20Factory /// @notice Address of the OptimismSuperchainERC20Factory predeploy. address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026; @@ -107,7 +108,7 @@ library Predeploys { if (_addr == WETH) return "WETH"; if (_addr == L2_CROSS_DOMAIN_MESSENGER) return "L2CrossDomainMessenger"; if (_addr == GAS_PRICE_ORACLE) return "GasPriceOracle"; - if (_addr == L2_STANDARD_BRIDGE) return "L2StandardBridgeInterop"; + if (_addr == L2_STANDARD_BRIDGE) return "L2StandardBridge"; if (_addr == SEQUENCER_FEE_WALLET) return "SequencerFeeVault"; if (_addr == OPTIMISM_MINTABLE_ERC20_FACTORY) return "OptimismMintableERC20Factory"; if (_addr == L1_BLOCK_NUMBER) return "L1BlockNumber"; diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index db065764eab3..e1151002eec5 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -9,8 +9,8 @@ import { console2 } from "forge-std/console2.sol"; import { L2StandardBridgeInterop, InvalidDecimals, - InvalidLegacyAddress, - InvalidSuperchainAddress, + InvalidLegacyERC20Address, + InvalidSuperchainERC20Address, InvalidTokenPair, IOptimismERC20Factory, MintableAndBurnable @@ -26,6 +26,12 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { /// @notice Emitted when a conversion is made. event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + /// @notice Test setup. + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { vm.mockCall(_receiver, _calldata, _returned); @@ -97,23 +103,23 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T l2StandardBridge.convert(_from, _to, _amount); } - /// @notice Test that the `convert` function with an invalid legacy address reverts - function testFuzz_convert_invalidLegacyAddress_reverts(address _from, address _to, uint256 _amount) public { + /// @notice Test that the `convert` function with an invalid legacy ERC20 address reverts + function testFuzz_convert_invalidLegacyERC20Address_reverts(address _from, address _to, uint256 _amount) public { // Arrange _setUpLegacyToSuper(_from, _to); // Mock the legacy factory to return address(0) _mockDeployments(address(l2OptimismMintableERC20Factory), _from, address(0)); - // Expect the revert with `InvalidLegacyAddress` selector - vm.expectRevert(InvalidLegacyAddress.selector); + // Expect the revert with `InvalidLegacyERC20Address` selector + vm.expectRevert(InvalidLegacyERC20Address.selector); // Act l2StandardBridge.convert(_from, _to, _amount); } - /// @notice Test that the `convert` function with an invalid superchain address reverts - function testFuzz_convert_invalidSuperchainAddress_reverts( + /// @notice Test that the `convert` function with an invalid superchain ERC20 address reverts + function testFuzz_convert_invalidSuperchainERC20Address_reverts( address _from, address _to, uint256 _amount, @@ -133,8 +139,8 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T // Mock the superchain factory to return address(0) _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, address(0)); - // Expect the revert with `InvalidSuperchainAddress` selector - vm.expectRevert(InvalidSuperchainAddress.selector); + // Expect the revert with `InvalidSuperchainERC20Address` selector + vm.expectRevert(InvalidSuperchainERC20Address.selector); // Act l2StandardBridge.convert(_from, _to, _amount); @@ -246,23 +252,23 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T l2StandardBridge.convert(_from, _to, _amount); } - /// @notice Test that the `convert` function with an invalid legacy address reverts - function testFuzz_convert_invalidLegacyAddress_reverts(address _from, address _to, uint256 _amount) public { + /// @notice Test that the `convert` function with an invalid legacy ERC20 address reverts + function testFuzz_convert_invalidLegacyERC20Address_reverts(address _from, address _to, uint256 _amount) public { // Arrange _setUpSuperToLegacy(_from, _to); // Mock the legacy factory to return address(0) _mockDeployments(address(l2OptimismMintableERC20Factory), _to, address(0)); - // Expect the revert with `InvalidLegacyAddress` selector - vm.expectRevert(InvalidLegacyAddress.selector); + // Expect the revert with `InvalidLegacyERC20Address` selector + vm.expectRevert(InvalidLegacyERC20Address.selector); // Act l2StandardBridge.convert(_from, _to, _amount); } - /// @notice Test that the `convert` function with an invalid superchain address reverts - function testFuzz_convert_invalidSuperchainAddress_reverts( + /// @notice Test that the `convert` function with an invalid superchain ERC20 address reverts + function testFuzz_convert_invalidSuperchainERC20Address_reverts( address _from, address _to, uint256 _amount, @@ -282,8 +288,8 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T // Mock the superchain factory to return address(0) _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, address(0)); - // Expect the revert with `InvalidSuperchainAddress` selector - vm.expectRevert(InvalidSuperchainAddress.selector); + // Expect the revert with `InvalidSuperchainERC20Address` selector + vm.expectRevert(InvalidSuperchainERC20Address.selector); // Act l2StandardBridge.convert(_from, _to, _amount); From 2a3a723ac827f5b9fb7f8ed041be42aee1158916 Mon Sep 17 00:00:00 2001 From: agusduha Date: Wed, 14 Aug 2024 15:07:55 -0300 Subject: [PATCH 06/14] test: fix address assuming --- .../test/L2/L2StandardBridgeInterop.t.sol | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index e1151002eec5..e6ca1fb4931d 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; // Target contract is imported by the `Bridge_Initializer` import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; -import { console2 } from "forge-std/console2.sol"; // Target contract dependencies import { @@ -56,6 +55,15 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { _factory, abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector, _token), abi.encode(_deployed) ); } + + /// @notice Assume a valid address for fuzzing + function _assumeAddress(address _address) internal { + vm.assume( + _address.code.length == 0 // No accounts with code + && _address != CONSOLE // The console has no code but behaves like a contract + && uint160(_address) > 9 // No precompiles (or zero address) + ); + } } /// @notice Test suite when converting from a legacy token to a SuperchainERC20 token @@ -63,8 +71,8 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T /// @notice Set up the test for converting from a legacy token to a SuperchainERC20 token function _setUpLegacyToSuper(address _from, address _to) internal { // Assume - vm.assume(_from != console2.CONSOLE_ADDRESS); - vm.assume(_to != console2.CONSOLE_ADDRESS); + _assumeAddress(_from); + _assumeAddress(_to); // Mock same decimals _mockDecimals(_from, 18); @@ -86,8 +94,8 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T public { // Assume - vm.assume(_from != console2.CONSOLE_ADDRESS); - vm.assume(_to != console2.CONSOLE_ADDRESS); + _assumeAddress(_from); + _assumeAddress(_to); vm.assume(_decimalsFrom != _decimalsTo); vm.assume(_from != _to); @@ -216,8 +224,8 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T /// @notice Set up the test for converting from a SuperchainERC20 token to a legacy token function _setUpSuperToLegacy(address _from, address _to) internal { // Assume - vm.assume(_from != console2.CONSOLE_ADDRESS); - vm.assume(_to != console2.CONSOLE_ADDRESS); + _assumeAddress(_from); + _assumeAddress(_to); // Mock same decimals _mockDecimals(_from, 18); @@ -235,8 +243,8 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T public { // Assume - vm.assume(_from != console2.CONSOLE_ADDRESS); - vm.assume(_to != console2.CONSOLE_ADDRESS); + _assumeAddress(_from); + _assumeAddress(_to); vm.assume(_decimalsFrom != _decimalsTo); vm.assume(_from != _to); From 8b74736b047f8126ae409d076b40769600d44420 Mon Sep 17 00:00:00 2001 From: agusduha Date: Wed, 14 Aug 2024 15:44:25 -0300 Subject: [PATCH 07/14] test: fix view warning --- .../contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index e6ca1fb4931d..ca8e968fe9e4 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -57,7 +57,7 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { } /// @notice Assume a valid address for fuzzing - function _assumeAddress(address _address) internal { + function _assumeAddress(address _address) internal view { vm.assume( _address.code.length == 0 // No accounts with code && _address != CONSOLE // The console has no code but behaves like a contract From 05a54f3bd4f932e5ca66fe2a6b3a82342c3bbcf0 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:57:02 -0300 Subject: [PATCH 08/14] feat: add superchain erc20 factory implementation (#23) * feat: add superchain erc20 factory implementation * fix: remove createX comments --- packages/contracts-bedrock/semver-lock.json | 8 ++ .../abi/OptimismSuperchainERC20Beacon.json | 28 ++++++ .../abi/OptimismSuperchainERC20Factory.json | 93 +++++++++++++++++++ .../OptimismSuperchainERC20Beacon.json | 1 + .../OptimismSuperchainERC20Factory.json | 9 ++ .../src/L2/OptimismSuperchainERC20Beacon.sol | 24 +++++ .../src/L2/OptimismSuperchainERC20Factory.sol | 66 +++++++++++++ .../src/libraries/Predeploys.sol | 8 +- 8 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json create mode 100644 packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Beacon.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Factory.json create mode 100644 packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol create mode 100644 packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 84e763bda19f..90c7426be51c 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -119,6 +119,14 @@ "initCodeHash": "0xd49214518ea1a30a43fac09f28b2cee9be570894a500cef342762c9820a070b0", "sourceCodeHash": "0x6943d40010dcbd1d51dc3668d0a154fbb1568ea49ebcf3aa039d65ef6eab321b" }, + "src/L2/OptimismSuperchainERC20Beacon.sol": { + "initCodeHash": "0xc9893a1ba9b6354ccbdc96918510c9402a0934a59f0cc4a6165955a8e8ee1f7d", + "sourceCodeHash": "0xd069f4bddd08c1435d286819e1bb765648a5ad861fa327547f52de65a372769e" + }, + "src/L2/OptimismSuperchainERC20Factory.sol": { + "initCodeHash": "0x98011045722178751e4a1112892f7d9a11bc1f5e42ac18205b6d30a1f1476d24", + "sourceCodeHash": "0x3206f4640662183bfbcd9c183b0d1c5d67c3ea670143daa731f23f62c0ae0855" + }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", "sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c" diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json new file mode 100644 index 000000000000..a06b5b5d140e --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json @@ -0,0 +1,28 @@ +[ + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json new file mode 100644 index 000000000000..7171cf1f3198 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json @@ -0,0 +1,93 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "_superchainERC20", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "superchainToken", + "type": "address" + } + ], + "name": "deployments", + "outputs": [ + { + "internalType": "address", + "name": "remoteToken", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "superchainToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "deployer", + "type": "address" + } + ], + "name": "OptimismSuperchainERC20Created", + "type": "event" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Beacon.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Beacon.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Beacon.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Factory.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Factory.json new file mode 100644 index 000000000000..3dd455fbc1bd --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20Factory.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "32", + "label": "deployments", + "offset": 0, + "slot": "0", + "type": "mapping(address => address)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol new file mode 100644 index 000000000000..7a19e1e9af9b --- /dev/null +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; +import { ISemver } from "src/universal/ISemver.sol"; + +/// @custom:proxied +/// @custom:predeployed 0x4200000000000000000000000000000000000027 +/// @title OptimismSuperchainERC20Beacon +/// @notice OptimismSuperchainERC20Beacon is the beacon proxy for the OptimismSuperchainERC20 implementation. +contract OptimismSuperchainERC20Beacon is IBeacon, ISemver { + /// TODO: Replace with real implementation address + /// @notice Address of the OptimismSuperchainERC20 implementation. + address internal constant IMPLEMENTATION_ADDRESS = 0x0000000000000000000000000000000000000000; + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + string public constant version = "1.0.0-beta.1"; + + /// @inheritdoc IBeacon + function implementation() external pure override returns (address) { + return IMPLEMENTATION_ADDRESS; + } +} diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol new file mode 100644 index 000000000000..95ae5bc071a6 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IOptimismERC20Factory } from "src/L2/IOptimismERC20Factory.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol"; +import { CREATE3 } from "@rari-capital/solmate/src/utils/CREATE3.sol"; + +/// @custom:proxied +/// @custom:predeployed 0x4200000000000000000000000000000000000026 +/// @title OptimismSuperchainERC20Factory +/// @notice OptimismSuperchainERC20Factory is a factory contract that deploys OptimismSuperchainERC20 Beacon Proxies +/// using CREATE3. +contract OptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { + /// @notice Mapping of the deployed OptimismSuperchainERC20 to the remote token address. + /// This is used to keep track of the token deployments. + mapping(address superchainToken => address remoteToken) public deployments; + + /// @notice Emitted when a OptimismSuperchainERC20 is deployed. + /// @param superchainToken Address of the SuperchainERC20 deployment. + /// @param remoteToken Address of the corresponding token on the remote chain. + /// @param deployer Address of the account that deployed the token. + event OptimismSuperchainERC20Created( + address indexed superchainToken, address indexed remoteToken, address deployer + ); + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + string public constant version = "1.0.0-beta.1"; + + /// @notice Deploys a OptimismSuperchainERC20 Beacon Proxy using CREATE3. + /// @param _remoteToken Address of the remote token. + /// @param _name Name of the OptimismSuperchainERC20. + /// @param _symbol Symbol of the OptimismSuperchainERC20. + /// @param _decimals Decimals of the OptimismSuperchainERC20. + /// @return _superchainERC20 Address of the OptimismSuperchainERC20 deployment. + function deploy( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + returns (address _superchainERC20) + { + // Encode the `initialize` call data for the OptimismSuperchainERC20. + bytes memory initCallData = + abi.encodeCall(OptimismSuperchainERC20.initialize, (_remoteToken, _name, _symbol, _decimals)); + + // Encode the BeaconProxy creation code with the beacon contract address and the `initialize` call data. + bytes memory creationCode = bytes.concat( + type(BeaconProxy).creationCode, abi.encode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON, initCallData) + ); + + // Use CREATE3 for deterministic deployment. + bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); + _superchainERC20 = CREATE3.deploy({ salt: salt, creationCode: creationCode, value: 0 }); + + // Store OptimismSuperchainERC20 and remote token addresses + deployments[_superchainERC20] = _remoteToken; + + emit OptimismSuperchainERC20Created(_superchainERC20, _remoteToken, msg.sender); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 9dcd7f0a95d0..9e252d52d1b7 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -95,10 +95,12 @@ library Predeploys { /// @notice Address of the ETHLiquidity predeploy. address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025; - /// TODO: Add correct predeploy address for OptimismSuperchainERC20Factory /// @notice Address of the OptimismSuperchainERC20Factory predeploy. address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026; + /// @notice Address of the OptimismSuperchainERC20Beacon predeploy. + address internal constant OPTIMISM_SUPERCHAIN_ERC20_BEACON = 0x4200000000000000000000000000000000000027; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -128,6 +130,7 @@ library Predeploys { if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH"; if (_addr == ETH_LIQUIDITY) return "ETHLiquidity"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; + if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; revert("Predeploys: unnamed predeploy"); } @@ -146,7 +149,8 @@ library Predeploys { || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) - || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON); } function isPredeployNamespace(address _addr) internal pure returns (bool) { From 25345553ab022e4fd872866537296df52fc73b01 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:57:49 -0300 Subject: [PATCH 09/14] test: add superchain erc20 factory tests (#25) * test: add superchain erc20 factory tests * test: add erc20 asserts --- packages/contracts-bedrock/.gas-snapshot | 8 +- .../contracts-bedrock/scripts/Artifacts.s.sol | 4 + .../contracts-bedrock/scripts/L2Genesis.s.sol | 14 +++ .../test/L2/L2StandardBridgeInterop.t.sol | 15 +-- .../test/L2/OptimismSuperchainERC20.t.sol | 32 ++++- .../L2/OptimismSuperchainERC20Factory.t.sol | 111 ++++++++++++++++++ .../contracts-bedrock/test/L2Genesis.t.sol | 4 +- .../contracts-bedrock/test/setup/Setup.sol | 7 ++ 8 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index b3ea3b88545e..bf6d48b6d342 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,11 +1,11 @@ GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564489) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076532) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624) -GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973) +GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92970) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68433) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68903) -GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155618) \ No newline at end of file +GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155615) \ No newline at end of file diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index b054d2e27254..f912c7f03824 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -152,6 +152,10 @@ abstract contract Artifacts { return payable(Predeploys.SCHEMA_REGISTRY); } else if (digest == keccak256(bytes("EAS"))) { return payable(Predeploys.EAS); + } else if (digest == keccak256(bytes("OptimismSuperchainERC20Factory"))) { + return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + } else if (digest == keccak256(bytes("OptimismSuperchainERC20Beacon"))) { + return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); } return payable(address(0)); } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 7d4568653936..efa7baf1fa79 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -254,6 +254,8 @@ contract L2Genesis is Deployer { setL2ToL2CrossDomainMessenger(); // 23 setSuperchainWETH(); // 24 setETHLiquidity(); // 25 + setOptimismSuperchainERC20Factory(); // 26 + setOptimismSuperchainERC20Beacon(); // 27 } } @@ -505,6 +507,18 @@ contract L2Genesis is Deployer { _setImplementationCode(Predeploys.SUPERCHAIN_WETH); } + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setOptimismSuperchainERC20Factory() internal { + _setImplementationCode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + } + + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setOptimismSuperchainERC20Beacon() internal { + _setImplementationCode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); + } + /// @notice Sets all the preinstalls. /// Warning: the creator-accounts of the preinstall contracts have 0 nonce values. /// When performing a regular user-initiated contract-creation of a preinstall, diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index ca8e968fe9e4..2a4fb8ce201d 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -18,9 +18,6 @@ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/I import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { IOptimismMintableERC20 } from "src/universal/IOptimismMintableERC20.sol"; -// TODO: Replace Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY with optimismSuperchainERC20Factory -import { Predeploys } from "src/libraries/Predeploys.sol"; - contract L2StandardBridgeInterop_Test is Bridge_Initializer { /// @notice Emitted when a conversion is made. event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); @@ -145,7 +142,7 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _remoteToken); // Mock the superchain factory to return address(0) - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, address(0)); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _to, address(0)); // Expect the revert with `InvalidSuperchainERC20Address` selector vm.expectRevert(InvalidSuperchainERC20Address.selector); @@ -176,7 +173,7 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _fromRemoteToken); // Mock the superchain factory to return `_toRemoteToken` - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, _toRemoteToken); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _to, _toRemoteToken); // Expect the revert with `InvalidTokenPair` selector vm.expectRevert(InvalidTokenPair.selector); @@ -203,7 +200,7 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T // Mock the legacy and superchain factory to return `_remoteToken` _mockDeployments(address(l2OptimismMintableERC20Factory), _from, _remoteToken); - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _to, _remoteToken); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _to, _remoteToken); // Expect the `Converted` event to be emitted vm.expectEmit(true, true, true, true, address(l2StandardBridge)); @@ -294,7 +291,7 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _remoteToken); // Mock the superchain factory to return address(0) - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, address(0)); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _from, address(0)); // Expect the revert with `InvalidSuperchainERC20Address` selector vm.expectRevert(InvalidSuperchainERC20Address.selector); @@ -325,7 +322,7 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _fromRemoteToken); // Mock the superchain factory to return `_toRemoteToken` - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, _toRemoteToken); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _from, _toRemoteToken); // Expect the revert with `InvalidTokenPair` selector vm.expectRevert(InvalidTokenPair.selector); @@ -352,7 +349,7 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T // Mock the legacy and superchain factory to return `_remoteToken` _mockDeployments(address(l2OptimismMintableERC20Factory), _to, _remoteToken); - _mockDeployments(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY, _from, _remoteToken); + _mockDeployments(address(l2OptimismSuperchainERC20Factory), _from, _remoteToken); // Expect the `Converted` event to be emitted vm.expectEmit(true, true, true, true, address(l2StandardBridge)); diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 84580fdd8687..73377735192c 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -3,14 +3,16 @@ pragma solidity 0.8.25; // Testing utilities import { Test } from "forge-std/Test.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.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 { ERC1967Proxy } from "@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165.sol"; +import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; +import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol"; // Target contract import { @@ -40,9 +42,32 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Sets up the test suite. function setUp() public { superchainERC20Impl = new OptimismSuperchainERC20(); + + // Deploy the OptimismSuperchainERC20Beacon contract + _deployBeacon(); + superchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); } + /// @notice Deploy the OptimismSuperchainERC20Beacon predeploy contract + function _deployBeacon() internal { + // Deploy the OptimismSuperchainERC20Beacon implementation + address _addr = Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON; + address _impl = Predeploys.predeployToCodeNamespace(_addr); + vm.etch(_impl, vm.getDeployedCode("OptimismSuperchainERC20Beacon.sol:OptimismSuperchainERC20Beacon")); + + // Deploy the ERC1967Proxy contract at the Predeploy + bytes memory code = vm.getDeployedCode("universal/Proxy.sol:Proxy"); + vm.etch(_addr, code); + EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); + EIP1967Helper.setImplementation(_addr, _impl); + + // Mock implementation address + vm.mockCall( + _impl, abi.encodeWithSelector(IBeacon.implementation.selector), abi.encode(address(superchainERC20Impl)) + ); + } + /// @notice Helper function to deploy a proxy of the OptimismSuperchainERC20 contract. function _deploySuperchainERC20Proxy( address _remoteToken, @@ -55,9 +80,8 @@ contract OptimismSuperchainERC20Test is Test { { return OptimismSuperchainERC20( address( - // TODO: Use the SuperchainERC20 Beacon Proxy - new ERC1967Proxy( - address(superchainERC20Impl), + new BeaconProxy( + Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON, abi.encodeCall(OptimismSuperchainERC20.initialize, (_remoteToken, _name, _symbol, _decimals)) ) ) diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol new file mode 100644 index 000000000000..b82a40bf2bc7 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test } from "forge-std/Test.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { CREATE3, Bytes32AddressLib } from "@rari-capital/solmate/src/utils/CREATE3.sol"; +import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; + +// Target contract +import { OptimismSuperchainERC20Factory, OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20Factory.sol"; + +/// @title OptimismSuperchainERC20FactoryTest +/// @notice Contract for testing the OptimismSuperchainERC20Factory contract. +contract OptimismSuperchainERC20FactoryTest is Test { + using Bytes32AddressLib for bytes32; + + OptimismSuperchainERC20 public superchainERC20Impl; + OptimismSuperchainERC20Factory public superchainERC20Factory; + + /// @notice Sets up the test suite. + function setUp() public { + superchainERC20Impl = new OptimismSuperchainERC20(); + + // Deploy the OptimismSuperchainERC20Beacon contract + _deployBeacon(); + + superchainERC20Factory = new OptimismSuperchainERC20Factory(); + } + + /// @notice Deploy the OptimismSuperchainERC20Beacon predeploy contract + function _deployBeacon() internal { + // Deploy the OptimismSuperchainERC20Beacon implementation + address _addr = Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON; + address _impl = Predeploys.predeployToCodeNamespace(_addr); + vm.etch(_impl, vm.getDeployedCode("OptimismSuperchainERC20Beacon.sol:OptimismSuperchainERC20Beacon")); + + // Deploy the ERC1967Proxy contract at the Predeploy + bytes memory code = vm.getDeployedCode("universal/Proxy.sol:Proxy"); + vm.etch(_addr, code); + EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); + EIP1967Helper.setImplementation(_addr, _impl); + + // Mock implementation address + vm.mockCall( + _impl, abi.encodeWithSelector(IBeacon.implementation.selector), abi.encode(address(superchainERC20Impl)) + ); + } + + /// @notice Test that calling `deploy` with valid parameters succeeds. + function test_deploy_succeeds( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + public + { + // Arrange + bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); + address deployment = _calculateTokenAddress(salt, address(superchainERC20Factory)); + + vm.expectEmit(true, true, true, true); + emit OptimismSuperchainERC20Factory.OptimismSuperchainERC20Created(deployment, _remoteToken, _caller); + + // Act + vm.prank(_caller); + address addr = superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + + // Assert + assertTrue(addr == deployment); + assertTrue(OptimismSuperchainERC20(deployment).decimals() == _decimals); + assertTrue(OptimismSuperchainERC20(deployment).remoteToken() == _remoteToken); + assertEq(OptimismSuperchainERC20(deployment).name(), _name); + assertEq(OptimismSuperchainERC20(deployment).symbol(), _symbol); + assertEq(superchainERC20Factory.deployments(deployment), _remoteToken); + } + + /// @notice Test that calling `deploy` with the same parameters twice reverts. + function test_deploy_sameTwice_reverts( + address _caller, + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + { + // Arrange + vm.prank(_caller); + superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + + vm.expectRevert(bytes("DEPLOYMENT_FAILED")); + + // Act + vm.prank(_caller); + superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + } + + /// @notice Precalculates the address of the token contract using CREATE3. + function _calculateTokenAddress(bytes32 _salt, address _deployer) internal pure returns (address) { + address proxy = + keccak256(abi.encodePacked(bytes1(0xFF), _deployer, _salt, CREATE3.PROXY_BYTECODE_HASH)).fromLast20Bytes(); + + return keccak256(abi.encodePacked(hex"d694", proxy, hex"01")).fromLast20Bytes(); + } +} diff --git a/packages/contracts-bedrock/test/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2Genesis.t.sol index 23c457da6a2f..882de4adeb53 100644 --- a/packages/contracts-bedrock/test/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/L2Genesis.t.sol @@ -150,8 +150,8 @@ contract L2GenesisTest is Test { // 2 predeploys do not have proxies assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); - // 22 proxies have the implementation set if useInterop is true and 17 if useInterop is false - assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 22 : 17); + // 23 proxies have the implementation set if useInterop is true and 17 if useInterop is false + assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 23 : 17); // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin assertEq( diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 5dc12d615afb..856658cfcfd7 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -43,6 +43,7 @@ import { DataAvailabilityChallenge } from "src/L1/DataAvailabilityChallenge.sol" import { WETH } from "src/L2/WETH.sol"; import { SuperchainWETH } from "src/L2/SuperchainWETH.sol"; import { ETHLiquidity } from "src/L2/ETHLiquidity.sol"; +import { IOptimismERC20Factory } from "src/L2/IOptimismERC20Factory.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -99,6 +100,10 @@ contract Setup { SuperchainWETH superchainWeth = SuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)); ETHLiquidity ethLiquidity = ETHLiquidity(Predeploys.ETH_LIQUIDITY); + // TODO: Replace with OptimismSuperchainERC20Factory when updating pragmas + IOptimismERC20Factory l2OptimismSuperchainERC20Factory = + IOptimismERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + /// @dev Deploys the Deploy contract without including its bytecode in the bytecode /// of this contract by fetching the bytecode dynamically using `vm.getCode()`. /// If the Deploy bytecode is included in this contract, then it will double @@ -217,6 +222,8 @@ contract Setup { labelPredeploy(Predeploys.WETH); labelPredeploy(Predeploys.SUPERCHAIN_WETH); labelPredeploy(Predeploys.ETH_LIQUIDITY); + labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3); From 49b3ca8a51773b244ab353302aa7ef43b3a85867 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 23 Aug 2024 09:27:51 -0300 Subject: [PATCH 10/14] test: fix expect emit --- .../test/L2/OptimismSuperchainERC20Factory.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol index b82a40bf2bc7..3951ebf7452e 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol @@ -64,7 +64,7 @@ contract OptimismSuperchainERC20FactoryTest is Test { bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); address deployment = _calculateTokenAddress(salt, address(superchainERC20Factory)); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(superchainERC20Factory)); emit OptimismSuperchainERC20Factory.OptimismSuperchainERC20Created(deployment, _remoteToken, _caller); // Act From 348b79848c85893fdcd45a11e3c0aa24f59ca4f7 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 27 Aug 2024 16:15:06 -0300 Subject: [PATCH 11/14] fix: remove comments --- packages/contracts-bedrock/semver-lock.json | 2 +- .../src/L2/OptimismSuperchainERC20Factory.sol | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 3aed12e4791e..bbfa7aeaae20 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -125,7 +125,7 @@ }, "src/L2/OptimismSuperchainERC20Factory.sol": { "initCodeHash": "0x98011045722178751e4a1112892f7d9a11bc1f5e42ac18205b6d30a1f1476d24", - "sourceCodeHash": "0x3206f4640662183bfbcd9c183b0d1c5d67c3ea670143daa731f23f62c0ae0855" + "sourceCodeHash": "0xc64e7f9719edf94a83ac8854b6236c451b8a0fb0e998621c41f4f1c94b5e46d3" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol index 95ae5bc071a6..ea27b526af95 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol @@ -18,7 +18,7 @@ contract OptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { /// This is used to keep track of the token deployments. mapping(address superchainToken => address remoteToken) public deployments; - /// @notice Emitted when a OptimismSuperchainERC20 is deployed. + /// @notice Emitted when an OptimismSuperchainERC20 is deployed. /// @param superchainToken Address of the SuperchainERC20 deployment. /// @param remoteToken Address of the corresponding token on the remote chain. /// @param deployer Address of the account that deployed the token. @@ -45,20 +45,16 @@ contract OptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { external returns (address _superchainERC20) { - // Encode the `initialize` call data for the OptimismSuperchainERC20. bytes memory initCallData = abi.encodeCall(OptimismSuperchainERC20.initialize, (_remoteToken, _name, _symbol, _decimals)); - // Encode the BeaconProxy creation code with the beacon contract address and the `initialize` call data. bytes memory creationCode = bytes.concat( type(BeaconProxy).creationCode, abi.encode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON, initCallData) ); - // Use CREATE3 for deterministic deployment. bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); _superchainERC20 = CREATE3.deploy({ salt: salt, creationCode: creationCode, value: 0 }); - // Store OptimismSuperchainERC20 and remote token addresses deployments[_superchainERC20] = _remoteToken; emit OptimismSuperchainERC20Created(_superchainERC20, _remoteToken, msg.sender); From 82062b4705ba7162ff29abcbc5b972a6a27fc427 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:28:06 -0300 Subject: [PATCH 12/14] feat: add constructor to superchain ERC20 beacon (#34) --- .../contracts-bedrock/scripts/L2Genesis.s.sol | 21 ++++- packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismSuperchainERC20Beacon.json | 13 ++- .../src/L2/OptimismSuperchainERC20Beacon.sol | 16 ++-- .../src/libraries/Predeploys.sol | 2 +- .../contracts-bedrock/test/L2Genesis.t.sol | 14 ++-- .../contracts-bedrock/test/Predeploys.t.sol | 80 +++++++++++-------- 7 files changed, 95 insertions(+), 55 deletions(-) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index efa7baf1fa79..326f324cdd92 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -26,6 +26,7 @@ import { L1StandardBridge } from "src/L1/L1StandardBridge.sol"; import { FeeVault } from "src/universal/FeeVault.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Process } from "scripts/libraries/Process.sol"; +import { OptimismSuperchainERC20Beacon } from "src/L2/OptimismSuperchainERC20Beacon.sol"; interface IInitializable { function initialize(address _addr) external; @@ -513,10 +514,24 @@ contract L2Genesis is Deployer { _setImplementationCode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); } - /// @notice This predeploy is following the safety invariant #1. - /// This contract has no initializer. + /// @notice This predeploy is following the safety invariant #2. function setOptimismSuperchainERC20Beacon() internal { - _setImplementationCode(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); + // TODO: Precalculate the address of the implementation contract + address superchainERC20Impl = address(uint160(uint256(keccak256("OptimismSuperchainERC20")))); + console.log("Setting %s implementation at: %s", "OptimismSuperchainERC20", superchainERC20Impl); + vm.etch(superchainERC20Impl, vm.getDeployedCode("OptimismSuperchainERC20.sol:OptimismSuperchainERC20")); + + OptimismSuperchainERC20Beacon beacon = new OptimismSuperchainERC20Beacon(superchainERC20Impl); + console.log( + "Setting %s implementation at: %s", + "OptimismSuperchainERC20Beacon", + Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON + ); + vm.etch(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON, address(beacon).code); + + /// Reset so its not included state dump + vm.etch(address(beacon), ""); + vm.resetNonce(address(beacon)); } /// @notice Sets all the preinstalls. diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index bbfa7aeaae20..a99b8907e6ba 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -120,8 +120,8 @@ "sourceCodeHash": "0x6943d40010dcbd1d51dc3668d0a154fbb1568ea49ebcf3aa039d65ef6eab321b" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { - "initCodeHash": "0xc9893a1ba9b6354ccbdc96918510c9402a0934a59f0cc4a6165955a8e8ee1f7d", - "sourceCodeHash": "0xd069f4bddd08c1435d286819e1bb765648a5ad861fa327547f52de65a372769e" + "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", + "sourceCodeHash": "0x00eb41c15cf548dfb6425f317d85262c1edd5f1ad2386a6a76695781d158cf16" }, "src/L2/OptimismSuperchainERC20Factory.sol": { "initCodeHash": "0x98011045722178751e4a1112892f7d9a11bc1f5e42ac18205b6d30a1f1476d24", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json index a06b5b5d140e..0bdfc64ed2fe 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Beacon.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "address", + "name": "_implementation", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "inputs": [], "name": "implementation", @@ -9,7 +20,7 @@ "type": "address" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol index 7a19e1e9af9b..2c00410c29eb 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Beacon.sol @@ -1,24 +1,26 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.15; -import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; import { ISemver } from "src/universal/ISemver.sol"; -/// @custom:proxied /// @custom:predeployed 0x4200000000000000000000000000000000000027 /// @title OptimismSuperchainERC20Beacon /// @notice OptimismSuperchainERC20Beacon is the beacon proxy for the OptimismSuperchainERC20 implementation. contract OptimismSuperchainERC20Beacon is IBeacon, ISemver { - /// TODO: Replace with real implementation address /// @notice Address of the OptimismSuperchainERC20 implementation. - address internal constant IMPLEMENTATION_ADDRESS = 0x0000000000000000000000000000000000000000; + address internal immutable IMPLEMENTATION; /// @notice Semantic version. /// @custom:semver 1.0.0-beta.1 string public constant version = "1.0.0-beta.1"; + constructor(address _implementation) { + IMPLEMENTATION = _implementation; + } + /// @inheritdoc IBeacon - function implementation() external pure override returns (address) { - return IMPLEMENTATION_ADDRESS; + function implementation() external view override returns (address) { + return IMPLEMENTATION; } } diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 9e252d52d1b7..26a0b7f95603 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -136,7 +136,7 @@ library Predeploys { /// @notice Returns true if the predeploy is not proxied. function notProxied(address _addr) internal pure returns (bool) { - return _addr == GOVERNANCE_TOKEN || _addr == WETH; + return _addr == GOVERNANCE_TOKEN || _addr == WETH || _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON; } /// @notice Returns true if the address is a defined predeploy that is embedded into new OP-Stack chains. diff --git a/packages/contracts-bedrock/test/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2Genesis.t.sol index 882de4adeb53..3a7abb9f7f5a 100644 --- a/packages/contracts-bedrock/test/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/L2Genesis.t.sol @@ -147,18 +147,18 @@ contract L2GenesisTest is Test { genesis.setPredeployProxies(); genesis.writeGenesisAllocs(_path); - // 2 predeploys do not have proxies - assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); + // 3 predeploys do not have proxies + assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 3); - // 23 proxies have the implementation set if useInterop is true and 17 if useInterop is false - assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 23 : 17); + // 22 proxies have the implementation set if useInterop is true and 17 if useInterop is false + assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 22 : 17); - // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin + // All proxies except 3 have the proxy 1967 admin slot set to the proxy admin assertEq( getPredeployCountWithSlotSetToValue( _path, Constants.PROXY_OWNER_ADDRESS, bytes32(uint256(uint160(Predeploys.PROXY_ADMIN))) ), - Predeploys.PREDEPLOY_COUNT - 2 + Predeploys.PREDEPLOY_COUNT - 3 ); // Also see Predeploys.t.test_predeploysSet_succeeds which uses L1Genesis for the CommonTest prestate. @@ -185,7 +185,7 @@ contract L2GenesisTest is Test { genesis.writeGenesisAllocs(_path); uint256 expected = 0; - expected += 2048 - 2; // predeploy proxies + expected += 2048 - 3; // predeploy proxies expected += 21; // predeploy implementations (excl. legacy erc20-style eth and legacy message sender) expected += 256; // precompiles expected += 12; // preinstalls diff --git a/packages/contracts-bedrock/test/Predeploys.t.sol b/packages/contracts-bedrock/test/Predeploys.t.sol index d39eb8b0ff61..0d9e4879fd6b 100644 --- a/packages/contracts-bedrock/test/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/Predeploys.t.sol @@ -7,26 +7,21 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; /// @title PredeploysTest -contract PredeploysTest is CommonTest { +contract PredeploysBaseTest is CommonTest { ////////////////////////////////////////////////////// /// Internal helpers ////////////////////////////////////////////////////// - /// @dev Returns true if the address is a predeploy that is active (i.e. embedded in L2 genesis). - function _isPredeploy(address _addr) internal pure returns (bool) { - return _addr == Predeploys.L2_TO_L1_MESSAGE_PASSER || _addr == Predeploys.L2_CROSS_DOMAIN_MESSENGER - || _addr == Predeploys.L2_STANDARD_BRIDGE || _addr == Predeploys.L2_ERC721_BRIDGE - || _addr == Predeploys.SEQUENCER_FEE_WALLET || _addr == Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY - || _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.L1_BLOCK_ATTRIBUTES - || _addr == Predeploys.GAS_PRICE_ORACLE || _addr == Predeploys.DEPLOYER_WHITELIST || _addr == Predeploys.WETH - || _addr == Predeploys.L1_BLOCK_NUMBER || _addr == Predeploys.LEGACY_MESSAGE_PASSER - || _addr == Predeploys.PROXY_ADMIN || _addr == Predeploys.BASE_FEE_VAULT || _addr == Predeploys.L1_FEE_VAULT - || _addr == Predeploys.GOVERNANCE_TOKEN || _addr == Predeploys.SCHEMA_REGISTRY || _addr == Predeploys.EAS; + /// @dev Returns true if the address is an interop predeploy. + function _isInterop(address _addr) internal pure returns (bool) { + return _addr == Predeploys.CROSS_L2_INBOX || _addr == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER + || _addr == Predeploys.SUPERCHAIN_WETH || _addr == Predeploys.ETH_LIQUIDITY + || _addr == Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY || _addr == Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON; } - /// @dev Returns true if the address is not proxied. - function _notProxied(address _addr) internal pure returns (bool) { - return _addr == Predeploys.GOVERNANCE_TOKEN || _addr == Predeploys.WETH; + /// @dev Returns true if the address is a predeploy that has a different code in the interop mode. + function _interopCodeDiffer(address _addr) internal pure returns (bool) { + return _addr == Predeploys.L1_BLOCK_ATTRIBUTES || _addr == Predeploys.L2_STANDARD_BRIDGE; } /// @dev Returns true if the account is not meant to be in the L2 genesis anymore. @@ -34,22 +29,17 @@ contract PredeploysTest is CommonTest { return _addr == Predeploys.L1_MESSAGE_SENDER; } + /// @dev Returns true if the predeploy is initializable. function _isInitializable(address _addr) internal pure returns (bool) { - return !( - _addr == Predeploys.LEGACY_MESSAGE_PASSER || _addr == Predeploys.DEPLOYER_WHITELIST - || _addr == Predeploys.GAS_PRICE_ORACLE || _addr == Predeploys.SEQUENCER_FEE_WALLET - || _addr == Predeploys.BASE_FEE_VAULT || _addr == Predeploys.L1_FEE_VAULT - || _addr == Predeploys.L1_BLOCK_NUMBER || _addr == Predeploys.L1_BLOCK_ATTRIBUTES - || _addr == Predeploys.L2_TO_L1_MESSAGE_PASSER || _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY - || _addr == Predeploys.PROXY_ADMIN || _addr == Predeploys.SCHEMA_REGISTRY || _addr == Predeploys.EAS - || _addr == Predeploys.GOVERNANCE_TOKEN - ); + return _addr == Predeploys.L2_CROSS_DOMAIN_MESSENGER || _addr == Predeploys.L2_STANDARD_BRIDGE + || _addr == Predeploys.L2_ERC721_BRIDGE || _addr == Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY; } + /// @dev Returns true if the predeploy uses immutables. function _usesImmutables(address _addr) internal pure returns (bool) { return _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.SEQUENCER_FEE_WALLET || _addr == Predeploys.BASE_FEE_VAULT || _addr == Predeploys.L1_FEE_VAULT || _addr == Predeploys.EAS - || _addr == Predeploys.GOVERNANCE_TOKEN; + || _addr == Predeploys.GOVERNANCE_TOKEN || _addr == Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON; } function test_predeployToCodeNamespace() external pure { @@ -67,9 +57,7 @@ contract PredeploysTest is CommonTest { ); } - /// @dev Tests that the predeploy addresses are set correctly. They have code - /// and the proxied accounts have the correct admin. - function test_predeploys_succeeds() external { + function _test_predeploys(bool _useInterop) internal { uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; @@ -77,23 +65,25 @@ contract PredeploysTest is CommonTest { for (uint256 i = 0; i < count; i++) { address addr = address(prefix | uint160(i)); - bytes memory code = addr.code; - assertTrue(code.length > 0); - address implAddr = Predeploys.predeployToCodeNamespace(addr); if (_isOmitted(addr)) { assertEq(implAddr.code.length, 0, "must have no code"); continue; } - bool isPredeploy = _isPredeploy(addr); + + bool isPredeploy = Predeploys.isSupportedPredeploy(addr, _useInterop); + + bytes memory code = addr.code; + if (isPredeploy) assertTrue(code.length > 0); + + bool proxied = Predeploys.notProxied(addr) == false; if (!isPredeploy) { // All of the predeploys, even if inactive, have their admin set to the proxy admin - assertEq(EIP1967Helper.getAdmin(addr), Predeploys.PROXY_ADMIN, "Admin mismatch"); + if (proxied) assertEq(EIP1967Helper.getAdmin(addr), Predeploys.PROXY_ADMIN, "Admin mismatch"); continue; } - bool proxied = _notProxied(addr) == false; string memory cname = Predeploys.getName(addr); assertNotEq(cname, "", "must have a name"); @@ -118,7 +108,7 @@ contract PredeploysTest is CommonTest { string.concat("Implementation mismatch for ", vm.toString(addr)) ); assertNotEq(implAddr.code.length, 0, "predeploy implementation account must have code"); - if (!_usesImmutables(addr)) { + if (!_usesImmutables(addr) && !_interopCodeDiffer(addr)) { // can't check bytecode if it's modified with immutables in genesis. assertEq(implAddr.code, supposedCode, "proxy implementation contract should match contract source"); } @@ -129,3 +119,25 @@ contract PredeploysTest is CommonTest { } } } + +contract PredeploysTest is PredeploysBaseTest { + /// @dev Tests that the predeploy addresses are set correctly. They have code + /// and the proxied accounts have the correct admin. + function test_predeploys_succeeds() external { + _test_predeploys(false); + } +} + +contract PredeploysInteropTest is PredeploysBaseTest { + /// @notice Test setup. Enabling interop to get all predeploys. + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + /// @dev Tests that the predeploy addresses are set correctly. They have code + /// and the proxied accounts have the correct admin. Using interop. + function test_predeploys_succeeds() external { + _test_predeploys(true); + } +} From d75694845155259ed4045052b460e66fb43ee8d2 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 30 Aug 2024 15:45:53 -0300 Subject: [PATCH 13/14] test: remove factory predeploy etch ---------- Co-authored-by: 0xng Co-authored-by: 0xParticle Co-authored-by: gotzenx <78360669+gotzenx@users.noreply.github.com> --- .../contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index 3be3101fa442..d9d91624daa8 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -26,10 +26,6 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { function setUp() public virtual override { super.enableInterop(); super.setUp(); - - // TODO: Remove it once the `OptimismSuperchainERC20Factory` is added to predeploys. - // Ensure OPTIMISM_SUPERCHAIN_ERC20_FACTORY's code is not empty. - vm.etch(Predeploys.predeployToCodeNamespace(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY), address(this).code); } /// @notice Helper function to setup a mock and expect a call to it. From 9a43e01cca9bb8a6dc6c7b19b6521f4116b5e4f1 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 30 Aug 2024 16:47:20 -0300 Subject: [PATCH 14/14] fix: set an arbitrary address for superchain erc20 impl --- packages/contracts-bedrock/scripts/L2Genesis.s.sol | 3 +-- packages/contracts-bedrock/src/libraries/Predeploys.sol | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 326f324cdd92..759a802966f1 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -516,8 +516,7 @@ contract L2Genesis is Deployer { /// @notice This predeploy is following the safety invariant #2. function setOptimismSuperchainERC20Beacon() internal { - // TODO: Precalculate the address of the implementation contract - address superchainERC20Impl = address(uint160(uint256(keccak256("OptimismSuperchainERC20")))); + address superchainERC20Impl = Predeploys.OPTIMISM_SUPERCHAIN_ERC20; console.log("Setting %s implementation at: %s", "OptimismSuperchainERC20", superchainERC20Impl); vm.etch(superchainERC20Impl, vm.getDeployedCode("OptimismSuperchainERC20.sol:OptimismSuperchainERC20")); diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 26a0b7f95603..5b7e5f410fa9 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -101,6 +101,10 @@ library Predeploys { /// @notice Address of the OptimismSuperchainERC20Beacon predeploy. address internal constant OPTIMISM_SUPERCHAIN_ERC20_BEACON = 0x4200000000000000000000000000000000000027; + // TODO: Precalculate the address of the implementation contract + /// @notice Arbitrary address of the OptimismSuperchainERC20 implementation contract. + address internal constant OPTIMISM_SUPERCHAIN_ERC20 = 0xB9415c6cA93bdC545D4c5177512FCC22EFa38F28; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy");