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] 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({