From 5542ee9c4d8bda1e730e60195ecc70656bd75ce2 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 17:41:01 -0300 Subject: [PATCH 1/3] test: add L2 standard bridge interop unit tests --- .../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 | 359 +++++++++ .../contracts-bedrock/test/L2Genesis.t.sol | 4 +- .../contracts-bedrock/test/setup/Setup.sol | 4 +- .../test/vendor/Initializable.t.sol | 8 + 9 files changed, 1127 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..0c7186502b33 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -0,0 +1,359 @@ +// 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 Mock ERC20 decimals + function _mockDecimals(address _token, uint8 _decimals) internal { + vm.mockCall(_token, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimals)); + } + + /// @notice Mock ERC165 interface + function _mockInterface(address _token, bytes4 _interfaceId, bool _supported) internal { + vm.mockCall( + _token, abi.encodeWithSelector(IERC165.supportsInterface.selector, _interfaceId), abi.encode(_supported) + ); + } + + /// @notice Mock factory deployment + function _mockDeployments(address _factory, address _token, address _deployed) internal { + vm.mockCall( + _factory, + abi.encodeWithSelector(IOptimismMintableERC20Factory.deployments.selector, _token), + abi.encode(_deployed) + ); + } +} + +contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_Test { + 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_differenteDecimals_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); + + // Mock the `burn` and `mint` functions + vm.mockCall(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), abi.encode()); + vm.mockCall(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); + + // Expect the `Converted` event to be emitted + vm.expectEmit(true, true, true, true, address(l2StandardBridge)); + emit Converted(_from, _to, _caller, _amount); + + // Expect the `mint` and `burn` functions to be called + vm.expectCall(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), 1); + vm.expectCall(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), 1); + + // Act + vm.prank(_caller); + l2StandardBridge.convert(_from, _to, _amount); + } +} + +contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_Test { + 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); + + // Mock `_to` to be a legacy address + _mockInterface(_to, type(IERC165).interfaceId, true); + _mockInterface(_to, type(IOptimismMintableERC20).interfaceId, true); + } + + /// @notice Test that the `convert` function with different decimals reverts + function testFuzz_convert_differenteDecimals_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); + + // Mock the `burn` and `mint` functions + vm.mockCall(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), abi.encode()); + vm.mockCall(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); + + // Expect the `Converted` event to be emitted + vm.expectEmit(true, true, true, true, address(l2StandardBridge)); + emit Converted(_from, _to, _caller, _amount); + + // Expect the `mint` and `burn` functions to be called + vm.expectCall(_from, abi.encodeWithSelector(MintableAndBurnable.burn.selector, _caller, _amount), 1); + vm.expectCall(_to, abi.encodeWithSelector(MintableAndBurnable.mint.selector, _caller, _amount), 1); + + // 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 ef1785f038bab7840221efc82f2a636f73b374f1 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 17:44:03 -0300 Subject: [PATCH 2/3] fix: add tests natspec --- .../contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index 0c7186502b33..e2f4beff54f0 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -48,6 +48,7 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { } } +/// @notice Test when converting from a legacy token to a SuperchainERC20 token contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_Test { function _setUpLegacyToSuper(address _from, address _to) internal { // Assume @@ -203,6 +204,7 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T } } +/// @notice Test when converting from a SuperchainERC20 token to a legacy token contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_Test { function _setUpSuperToLegacy(address _from, address _to) internal { // Assume From a6b9fcbbd9e92c0601206190e35ae3145de27bc2 Mon Sep 17 00:00:00 2001 From: agusduha Date: Wed, 7 Aug 2024 10:54:19 -0300 Subject: [PATCH 3/3] 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 e2f4beff54f0..091f7b35d61c 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"; @@ -41,9 +41,7 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { /// @notice Mock factory deployment function _mockDeployments(address _factory, address _token, address _deployed) internal { vm.mockCall( - _factory, - abi.encodeWithSelector(IOptimismMintableERC20Factory.deployments.selector, _token), - abi.encode(_deployed) + _factory, abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector, _token), abi.encode(_deployed) ); } }