diff --git a/scripts/checks/interfaces/main.go b/scripts/checks/interfaces/main.go index 9274ad843..ddff55d51 100644 --- a/scripts/checks/interfaces/main.go +++ b/scripts/checks/interfaces/main.go @@ -24,8 +24,12 @@ func normalizeInternalType(internalType string) string { var excludeContracts = []string{ "IProxy", "IEIP712", "IEAS", "ISchemaResolver", "ISchemaRegistry", + // ERC-165 interfaces that bridges check deployed tokens against. Must never become ABI mirrors, + // since any change to their functions changes the interface ID and strands existing tokens. + "IOptimismMintableERC20", + // TODO: Interfaces that need to be fixed - "IInitializable", "IOptimismMintableERC20", "IResolvedDelegateProxy", + "IInitializable", "IResolvedDelegateProxy", } // excludeSourceContracts is a list of contracts that are allowed to not have interfaces diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index b4702b3f4..3e786cfbb 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -80,8 +80,8 @@ "sourceCodeHash": "0x12ea125038b87e259a0d203e119faa6e9726ab2bdbc30430f820ccd48fe87e14" }, "src/L2/L2ERC721Bridge.sol:L2ERC721Bridge": { - "initCodeHash": "0xe9d79ab92871ff7d1336059a8756041e052bca05f2c0e98d20ed97f545162191", - "sourceCodeHash": "0x032bda19a2c0a05e8abfdac182982ad14256cbcebc1896ba503bb1a2a681e81f" + "initCodeHash": "0x6e3e9c0aee26dcdf2e866a076e04322c9fe358ed1b0cc278c3ebdd23227dd868", + "sourceCodeHash": "0x6f1e82203041282f13949525c092d1ce1221df5d29429148e3c1b20b20aa9f99" }, "src/L2/L2StandardBridge.sol:L2StandardBridge": { "initCodeHash": "0x61f1e19928b92a5455d04c990071728b006d2083887250f473f147cd9002345f", @@ -96,12 +96,12 @@ "sourceCodeHash": "0xd6e94bc9df025855916aa4184d0bc739b0fbe786dfd037b99dbb51d0d3e46918" }, "src/L2/OptimismMintableERC721.sol:OptimismMintableERC721": { - "initCodeHash": "0x363dcb59140ecfbcdff7b5f4e822f0fdc8492fc02b6d9abf3cb822ae75e8dcfb", - "sourceCodeHash": "0xe9271c174be0579d59221bdcd66e408c0327a72a7898175bf3df6919863ef1cb" + "initCodeHash": "0x2b0a211f46ad957a142a73d61760809494dc212cd0c366ca5407f24f211ea810", + "sourceCodeHash": "0x8ae7768566b70e28bd2529c070831095b4087802f9012a0e95121f21eec9e4cb" }, "src/L2/OptimismMintableERC721Factory.sol:OptimismMintableERC721Factory": { - "initCodeHash": "0x3f31209e30a79f367b59b66e3ee6f43f5916ec26e6d1c5a08846d636884f020d", - "sourceCodeHash": "0xb0be3deac32956251adb37d3ca61f619ca4348a1355a41c856a3a95adde0e4ff" + "initCodeHash": "0x4a584d196f965d239cf31f63b54fde68dab1dc151b5e4b700380102380abe707", + "sourceCodeHash": "0x93ca86cc152da8633ceb9f35e171b418bd4d90e52ed05ccde3e1026039272cb4" }, "src/L2/SequencerFeeVault.sol:SequencerFeeVault": { "initCodeHash": "0xd80f5cee90a78db1f7a506d5b6eaa22ff672a78cb2fd7ce9b7130c1ce571ffa9", diff --git a/src/L2/L2ERC721Bridge.sol b/src/L2/L2ERC721Bridge.sol index 4c34051a1..b58408366 100644 --- a/src/L2/L2ERC721Bridge.sol +++ b/src/L2/L2ERC721Bridge.sol @@ -7,6 +7,7 @@ import { ERC721Bridge } from "src/universal/ERC721Bridge.sol"; // Libraries import { ERC165Checker } from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -24,8 +25,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// **WARNING**: Do not bridge an ERC721 that was originally deployed on Optimism. This /// bridge ONLY supports ERC721s originally deployed on Ethereum. contract L2ERC721Bridge is ERC721Bridge, ISemver { - /// @custom:semver 1.10.0 - string public constant version = "1.10.0"; + /// @custom:semver 1.11.0 + string public constant version = "1.11.0"; /// @notice Constructs the L2ERC721Bridge contract. constructor() ERC721Bridge() { @@ -67,7 +68,11 @@ contract L2ERC721Bridge is ERC721Bridge, ISemver { // Note that supportsInterface makes a callback to the _localToken address which is user // provided. require( - ERC165Checker.supportsInterface(_localToken, type(IOptimismMintableERC721).interfaceId), + ERC165Checker.supportsInterface(_localToken, Constants.OPTIMISM_MINTABLE_ERC721_INTERFACE_ID) + || ERC165Checker.supportsInterface(_localToken, Constants.BEDROCK_OPTIMISM_MINTABLE_ERC721_INTERFACE_ID) + || ERC165Checker.supportsInterface( + _localToken, Constants.PRE_BEDROCK_OPTIMISM_MINTABLE_ERC721_INTERFACE_ID + ), "L2ERC721Bridge: local token interface is not compliant" ); diff --git a/src/L2/OptimismMintableERC721.sol b/src/L2/OptimismMintableERC721.sol index 91a31c0e8..4057e4af6 100644 --- a/src/L2/OptimismMintableERC721.sol +++ b/src/L2/OptimismMintableERC721.sol @@ -7,10 +7,10 @@ import { ERC721 } from "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721 // Libraries import { Strings } from "lib/openzeppelin-contracts/contracts/utils/Strings.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IOptimismMintableERC721 } from "interfaces/L2/IOptimismMintableERC721.sol"; /// @title OptimismMintableERC721 /// @notice This contract is the remote representation for some token that lives on another network, @@ -46,8 +46,8 @@ contract OptimismMintableERC721 is ERC721Enumerable, ISemver { } /// @notice Semantic version. - /// @custom:semver 1.3.2 - string public constant version = "1.3.2"; + /// @custom:semver 1.4.0 + string public constant version = "1.4.0"; /// @param _bridge Address of the bridge on this network. /// @param _remoteChainId Chain ID where the remote token is deployed. @@ -122,8 +122,9 @@ contract OptimismMintableERC721 is ERC721Enumerable, ISemver { /// @param _interfaceId The interface ID to check. /// @return True if the interface ID is supported, false otherwise. function supportsInterface(bytes4 _interfaceId) public view override(ERC721Enumerable) returns (bool) { - bytes4 iface = type(IOptimismMintableERC721).interfaceId; - return _interfaceId == iface || super.supportsInterface(_interfaceId); + return _interfaceId == Constants.OPTIMISM_MINTABLE_ERC721_INTERFACE_ID + || _interfaceId == Constants.BEDROCK_OPTIMISM_MINTABLE_ERC721_INTERFACE_ID + || super.supportsInterface(_interfaceId); } /// @notice Returns the base token URI. diff --git a/src/L2/OptimismMintableERC721Factory.sol b/src/L2/OptimismMintableERC721Factory.sol index 3838e5905..728d8ef46 100644 --- a/src/L2/OptimismMintableERC721Factory.sol +++ b/src/L2/OptimismMintableERC721Factory.sol @@ -30,8 +30,8 @@ contract OptimismMintableERC721Factory is ISemver { event OptimismMintableERC721Created(address indexed localToken, address indexed remoteToken, address deployer); /// @notice Semantic version. - /// @custom:semver 1.4.2 - string public constant version = "1.4.2"; + /// @custom:semver 1.5.0 + string public constant version = "1.5.0"; /// @notice The semver MUST be bumped any time that there is a change in /// the OptimismMintableERC721 token contract since this contract diff --git a/src/libraries/Constants.sol b/src/libraries/Constants.sol index 50ad71669..c2c3e0182 100644 --- a/src/libraries/Constants.sol +++ b/src/libraries/Constants.sol @@ -42,6 +42,21 @@ library Constants { /// transactions. address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + /// @notice ERC-165 interface ID of OptimismMintableERC721 tokens built from source between + /// September 2022 and Bedrock. Those tokens can't be upgraded, so this ID must be + /// accepted forever. + bytes4 internal constant PRE_BEDROCK_OPTIMISM_MINTABLE_ERC721_INTERFACE_ID = 0xe49bc7f8; + + /// @notice ERC-165 interface ID of OptimismMintableERC721 tokens built from source between + /// Bedrock and op-contracts v1.8.0. Those tokens can't be upgraded, so this ID must be + /// accepted forever. Newer tokens answer it too, so that older bridges accept them. + bytes4 internal constant BEDROCK_OPTIMISM_MINTABLE_ERC721_INTERFACE_ID = 0x74259ebf; + + /// @notice ERC-165 interface ID answered by OptimismMintableERC721 tokens built from + /// op-contracts v1.8.0 source onwards. Written as a literal because + /// type(IOptimismMintableERC721) changes whenever the contract's ABI does. + bytes4 internal constant OPTIMISM_MINTABLE_ERC721_INTERFACE_ID = 0xaecafc23; + /// @notice Returns the default values for the ResourceConfig. These are the recommended values /// for a production network. function DEFAULT_RESOURCE_CONFIG() internal pure returns (IResourceMetering.ResourceConfig memory) { diff --git a/test/L2/L2ERC721Bridge.t.sol b/test/L2/L2ERC721Bridge.t.sol index 07f15b0cb..3e2ed80b6 100644 --- a/test/L2/L2ERC721Bridge.t.sol +++ b/test/L2/L2ERC721Bridge.t.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Contracts +import { ERC721 } from "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import { OptimismMintableERC721 } from "src/L2/OptimismMintableERC721.sol"; // Interfaces @@ -23,6 +24,31 @@ contract L2ERC721Bridge_TestMintableERC721_Harness is OptimismMintableERC721 { } } +/// @title L2ERC721Bridge_FixedInterfaceIdERC721_Harness +/// @notice An OptimismMintableERC721 stand-in that answers a single, fixed interface ID, like a +/// token deployed by an earlier factory that can't be upgraded. +contract L2ERC721Bridge_FixedInterfaceIdERC721_Harness is ERC721 { + address internal immutable REMOTE_TOKEN; + bytes4 internal immutable INTERFACE_ID; + + constructor(address _remoteToken, bytes4 _interfaceId) ERC721("Test", "TST") { + REMOTE_TOKEN = _remoteToken; + INTERFACE_ID = _interfaceId; + } + + function remoteToken() external view returns (address) { + return REMOTE_TOKEN; + } + + function safeMint(address _to, uint256 _tokenId) external { + _safeMint(_to, _tokenId); + } + + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == INTERFACE_ID || super.supportsInterface(_interfaceId); + } +} + /// @title NonCompliantERC721 /// @notice A non-compliant ERC721 token that does not implement the full ERC721 interface. /// This is used to test that the bridge will revert if the token does not claim to @@ -166,6 +192,35 @@ contract L2ERC721Bridge_FinalizeBridgeERC721_Test is L2ERC721Bridge_Bridge_TestI _assertTokenOwnedBy(alice); } + /// @notice Tests that `finalizeBridgeERC721` mints a token that only answers the interface ID + /// of tokens built from source between Bedrock and op-contracts v1.8.0. + function test_finalizeBridgeERC721_bedrockInterfaceId_succeeds() external { + _finalizeBridgeERC721ToFixedInterfaceIdToken(0x74259ebf); + } + + /// @notice Tests that `finalizeBridgeERC721` mints a token that only answers the interface ID + /// of tokens built from source between September 2022 and Bedrock. + function test_finalizeBridgeERC721_preBedrockInterfaceId_succeeds() external { + _finalizeBridgeERC721ToFixedInterfaceIdToken(0xe49bc7f8); + } + + /// @notice Tests that `finalizeBridgeERC721` mints a token that only answers the interface ID + /// of tokens built from op-contracts v1.8.0 source onwards. + function test_finalizeBridgeERC721_currentInterfaceId_succeeds() external { + _finalizeBridgeERC721ToFixedInterfaceIdToken(0xaecafc23); + } + + function _finalizeBridgeERC721ToFixedInterfaceIdToken(bytes4 _interfaceId) internal { + L2ERC721Bridge_FixedInterfaceIdERC721_Harness token = + new L2ERC721Bridge_FixedInterfaceIdERC721_Harness(remoteToken, _interfaceId); + + _mockOtherBridge(); + vm.prank(address(l2ERC721Bridge.messenger())); + l2ERC721Bridge.finalizeBridgeERC721(address(token), remoteToken, alice, alice, tokenId, EXTRA_DATA); + + assertEq(token.ownerOf(tokenId), alice); + } + /// @notice Tests that `finalizeBridgeERC721` reverts if the token is not compliant with the /// `IOptimismMintableERC721` interface. function test_finalizeBridgeERC721_interfaceNotCompliant_reverts() external { diff --git a/test/L2/OptimismMintableERC721.t.sol b/test/L2/OptimismMintableERC721.t.sol index 0783be4a6..e3869d65d 100644 --- a/test/L2/OptimismMintableERC721.t.sol +++ b/test/L2/OptimismMintableERC721.t.sol @@ -6,7 +6,7 @@ import { IERC721Enumerable } from "lib/openzeppelin-contracts/contracts/token/ER import { IERC165 } from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import { Strings } from "lib/openzeppelin-contracts/contracts/utils/Strings.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -import { OptimismMintableERC721, IOptimismMintableERC721 } from "src/L2/OptimismMintableERC721.sol"; +import { OptimismMintableERC721 } from "src/L2/OptimismMintableERC721.sol"; /// @title OptimismMintableERC721_TestInit /// @notice Reusable test initialization for `OptimismMintableERC721` tests. @@ -133,10 +133,12 @@ contract OptimismMintableERC721_Burn_Test is OptimismMintableERC721_TestInit { /// @title OptimismMintableERC721_SupportsInterface_Test /// @notice Tests the `supportsInterface` function of the `OptimismMintableERC721` contract. contract OptimismMintableERC721_SupportsInterface_Test is OptimismMintableERC721_TestInit { - /// @notice Tests that the `supportsInterface` function returns true for - /// IOptimismMintableERC721, IERC721Enumerable, IERC721 and IERC165 interfaces. + /// @notice Tests that the `supportsInterface` function returns true for both + /// IOptimismMintableERC721 interface IDs, IERC721Enumerable, IERC721 and IERC165. function test_supportsInterface_succeeds() external view { - assertTrue(L2NFT.supportsInterface(type(IOptimismMintableERC721).interfaceId)); + // Bridges check tokens against these IDs, so they must never change. + assertTrue(L2NFT.supportsInterface(0x74259ebf)); + assertTrue(L2NFT.supportsInterface(0xaecafc23)); assertTrue(L2NFT.supportsInterface(type(IERC721Enumerable).interfaceId)); assertTrue(L2NFT.supportsInterface(type(IERC721).interfaceId)); assertTrue(L2NFT.supportsInterface(type(IERC165).interfaceId)); diff --git a/test/universal/OptimismMintableERC20.t.sol b/test/universal/OptimismMintableERC20.t.sol index 4b0475b96..3b95e96be 100644 --- a/test/universal/OptimismMintableERC20.t.sol +++ b/test/universal/OptimismMintableERC20.t.sol @@ -86,6 +86,13 @@ contract OptimismMintableERC20_SupportsInterface_Test is OptimismMintableERC20_T assertTrue(L2Token.supportsInterface(type(IERC165).interfaceId)); assertTrue(L2Token.supportsInterface(type(ILegacyMintableERC20).interfaceId)); assertTrue(L2Token.supportsInterface(type(IOptimismMintableERC20).interfaceId)); + + // Bridges check deployed tokens against these two IDs, so changing either one strands + // every existing token. + // l1Token() ^ mint(address,uint256) ^ burn(address,uint256) + assertEq(type(ILegacyMintableERC20).interfaceId, bytes4(0x1d1d8b63)); + // remoteToken() ^ bridge() ^ mint(address,uint256) ^ burn(address,uint256) + assertEq(type(IOptimismMintableERC20).interfaceId, bytes4(0xec4fc8e3)); } }