diff --git a/.changeset/happy-carpets-rush.md b/.changeset/happy-carpets-rush.md new file mode 100644 index 0000000000000000000000000000000000000000..249dbedbb6d30615a3b9c030a0826552e210e829 --- /dev/null +++ b/.changeset/happy-carpets-rush.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts-periphery': patch +--- + +Simplify, cleanup, and standardize ERC721 bridge contracts. diff --git a/integration-tests/contracts/FakeL2StandardERC721.sol b/integration-tests/contracts/FakeOptimismMintableERC721.sol similarity index 56% rename from integration-tests/contracts/FakeL2StandardERC721.sol rename to integration-tests/contracts/FakeOptimismMintableERC721.sol index 30d1a2e898fa53c3b6917d924fd614795b1a346e..2108dbbfc373b86ca3c86ee39f6e962648a20a80 100644 --- a/integration-tests/contracts/FakeL2StandardERC721.sol +++ b/integration-tests/contracts/FakeOptimismMintableERC721.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.9; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -contract FakeL2StandardERC721 is ERC721 { +contract FakeOptimismMintableERC721 is ERC721 { - address public immutable l1Token; - address public immutable l2Bridge; + address public immutable remoteToken; + address public immutable bridge; - constructor(address _l1Token, address _l2Bridge) ERC721("FakeERC721", "FAKE") { - l1Token = _l1Token; - l2Bridge = _l2Bridge; + constructor(address _remoteToken, address _bridge) ERC721("FakeERC721", "FAKE") { + remoteToken = _remoteToken; + bridge = _bridge; } function mint(address to, uint256 tokenId) public { diff --git a/integration-tests/test/nft-bridge.spec.ts b/integration-tests/test/nft-bridge.spec.ts index 3ff1abaf5bf3f364fbe7d8b9aae30b185e8f55cb..74abda59f857b4e010086dba5ec060f4209a9eb8 100644 --- a/integration-tests/test/nft-bridge.spec.ts +++ b/integration-tests/test/nft-bridge.spec.ts @@ -5,8 +5,8 @@ import { predeploys } from '@eth-optimism/contracts' import Artifact__TestERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/testing/helpers/TestERC721.sol/TestERC721.json' import Artifact__L1ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L1/messaging/L1ERC721Bridge.sol/L1ERC721Bridge.json' import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/messaging/L2ERC721Bridge.sol/L2ERC721Bridge.json' -import Artifact__L2StandardERC721Factory from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/messaging/L2StandardERC721Factory.sol/L2StandardERC721Factory.json' -import Artifact__L2StandardERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/standards/L2StandardERC721.sol/L2StandardERC721.json' +import Artifact__OptimismMintableERC721Factory from '@eth-optimism/contracts-periphery/artifacts/contracts/universal/op-erc721/OptimismMintableERC721Factory.sol/OptimismMintableERC721Factory.json' +import Artifact__OptimismMintableERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/universal/op-erc721/OptimismMintableERC721.sol/OptimismMintableERC721.json' /* Imports: Internal */ import { expect } from './shared/setup' @@ -49,7 +49,7 @@ describe('ERC721 Bridge', () => { let Factory__L1ERC721: ContractFactory let Factory__L1ERC721Bridge: ContractFactory let Factory__L2ERC721Bridge: ContractFactory - let Factory__L2StandardERC721Factory: ContractFactory + let Factory__OptimismMintableERC721Factory: ContractFactory before(async () => { Factory__L1ERC721 = await ethers.getContractFactory( Artifact__TestERC721.abi, @@ -66,9 +66,9 @@ describe('ERC721 Bridge', () => { Artifact__L2ERC721Bridge.bytecode, bobWalletL2 ) - Factory__L2StandardERC721Factory = await ethers.getContractFactory( - Artifact__L2StandardERC721Factory.abi, - Artifact__L2StandardERC721Factory.bytecode, + Factory__OptimismMintableERC721Factory = await ethers.getContractFactory( + Artifact__OptimismMintableERC721Factory.abi, + Artifact__OptimismMintableERC721Factory.bytecode, bobWalletL2 ) }) @@ -76,48 +76,58 @@ describe('ERC721 Bridge', () => { let L1ERC721: Contract let L1ERC721Bridge: Contract let L2ERC721Bridge: Contract - let L2StandardERC721Factory: Contract - let L2StandardERC721: Contract + let OptimismMintableERC721Factory: Contract + let OptimismMintableERC721: Contract beforeEach(async () => { L1ERC721 = await Factory__L1ERC721.deploy() await L1ERC721.deployed() - L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy( - predeploys.L2CrossDomainMessenger - ) - await L2ERC721Bridge.deployed() - L1ERC721Bridge = await Factory__L1ERC721Bridge.deploy( env.messenger.contracts.l1.L1CrossDomainMessenger.address, - L2ERC721Bridge.address + ethers.utils.getContractAddress({ + from: await Factory__L2ERC721Bridge.signer.getAddress(), + nonce: await Factory__L2ERC721Bridge.signer.getTransactionCount(), + }) ) await L1ERC721Bridge.deployed() - L2StandardERC721Factory = await Factory__L2StandardERC721Factory.deploy( + L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy( + predeploys.L2CrossDomainMessenger, + L1ERC721Bridge.address + ) + await L2ERC721Bridge.deployed() + + OptimismMintableERC721Factory = + await Factory__OptimismMintableERC721Factory.deploy( + L2ERC721Bridge.address + ) + await OptimismMintableERC721Factory.deployed() + + expect(await L1ERC721Bridge.otherBridge()).to.equal(L2ERC721Bridge.address) + expect(await L2ERC721Bridge.otherBridge()).to.equal(L1ERC721Bridge.address) + + expect(await OptimismMintableERC721Factory.bridge()).to.equal( L2ERC721Bridge.address ) - await L2StandardERC721Factory.deployed() // Create a L2 Standard ERC721 with the Standard ERC721 Factory - const tx = await L2StandardERC721Factory.createStandardL2ERC721( - L1ERC721.address, - 'L2ERC721', - 'L2' - ) - await tx.wait() + const tx = + await OptimismMintableERC721Factory.createStandardOptimismMintableERC721( + L1ERC721.address, + 'L2ERC721', + 'L2' + ) + const receipt = await tx.wait() - // Retrieve the deployed L2 Standard ERC721 - const L2StandardERC721Address = - await L2StandardERC721Factory.standardERC721Mapping(L1ERC721.address) - L2StandardERC721 = await ethers.getContractAt( - Artifact__L2StandardERC721.abi, - L2StandardERC721Address - ) - await L2StandardERC721.deployed() + // Get the OptimismMintableERC721Created event + const erc721CreatedEvent = receipt.events[0] + expect(erc721CreatedEvent.event).to.be.eq('OptimismMintableERC721Created') - // Initialize the L2 bridge contract - const tx1 = await L2ERC721Bridge.initialize(L1ERC721Bridge.address) - await tx1.wait() + OptimismMintableERC721 = await ethers.getContractAt( + Artifact__OptimismMintableERC721.abi, + erc721CreatedEvent.args.localToken + ) + await OptimismMintableERC721.deployed() // Mint an L1 ERC721 to Bob on L1 const tx2 = await L1ERC721.mint(bobAddress, TOKEN_ID) @@ -128,11 +138,11 @@ describe('ERC721 Bridge', () => { await tx3.wait() }) - it('depositERC721', async () => { + it('bridgeERC721', async () => { await env.messenger.waitForMessageReceipt( - await L1ERC721Bridge.depositERC721( + await L1ERC721Bridge.bridgeERC721( L1ERC721.address, - L2StandardERC721.address, + OptimismMintableERC721.address, TOKEN_ID, FINALIZATION_GAS, NON_NULL_BYTES @@ -143,14 +153,14 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) // Bob owns the NFT on L2 - expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) + expect(await OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) }) - it('depositERC721To', async () => { + it('bridgeERC721To', async () => { await env.messenger.waitForMessageReceipt( - await L1ERC721Bridge.depositERC721To( + await L1ERC721Bridge.bridgeERC721To( L1ERC721.address, - L2StandardERC721.address, + OptimismMintableERC721.address, aliceAddress, TOKEN_ID, FINALIZATION_GAS, @@ -162,15 +172,17 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) // Alice owns the NFT on L2 - expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress) + expect(await OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal( + aliceAddress + ) }) - withdrawalTest('withdrawERC721', async () => { + withdrawalTest('bridgeERC721', async () => { // Deposit an NFT into L2 so that there's something to withdraw await env.messenger.waitForMessageReceipt( - await L1ERC721Bridge.depositERC721( + await L1ERC721Bridge.bridgeERC721( L1ERC721.address, - L2StandardERC721.address, + OptimismMintableERC721.address, TOKEN_ID, FINALIZATION_GAS, NON_NULL_BYTES @@ -181,10 +193,11 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) // Also check that Bob owns the NFT on L2 initially - expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) + expect(await OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) - const tx = await L2ERC721Bridge.withdrawERC721( - L2StandardERC721.address, + const tx = await L2ERC721Bridge.bridgeERC721( + OptimismMintableERC721.address, + L1ERC721.address, TOKEN_ID, 0, NON_NULL_BYTES @@ -196,15 +209,15 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) // L2 NFT is burned - await expect(L2StandardERC721.ownerOf(TOKEN_ID)).to.be.reverted + await expect(OptimismMintableERC721.ownerOf(TOKEN_ID)).to.be.reverted }) - withdrawalTest('withdrawERC721To', async () => { + withdrawalTest('bridgeERC721To', async () => { // Deposit an NFT into L2 so that there's something to withdraw await env.messenger.waitForMessageReceipt( - await L1ERC721Bridge.depositERC721( + await L1ERC721Bridge.bridgeERC721( L1ERC721.address, - L2StandardERC721.address, + OptimismMintableERC721.address, TOKEN_ID, FINALIZATION_GAS, NON_NULL_BYTES @@ -215,10 +228,11 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) // Also check that Bob owns the NFT on L2 initially - expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) + expect(await OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) - const tx = await L2ERC721Bridge.withdrawERC721To( - L2StandardERC721.address, + const tx = await L2ERC721Bridge.bridgeERC721To( + OptimismMintableERC721.address, + L1ERC721.address, aliceAddress, TOKEN_ID, 0, @@ -231,7 +245,7 @@ describe('ERC721 Bridge', () => { expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress) // L2 NFT is burned - await expect(L2StandardERC721.ownerOf(TOKEN_ID)).to.be.reverted + await expect(OptimismMintableERC721.ownerOf(TOKEN_ID)).to.be.reverted }) withdrawalTest( @@ -239,9 +253,9 @@ describe('ERC721 Bridge', () => { async () => { // First, deposit the legitimate L1 NFT. await env.messenger.waitForMessageReceipt( - await L1ERC721Bridge.depositERC721( + await L1ERC721Bridge.bridgeERC721( L1ERC721.address, - L2StandardERC721.address, + OptimismMintableERC721.address, TOKEN_ID, FINALIZATION_GAS, NON_NULL_BYTES @@ -253,25 +267,26 @@ describe('ERC721 Bridge', () => { // Deploy a fake L2 ERC721, which: // - Returns the address of the legitimate L1 token from its l1Token() getter. // - Allows the L2 bridge to call its burn() function. - const FakeL2StandardERC721 = await ( - await ethers.getContractFactory('FakeL2StandardERC721', bobWalletL2) + const FakeOptimismMintableERC721 = await ( + await ethers.getContractFactory('FakeOptimismMintableERC721', bobWalletL2) ).deploy(L1ERC721.address, L2ERC721Bridge.address) - await FakeL2StandardERC721.deployed() + await FakeOptimismMintableERC721.deployed() // Use the fake contract to mint Alice an NFT with the same token ID - const tx = await FakeL2StandardERC721.mint(aliceAddress, TOKEN_ID) + const tx = await FakeOptimismMintableERC721.mint(aliceAddress, TOKEN_ID) await tx.wait() // Check that Alice owns the NFT from the fake ERC721 contract - expect(await FakeL2StandardERC721.ownerOf(TOKEN_ID)).to.equal( + expect(await FakeOptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal( aliceAddress ) // Alice withdraws the NFT from the fake contract to L1, hoping to receive the legitimate L1 NFT. const withdrawalTx = await L2ERC721Bridge.connect( aliceWalletL2 - ).withdrawERC721( - FakeL2StandardERC721.address, + ).bridgeERC721( + FakeOptimismMintableERC721.address, + L1ERC721.address, TOKEN_ID, 0, NON_NULL_BYTES diff --git a/packages/contracts-periphery/.depcheckrc b/packages/contracts-periphery/.depcheckrc index 28241fd9eee325421f01a16b92d46b64305a76f3..b882779bd5a92004f9656e132a778eab4c6995b9 100644 --- a/packages/contracts-periphery/.depcheckrc +++ b/packages/contracts-periphery/.depcheckrc @@ -1,5 +1,6 @@ ignores: [ "@openzeppelin/contracts", + "@openzeppelin/contracts-upgradeable", "@rari-capital/solmate", "@types/node", "hardhat-deploy", diff --git a/packages/contracts-periphery/contracts/L1/messaging/IL1ERC721Bridge.sol b/packages/contracts-periphery/contracts/L1/messaging/IL1ERC721Bridge.sol deleted file mode 100644 index 5d06cc9e67db1e121488cc75e951f625768ff4c0..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/L1/messaging/IL1ERC721Bridge.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >0.5.0 <0.9.0; - -/** - * @title IL1ERC721Bridge - */ -interface IL1ERC721Bridge { - /********** - * Events * - **********/ - - event ERC721DepositInitiated( - address indexed _l1Token, - address indexed _l2Token, - address indexed _from, - address _to, - uint256 _tokenId, - bytes _data - ); - - event ERC721WithdrawalFinalized( - address indexed _l1Token, - address indexed _l2Token, - address indexed _from, - address _to, - uint256 _tokenId, - bytes _data - ); - - /******************** - * Public Functions * - ********************/ - - /** - * @dev get the address of the corresponding L2 bridge contract. - * @return Address of the corresponding L2 bridge contract. - */ - function l2ERC721Bridge() external returns (address); - - /** - * @dev deposit the ERC721 token to the caller on L2. - * @param _l1Token Address of the L1 ERC721 we are depositing - * @param _l2Token Address of the L1 respective L2 ERC721 - * @param _tokenId Token ID of the ERC721 to deposit - * @param _l2Gas Gas limit required to complete the deposit on L2. - * @param _data Optional data to forward to L2. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function depositERC721( - address _l1Token, - address _l2Token, - uint256 _tokenId, - uint32 _l2Gas, - bytes calldata _data - ) external; - - /** - * @dev deposit the ERC721 token to a recipient on L2. - * @param _l1Token Address of the L1 ERC721 we are depositing - * @param _l2Token Address of the L1 respective L2 ERC721 - * @param _to L2 address to credit the withdrawal to. - * @param _tokenId Token ID of the ERC721 to deposit. - * @param _l2Gas Gas limit required to complete the deposit on L2. - * @param _data Optional data to forward to L2. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function depositERC721To( - address _l1Token, - address _l2Token, - address _to, - uint256 _tokenId, - uint32 _l2Gas, - bytes calldata _data - ) external; - - /************************* - * Cross-chain Functions * - *************************/ - - /** - * @dev Complete a withdrawal from L2 to L1, and send the ERC721 token to the recipient on L1 - * This call will fail if the initialized withdrawal from L2 has not been finalized. - * - * @param _l1Token Address of L1 token to finalizeWithdrawal for. - * @param _l2Token Address of L2 token where withdrawal was initiated. - * @param _from L2 address initiating the transfer. - * @param _to L1 address to credit the withdrawal to. - * @param _tokenId Token ID of the ERC721 to deposit. - * @param _data Data provided by the sender on L2. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function finalizeERC721Withdrawal( - address _l1Token, - address _l2Token, - address _from, - address _to, - uint256 _tokenId, - bytes calldata _data - ) external; -} diff --git a/packages/contracts-periphery/contracts/L1/messaging/L1ERC721Bridge.sol b/packages/contracts-periphery/contracts/L1/messaging/L1ERC721Bridge.sol index 6365a2d048b50f552064da12e4d0724b40a54410..8f6ce9c2a9afa7fe8f3313b0e6569e37173746c1 100644 --- a/packages/contracts-periphery/contracts/L1/messaging/L1ERC721Bridge.sol +++ b/packages/contracts-periphery/contracts/L1/messaging/L1ERC721Bridge.sol @@ -1,138 +1,160 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -/* Interface Imports */ -import { IL1ERC721Bridge } from "./IL1ERC721Bridge.sol"; -import { IL2ERC721Bridge } from "../../L2/messaging/IL2ERC721Bridge.sol"; -import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/* Library Imports */ import { CrossDomainEnabled } from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { L2ERC721Bridge } from "../../L2/messaging/L2ERC721Bridge.sol"; /** * @title L1ERC721Bridge - * @dev The L1 ERC721 Bridge is a contract which stores deposited L1 NFTs that are in use - * on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits and listening - * to it for newly finalized withdrawals. + * @notice The L1 ERC721 bridge is a contract which works together with the L2 ERC721 bridge to + * make it possible to transfer ERC721 tokens between Optimism and Ethereum. This contract + * acts as an escrow for ERC721 tokens deposted into L2. */ -contract L1ERC721Bridge is IL1ERC721Bridge, CrossDomainEnabled { - /******************************** - * External Contract References * - ********************************/ - - address public l2ERC721Bridge; +contract L1ERC721Bridge is CrossDomainEnabled, OwnableUpgradeable { + /** + * @notice Contract version number. + */ + uint8 public constant VERSION = 1; - // Maps L1 token to L2 token to token ID to a boolean indicating if the token is deposited - mapping(address => mapping(address => mapping(uint256 => bool))) public deposits; + /** + * @notice Emitted when an ERC721 bridge to the other network is initiated. + * + * @param localToken Address of the token on this domain. + * @param remoteToken Address of the token on the remote domain. + * @param from Address that initiated bridging action. + * @param to Address to receive the token. + * @param tokenId ID of the specific token deposited. + * @param extraData Extra data for use on the client-side. + */ + event ERC721BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 tokenId, + bytes extraData + ); - /*************** - * Constructor * - ***************/ + /** + * @notice Emitted when an ERC721 bridge from the other network is finalized. + * + * @param localToken Address of the token on this domain. + * @param remoteToken Address of the token on the remote domain. + * @param from Address that initiated bridging action. + * @param to Address to receive the token. + * @param tokenId ID of the specific token deposited. + * @param extraData Extra data for use on the client-side. + */ + event ERC721BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 tokenId, + bytes extraData + ); - // This contract lives behind a proxy, so the constructor parameters will go unused. - constructor(address _l1messenger, address _l2ERC721Bridge) CrossDomainEnabled(address(0)) { - _initialize(_l1messenger, _l2ERC721Bridge); - } + /** + * @notice Address of the bridge on the other network. + */ + address public otherBridge; - /****************** - * Initialization * - ******************/ + // Maps L1 token to L2 token to token ID to a boolean indicating if the token is deposited + /** + * @notice Mapping of L1 token to L2 token to ID to boolean, indicating if the given L1 token + * by ID was deposited for a given L2 token. + */ + mapping(address => mapping(address => mapping(uint256 => bool))) public deposits; /** - * @param _l1messenger L1 Messenger address being used for cross-chain communications. - * @param _l2ERC721Bridge L2 ERC721 bridge address. + * @param _messenger Address of the CrossDomainMessenger on this network. + * @param _otherBridge Address of the ERC721 bridge on the other network. */ - function _initialize(address _l1messenger, address _l2ERC721Bridge) internal { - messenger = _l1messenger; - l2ERC721Bridge = _l2ERC721Bridge; + constructor(address _messenger, address _otherBridge) CrossDomainEnabled(address(0)) { + initialize(_messenger, _otherBridge); } - /************** - * Depositing * - **************/ - /** - * @inheritdoc IL1ERC721Bridge + * @param _messenger Address of the CrossDomainMessenger on this network. + * @param _otherBridge Address of the ERC721 bridge on the other network. */ - function depositERC721( - address _l1Token, - address _l2Token, - uint256 _tokenId, - uint32 _l2Gas, - bytes calldata _data - ) external virtual { - // Modifier requiring sender to be EOA. This check could be bypassed by a malicious - // contract via initcode, but it takes care of the user error we want to avoid. - require(!Address.isContract(msg.sender), "Account not EOA"); + function initialize(address _messenger, address _otherBridge) public reinitializer(VERSION) { + messenger = _messenger; + otherBridge = _otherBridge; - _initiateERC721Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _tokenId, _l2Gas, _data); + // Initialize upgradable OZ contracts + __Ownable_init(); } /** - * @inheritdoc IL1ERC721Bridge + * @notice Initiates a bridge of an NFT to the caller's account on L2. + * + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L2. Data supplied here will not be used to + * execute any code on L2 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function depositERC721To( - address _l1Token, - address _l2Token, - address _to, + function bridgeERC721( + address _localToken, + address _remoteToken, uint256 _tokenId, - uint32 _l2Gas, - bytes calldata _data - ) external virtual { - _initiateERC721Deposit(_l1Token, _l2Token, msg.sender, _to, _tokenId, _l2Gas, _data); + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + // Modifier requiring sender to be EOA. This check could be bypassed by a malicious + // contract via initcode, but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "L1ERC721Bridge: account is not externally owned"); + + _initiateBridgeERC721( + _localToken, + _remoteToken, + msg.sender, + msg.sender, + _tokenId, + _minGasLimit, + _extraData + ); } /** - * @dev Performs the logic for deposits by informing the L2 Deposited Token - * contract of the deposit and calling a handler to lock the L1 NFT. (e.g. transferFrom) + * @notice Initiates a bridge of an NFT to some recipient's account on L2. * - * @param _l1Token Address of the L1 ERC721 we are depositing - * @param _l2Token Address of the L1 respective L2 ERC721 - * @param _from Account to pull the deposit from on L1 - * @param _to Account to give the deposit to on L2 - * @param _tokenId Token ID of the ERC721 to deposit. - * @param _l2Gas Gas limit required to complete the deposit on L2. - * @param _data Optional data to forward to L2. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _to Address to receive the token on the other domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L2. Data supplied here will not be used to + * execute any code on L2 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function _initiateERC721Deposit( - address _l1Token, - address _l2Token, - address _from, + function bridgeERC721To( + address _localToken, + address _remoteToken, address _to, uint256 _tokenId, - uint32 _l2Gas, - bytes calldata _data - ) internal { - // When a deposit is initiated on L1, the L1 Bridge transfers the NFT to itself for future - // withdrawals. - // slither-disable-next-line reentrancy-events, reentrancy-benign - IERC721(_l1Token).transferFrom(_from, address(this), _tokenId); - - // Construct calldata for _l2Token.finalizeERC721Deposit(_to, _tokenId) - bytes memory message = abi.encodeWithSelector( - IL2ERC721Bridge.finalizeERC721Deposit.selector, - _l1Token, - _l2Token, - _from, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + _initiateBridgeERC721( + _localToken, + _remoteToken, + msg.sender, _to, _tokenId, - _data + _minGasLimit, + _extraData ); - - // Send calldata into L2 - // slither-disable-next-line reentrancy-events, reentrancy-benign - sendCrossDomainMessage(l2ERC721Bridge, _l2Gas, message); - - // slither-disable-next-line reentrancy-benign - deposits[_l1Token][_l2Token][_tokenId] = true; - - // slither-disable-next-line reentrancy-events - emit ERC721DepositInitiated(_l1Token, _l2Token, _from, _to, _tokenId, _data); } /************************* @@ -140,29 +162,81 @@ contract L1ERC721Bridge is IL1ERC721Bridge, CrossDomainEnabled { *************************/ /** - * @inheritdoc IL1ERC721Bridge + * @notice Completes an ERC721 bridge from the other domain and sends the ERC721 token to the + * recipient on this domain. + * + * @param _localToken Address of the ERC721 token on this domain. + * @param _remoteToken Address of the ERC721 token on the other domain. + * @param _from Address that triggered the bridge on the other domain. + * @param _to Address to receive the token on this domain. + * @param _tokenId ID of the token being deposited. + * @param _extraData Optional data to forward to L2. Data supplied here will not be used to + * execute any code on L2 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function finalizeERC721Withdrawal( - address _l1Token, - address _l2Token, + function finalizeBridgeERC721( + address _localToken, + address _remoteToken, address _from, address _to, uint256 _tokenId, - bytes calldata _data - ) external onlyFromCrossDomainAccount(l2ERC721Bridge) { + bytes calldata _extraData + ) external onlyFromCrossDomainAccount(otherBridge) { // Checks that the L1/L2 token pair has a token ID that is escrowed in the L1 Bridge require( - deposits[_l1Token][_l2Token][_tokenId] == true, + deposits[_localToken][_remoteToken][_tokenId] == true, "Token ID is not escrowed in the L1 Bridge" ); - deposits[_l1Token][_l2Token][_tokenId] = false; + deposits[_localToken][_remoteToken][_tokenId] = false; // When a withdrawal is finalized on L1, the L1 Bridge transfers the NFT to the withdrawer // slither-disable-next-line reentrancy-events - IERC721(_l1Token).transferFrom(address(this), _to, _tokenId); + IERC721(_localToken).transferFrom(address(this), _to, _tokenId); // slither-disable-next-line reentrancy-events - emit ERC721WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _tokenId, _data); + emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData); + } + + /** + * @notice Internal function for initiating a token bridge to the other domain. + * + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _from Address of the sender on this domain. + * @param _to Address to receive the token on the other domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L2. Data supplied here will not be used to + * execute any code on L2 and is only emitted as extra data for the + * convenience of off-chain tooling. + */ + function _initiateBridgeERC721( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _tokenId, + uint32 _minGasLimit, + bytes calldata _extraData + ) internal { + // Construct calldata for _l2Token.finalizeBridgeERC721(_to, _tokenId) + bytes memory message = abi.encodeWithSelector( + L2ERC721Bridge.finalizeBridgeERC721.selector, + _remoteToken, + _localToken, + _from, + _to, + _tokenId, + _extraData + ); + + // Lock token into bridge + deposits[_localToken][_remoteToken][_tokenId] = true; + IERC721(_localToken).transferFrom(_from, address(this), _tokenId); + + // Send calldata into L2 + sendCrossDomainMessage(otherBridge, _minGasLimit, message); + emit ERC721BridgeInitiated(_localToken, _remoteToken, _from, _to, _tokenId, _extraData); } } diff --git a/packages/contracts-periphery/contracts/L2/messaging/IL2ERC721Bridge.sol b/packages/contracts-periphery/contracts/L2/messaging/IL2ERC721Bridge.sol deleted file mode 100644 index 67772c5dbe5845ab35fee793f3a65c4f7423c9df..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/L2/messaging/IL2ERC721Bridge.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -/** - * @title IL2ERC721Bridge - */ -interface IL2ERC721Bridge { - /********** - * Events * - **********/ - - event ERC721WithdrawalInitiated( - address indexed _l1Token, - address indexed _l2Token, - address indexed _from, - address _to, - uint256 _tokenId, - bytes _data - ); - - event ERC721DepositFinalized( - address indexed _l1Token, - address indexed _l2Token, - address indexed _from, - address _to, - uint256 _tokenId, - bytes _data - ); - - event ERC721DepositFailed( - address indexed _l1Token, - address indexed _l2Token, - address indexed _from, - address _to, - uint256 _tokenId, - bytes _data - ); - - /******************** - * Public Functions * - ********************/ - - /** - * @dev get the address of the corresponding L1 bridge contract. - * @return Address of the corresponding L1 bridge contract. - */ - function l1ERC721Bridge() external returns (address); - - /** - * @dev initiate a withdraw of an NFT to the caller's account on L1 - * @param _l2Token Address of L2 token where withdrawal was initiated. - * @param _tokenId Token ID to withdraw. - * @param _l1Gas Unused, but included for potential forward compatibility considerations. - * @param _data Optional data to forward to L1. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function withdrawERC721( - address _l2Token, - uint256 _tokenId, - uint32 _l1Gas, - bytes calldata _data - ) external; - - /** - * @dev initiate a withdrawal of an NFT to a recipient's account on L1. - * @param _l2Token Address of L2 token where withdrawal is initiated. - * @param _to L1 adress to send the withdrawal to. - * @param _tokenId Token ID to withdraw. - * @param _l1Gas Unused, but included for potential forward compatibility considerations. - * @param _data Optional data to forward to L1. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function withdrawERC721To( - address _l2Token, - address _to, - uint256 _tokenId, - uint32 _l1Gas, - bytes calldata _data - ) external; - - /************************* - * Cross-chain Functions * - *************************/ - - /** - * @dev Complete a deposit from L1 to L2, and send ERC721 token to the recipient on L2. - * This call will fail if it did not originate from a corresponding deposit in - * L1ERC721Bridge. - * @param _l1Token Address for the l1 token this is called with - * @param _l2Token Address for the l2 token this is called with - * @param _from Account to pull the deposit from on L2. - * @param _to Address to receive the withdrawal at - * @param _tokenId Token ID to withdraw - * @param _data Data provider by the sender on L1. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. - */ - function finalizeERC721Deposit( - address _l1Token, - address _l2Token, - address _from, - address _to, - uint256 _tokenId, - bytes calldata _data - ) external; -} diff --git a/packages/contracts-periphery/contracts/L2/messaging/L2ERC721Bridge.sol b/packages/contracts-periphery/contracts/L2/messaging/L2ERC721Bridge.sol index 5f4cb1db1bdf592b67f189f0659f579d878e7ea5..846554244ae2feabab1f6ccc189e8931cb133072 100644 --- a/packages/contracts-periphery/contracts/L2/messaging/L2ERC721Bridge.sol +++ b/packages/contracts-periphery/contracts/L2/messaging/L2ERC721Bridge.sol @@ -1,172 +1,212 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -/* Interface Imports */ -import { IL1ERC721Bridge } from "../../L1/messaging/IL1ERC721Bridge.sol"; -import { IL1ERC721Bridge } from "../../L1/messaging/IL1ERC721Bridge.sol"; -import { IL2ERC721Bridge } from "./IL2ERC721Bridge.sol"; - -/* Library Imports */ import { CrossDomainEnabled } from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; - -/* Contract Imports */ -import { IL2StandardERC721 } from "../../standards/IL2StandardERC721.sol"; +import { L1ERC721Bridge } from "../../L1/messaging/L1ERC721Bridge.sol"; +import { IOptimismMintableERC721 } from "../../universal/op-erc721/IOptimismMintableERC721.sol"; /** * @title L2ERC721Bridge - * @dev The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to - * enable ERC721 transitions between L1 and L2. - * This contract acts as a minter for new tokens when it hears about deposits into the L1 ERC721 - * bridge. - * This contract also acts as a burner of the token intended for withdrawal, informing the L1 - * bridge to release the L1 NFT. + * @notice The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to + * make it possible to transfer ERC721 tokens between Optimism and Ethereum. This contract + * acts as a minter for new tokens when it hears about deposits into the L1 ERC721 bridge. + * This contract also acts as a burner for tokens being withdrawn. */ -contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled { - /******************************** - * External Contract References * - ********************************/ +contract L2ERC721Bridge is CrossDomainEnabled, OwnableUpgradeable { + /** + * @notice Contract version number. + */ + uint8 public constant VERSION = 1; - address public l1ERC721Bridge; + /** + * @notice Emitted when an ERC721 bridge to the other network is initiated. + * + * @param localToken Address of the token on this domain. + * @param remoteToken Address of the token on the remote domain. + * @param from Address that initiated bridging action. + * @param to Address to receive the token. + * @param tokenId ID of the specific token deposited. + * @param extraData Extra data for use on the client-side. + */ + event ERC721BridgeInitiated( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 tokenId, + bytes extraData + ); - /*************** - * Constructor * - ***************/ + /** + * @notice Emitted when an ERC721 bridge from the other network is finalized. + * + * @param localToken Address of the token on this domain. + * @param remoteToken Address of the token on the remote domain. + * @param from Address that initiated bridging action. + * @param to Address to receive the token. + * @param tokenId ID of the specific token deposited. + * @param extraData Extra data for use on the client-side. + */ + event ERC721BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 tokenId, + bytes extraData + ); /** - * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract. + * @notice Emitted when an ERC721 bridge from the other network fails. + * + * @param localToken Address of the token on this domain. + * @param remoteToken Address of the token on the remote domain. + * @param from Address that initiated bridging action. + * @param to Address to receive the token. + * @param tokenId ID of the specific token deposited. + * @param extraData Extra data for use on the client-side. */ - constructor(address _l2CrossDomainMessenger) CrossDomainEnabled(_l2CrossDomainMessenger) {} + event ERC721BridgeFailed( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 tokenId, + bytes extraData + ); - /****************** - * Initialization * - ******************/ + /** + * @notice Address of the bridge on the other network. + */ + address public otherBridge; /** - * @param _l1ERC721Bridge Address of the L1 bridge deployed to the main chain. + * @param _messenger Address of the CrossDomainMessenger on this network. + * @param _otherBridge Address of the ERC721 bridge on the other network. */ - // slither-disable-next-line external-function - function initialize(address _l1ERC721Bridge) public { - require(l1ERC721Bridge == address(0), "Contract has already been initialized."); - l1ERC721Bridge = _l1ERC721Bridge; + constructor(address _messenger, address _otherBridge) CrossDomainEnabled(address(0)) { + initialize(_messenger, _otherBridge); } - /*************** - * Withdrawing * - ***************/ - /** - * @inheritdoc IL2ERC721Bridge + * @param _messenger Address of the CrossDomainMessenger on this network. + * @param _otherBridge Address of the ERC721 bridge on the other network. */ - function withdrawERC721( - address _l2Token, - uint256 _tokenId, - uint32 _l1Gas, - bytes calldata _data - ) external virtual { - // Modifier requiring sender to be EOA. This check could be bypassed by a malicious - // contract via initcode, but it takes care of the user error we want to avoid. - require(!Address.isContract(msg.sender), "Account not EOA"); + function initialize(address _messenger, address _otherBridge) public reinitializer(VERSION) { + messenger = _messenger; + otherBridge = _otherBridge; - _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _tokenId, _l1Gas, _data); + // Initialize upgradable OZ contracts + __Ownable_init(); } /** - * @inheritdoc IL2ERC721Bridge + * @notice Initiates a bridge of an NFT to the caller's account on L1. + * + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L1. Data supplied here will not be used to + * execute any code on L1 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function withdrawERC721To( - address _l2Token, - address _to, + function bridgeERC721( + address _localToken, + address _remoteToken, uint256 _tokenId, - uint32 _l1Gas, - bytes calldata _data - ) external virtual { - _initiateWithdrawal(_l2Token, msg.sender, _to, _tokenId, _l1Gas, _data); + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + // Modifier requiring sender to be EOA. This check could be bypassed by a malicious + // contract via initcode, but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "L2ERC721Bridge: account is not externally owned"); + + _initiateBridgeERC721( + _localToken, + _remoteToken, + msg.sender, + msg.sender, + _tokenId, + _minGasLimit, + _extraData + ); } /** - * @dev Performs the logic for withdrawals by burning the token and informing - * the L1 token Gateway of the withdrawal. - * @param _l2Token Address of L2 token where withdrawal is initiated. - * @param _from Account to pull the withdrawal from on L2. - * @param _to Account to give the withdrawal to on L1. - * @param _tokenId Token ID of the token to withdraw. - * @param _l1Gas Unused, but included for potential forward compatibility considerations. - * @param _data Optional data to forward to L1. This data is provided - * solely as a convenience for external contracts. Aside from enforcing a maximum - * length, these contracts provide no guarantees about its content. + * @notice Initiates a bridge of an NFT to some recipient's account on L1. + * + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _to Address to receive the token on the other domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L1. Data supplied here will not be used to + * execute any code on L1 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function _initiateWithdrawal( - address _l2Token, - address _from, + function bridgeERC721To( + address _localToken, + address _remoteToken, address _to, uint256 _tokenId, - uint32 _l1Gas, - bytes calldata _data - ) internal { - // Check that the withdrawal is being initiated by the NFT owner - require( - _from == IL2StandardERC721(_l2Token).ownerOf(_tokenId), - "Withdrawal is not being initiated by NFT owner" - ); - - // When a withdrawal is initiated, we burn the withdrawer's NFT to prevent subsequent L2 - // usage - // slither-disable-next-line reentrancy-events - IL2StandardERC721(_l2Token).burn(_from, _tokenId); - - // Construct calldata for l1ERC721Bridge.finalizeERC721Withdrawal(_to, _tokenId) - // slither-disable-next-line reentrancy-events - address l1Token = IL2StandardERC721(_l2Token).l1Token(); - bytes memory message = abi.encodeWithSelector( - IL1ERC721Bridge.finalizeERC721Withdrawal.selector, - l1Token, - _l2Token, - _from, + uint32 _minGasLimit, + bytes calldata _extraData + ) external { + _initiateBridgeERC721( + _localToken, + _remoteToken, + msg.sender, _to, _tokenId, - _data + _minGasLimit, + _extraData ); - - // Send message to L1 bridge - // slither-disable-next-line reentrancy-events - sendCrossDomainMessage(l1ERC721Bridge, _l1Gas, message); - - // slither-disable-next-line reentrancy-events - emit ERC721WithdrawalInitiated(l1Token, _l2Token, _from, _to, _tokenId, _data); } - /************************************ - * Cross-chain Function: Depositing * - ************************************/ - /** - * @inheritdoc IL2ERC721Bridge + * @notice Completes an ERC721 bridge from the other domain and sends the ERC721 token to the + * recipient on this domain. + * + * @param _localToken Address of the ERC721 token on this domain. + * @param _remoteToken Address of the ERC721 token on the other domain. + * @param _from Address that triggered the bridge on the other domain. + * @param _to Address to receive the token on this domain. + * @param _tokenId ID of the token being deposited. + * @param _extraData Optional data to forward to L1. Data supplied here will not be used to + * execute any code on L1 and is only emitted as extra data for the + * convenience of off-chain tooling. */ - function finalizeERC721Deposit( - address _l1Token, - address _l2Token, + function finalizeBridgeERC721( + address _localToken, + address _remoteToken, address _from, address _to, uint256 _tokenId, - bytes calldata _data - ) external virtual onlyFromCrossDomainAccount(l1ERC721Bridge) { - // Check the target token is compliant and - // verify the deposited token on L1 matches the L2 deposited token representation here + bytes calldata _extraData + ) external onlyFromCrossDomainAccount(otherBridge) { + // Check the target token is compliant and verify the deposited token on L1 matches the L2 + // deposited token representation. if ( // slither-disable-next-line reentrancy-events - ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && - _l1Token == IL2StandardERC721(_l2Token).l1Token() + ERC165Checker.supportsInterface( + _localToken, + type(IOptimismMintableERC721).interfaceId + ) && _remoteToken == IOptimismMintableERC721(_localToken).remoteToken() ) { // When a deposit is finalized, we give the NFT with the same tokenId to the account // on L2. // slither-disable-next-line reentrancy-events - IL2StandardERC721(_l2Token).mint(_to, _tokenId); + IOptimismMintableERC721(_localToken).mint(_to, _tokenId); // slither-disable-next-line reentrancy-events - emit ERC721DepositFinalized(_l1Token, _l2Token, _from, _to, _tokenId, _data); + emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData); } else { // Either the L2 token which is being deposited-into disagrees about the correct address // of its L1 token, or does not support the correct interface. @@ -177,20 +217,80 @@ contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled { // There is no way to prevent malicious token contracts altogether, but this does limit // user error and mitigate some forms of malicious contract behavior. bytes memory message = abi.encodeWithSelector( - IL1ERC721Bridge.finalizeERC721Withdrawal.selector, - _l1Token, - _l2Token, + L1ERC721Bridge.finalizeBridgeERC721.selector, + _remoteToken, + _localToken, _to, // switched the _to and _from here to bounce back the deposit to the sender _from, _tokenId, - _data + _extraData ); // Send message up to L1 bridge // slither-disable-next-line reentrancy-events - sendCrossDomainMessage(l1ERC721Bridge, 0, message); + sendCrossDomainMessage(otherBridge, 0, message); + // slither-disable-next-line reentrancy-events - emit ERC721DepositFailed(_l1Token, _l2Token, _from, _to, _tokenId, _data); + emit ERC721BridgeFailed(_localToken, _remoteToken, _from, _to, _tokenId, _extraData); } } + + /** + * @notice Internal function for initiating a token bridge to the other domain. + * + * @param _localToken Address of the ERC721 on this domain. + * @param _remoteToken Address of the ERC721 on the remote domain. + * @param _from Address of the sender on this domain. + * @param _to Address to receive the token on the other domain. + * @param _tokenId Token ID to bridge. + * @param _minGasLimit Minimum gas limit for the bridge message on the other domain. + * @param _extraData Optional data to forward to L1. Data supplied here will not be used to + * execute any code on L1 and is only emitted as extra data for the + * convenience of off-chain tooling. + */ + function _initiateBridgeERC721( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _tokenId, + uint32 _minGasLimit, + bytes calldata _extraData + ) internal { + // Check that the withdrawal is being initiated by the NFT owner + require( + _from == IOptimismMintableERC721(_localToken).ownerOf(_tokenId), + "Withdrawal is not being initiated by NFT owner" + ); + + // When a withdrawal is initiated, we burn the withdrawer's NFT to prevent subsequent L2 + // usage + // slither-disable-next-line reentrancy-events + IOptimismMintableERC721(_localToken).burn(_from, _tokenId); + + // Construct calldata for l1ERC721Bridge.finalizeBridgeERC721(_to, _tokenId) + // slither-disable-next-line reentrancy-events + address remoteToken = IOptimismMintableERC721(_localToken).remoteToken(); + require( + remoteToken == _remoteToken, + "L2ERC721Bridge: remote token does not match given value" + ); + + bytes memory message = abi.encodeWithSelector( + L1ERC721Bridge.finalizeBridgeERC721.selector, + remoteToken, + _localToken, + _from, + _to, + _tokenId, + _extraData + ); + + // Send message to L1 bridge + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(otherBridge, _minGasLimit, message); + + // slither-disable-next-line reentrancy-events + emit ERC721BridgeInitiated(_localToken, remoteToken, _from, _to, _tokenId, _extraData); + } } diff --git a/packages/contracts-periphery/contracts/L2/messaging/L2StandardERC721Factory.sol b/packages/contracts-periphery/contracts/L2/messaging/L2StandardERC721Factory.sol deleted file mode 100644 index d80ef469463a01eeb30d70594689606b3357632b..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/L2/messaging/L2StandardERC721Factory.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -/* Contract Imports */ -import { L2StandardERC721 } from "../../standards/L2StandardERC721.sol"; - -/** - * @title L2StandardERC721Factory - * @dev Factory contract for creating standard L2 ERC721 representations of L1 ERC721s - * compatible with and working on the NFT bridge. - */ -contract L2StandardERC721Factory { - event StandardL2ERC721Created(address indexed _l1Token, address indexed _l2Token); - - // Address of the L2 ERC721 Bridge. - address public l2ERC721Bridge; - - // Maps an L2 ERC721 token address to a boolean that returns true if the token was created - // with the L2StandardERC721Factory. - mapping(address => bool) public isStandardERC721; - - // Maps an L1 ERC721 to its L2 Standard ERC721 contract, if it exists. This mapping enforces - // that there is one, and only one, L2 Standard ERC721 for each L1 ERC721. The purpose of this - // is to prevent multiple L2 Standard ERC721s from existing for a single L1 ERC721, which - // would result in unnecessary fragmentation, since the Standard ERC721s deployed by this - // factory implement the exact same functionality. This mapping should NOT be interpreted as - // a token list. This is because a custom L2 ERC721 may be recognized by the community as - // the official L2 contract for an L1 ERC721, but the custom contract address wouldn't appear - // in this mapping. An off-chain token list will serve as the official source of truth for - // L2 ERC721s, similar to Optimism's ERC20 token list: - // https://github.com/ethereum-optimism/ethereum-optimism.github.io - mapping(address => address) public standardERC721Mapping; - - constructor(address _l2ERC721Bridge) { - l2ERC721Bridge = _l2ERC721Bridge; - } - - /** - * @dev Creates an instance of the standard ERC721 token on L2. - * @param _l1Token Address of the corresponding L1 token. - * @param _name ERC721 name. - * @param _symbol ERC721 symbol. - */ - function createStandardL2ERC721( - address _l1Token, - string memory _name, - string memory _symbol - ) external { - require(_l1Token != address(0), "Must provide L1 token address"); - - // Only one L2 Standard Token can exist for each L1 Token - require( - standardERC721Mapping[_l1Token] == address(0), - "L2 Standard Token already exists for this L1 Token" - ); - - L2StandardERC721 l2Token = new L2StandardERC721(l2ERC721Bridge, _l1Token, _name, _symbol); - - isStandardERC721[address(l2Token)] = true; - standardERC721Mapping[_l1Token] = address(l2Token); - emit StandardL2ERC721Created(_l1Token, address(l2Token)); - } -} diff --git a/packages/contracts-periphery/contracts/libraries/utils/Lib_Strings.sol b/packages/contracts-periphery/contracts/libraries/utils/Lib_Strings.sol deleted file mode 100644 index 05ce0acfeafc6976a05a36cd5a28182e6f5e623c..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/libraries/utils/Lib_Strings.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -/** - * @title Lib_Strings - * @dev This library implements a function to convert an address to an ASCII string. - * It uses the implementation written by tkeber: - * https://ethereum.stackexchange.com/questions/8346/convert-address-to-string/8447#8447 - */ -library Lib_Strings { - /********************** - * Internal Functions * - **********************/ - - /** - * Converts an address to its ASCII string representation. The returned string will be - * lowercase and the 0x prefix will be removed. - * @param _address Address to convert to an ASCII string. - * @return String representation of the address. - */ - function addressToString(address _address) internal pure returns (string memory) { - bytes memory s = new bytes(40); - for (uint256 i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint256(uint160(_address)) / (2**(8 * (19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - s[2 * i] = hexCharToAscii(hi); - s[2 * i + 1] = hexCharToAscii(lo); - } - return string(s); - } - - /** - * Converts a hexadecimal character into its ASCII representation. - * @param _byte A single hexadecimal character - * @return ASCII representation of the hexadecimal character. - */ - function hexCharToAscii(bytes1 _byte) internal pure returns (bytes1) { - if (uint8(_byte) < 10) return bytes1(uint8(_byte) + 0x30); - else return bytes1(uint8(_byte) + 0x57); - } -} diff --git a/packages/contracts-periphery/contracts/standards/IL2StandardERC721.sol b/packages/contracts-periphery/contracts/standards/IL2StandardERC721.sol deleted file mode 100644 index 09973ecc01d54ce685f7170952053682a4dae8d4..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/standards/IL2StandardERC721.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -interface IL2StandardERC721 is IERC721 { - function l1Token() external returns (address); - - function mint(address _to, uint256 _tokenId) external; - - function burn(address _from, uint256 _tokenId) external; - - event Mint(address indexed _account, uint256 _tokenId); - event Burn(address indexed _account, uint256 _tokenId); -} diff --git a/packages/contracts-periphery/contracts/standards/L2StandardERC721.sol b/packages/contracts-periphery/contracts/standards/L2StandardERC721.sol deleted file mode 100644 index fd5a4b77a5caac633754546ac1ef3b735b897242..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/standards/L2StandardERC721.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Lib_Strings } from "../libraries/utils/Lib_Strings.sol"; -import "./IL2StandardERC721.sol"; - -contract L2StandardERC721 is IL2StandardERC721, ERC721 { - address public l1Token; - address public l2Bridge; - string public baseTokenURI; - - /** - * @param _l2Bridge Address of the L2 standard bridge. - * @param _l1Token Address of the corresponding L1 token. - * @param _name ERC721 name. - * @param _symbol ERC721 symbol. - */ - constructor( - address _l2Bridge, - address _l1Token, - string memory _name, - string memory _symbol - ) ERC721(_name, _symbol) { - l1Token = _l1Token; - l2Bridge = _l2Bridge; - - // Creates a base URI in the format specified by EIP-681: - // https://eips.ethereum.org/EIPS/eip-681 - baseTokenURI = string( - abi.encodePacked( - "ethereum:0x", - Lib_Strings.addressToString(_l1Token), - "@", - Strings.toString(block.chainid), - "/tokenURI?uint256=" - ) - ); - } - - modifier onlyL2Bridge() { - require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); - _; - } - - // slither-disable-next-line external-function - function supportsInterface(bytes4 _interfaceId) - public - view - override(ERC721, IERC165) - returns (bool) - { - bytes4 iface1 = type(IERC165).interfaceId; - bytes4 iface2 = type(IL2StandardERC721).interfaceId; - return - _interfaceId == iface1 || - _interfaceId == iface2 || - super.supportsInterface(_interfaceId); - } - - // slither-disable-next-line external-function - function mint(address _to, uint256 _tokenId) public virtual onlyL2Bridge { - _mint(_to, _tokenId); - - emit Mint(_to, _tokenId); - } - - // slither-disable-next-line external-function - function burn(address _from, uint256 _tokenId) public virtual onlyL2Bridge { - _burn(_tokenId); - - emit Burn(_from, _tokenId); - } - - function _baseURI() internal view virtual override returns (string memory) { - return baseTokenURI; - } -} diff --git a/packages/contracts-periphery/contracts/test-libraries/utils/TestLib_Strings.sol b/packages/contracts-periphery/contracts/test-libraries/utils/TestLib_Strings.sol deleted file mode 100644 index 520093b3b759d21a17610fc113a233097639410c..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/contracts/test-libraries/utils/TestLib_Strings.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -/* Library Imports */ -import { Lib_Strings } from "../../libraries/utils/Lib_Strings.sol"; - -/** - * @title TestLib_Strings - */ -contract TestLib_Strings { - function addressToString(address _address) public pure returns (string memory) { - return Lib_Strings.addressToString(_address); - } - - function hexCharToAscii(bytes1 _byte) public pure returns (bytes1) { - return Lib_Strings.hexCharToAscii(_byte); - } -} diff --git a/packages/contracts-periphery/contracts/universal/op-erc721/IOptimismMintableERC721.sol b/packages/contracts-periphery/contracts/universal/op-erc721/IOptimismMintableERC721.sol new file mode 100644 index 0000000000000000000000000000000000000000..bab4e56865ec9f7f19793aee16a372818827aee2 --- /dev/null +++ b/packages/contracts-periphery/contracts/universal/op-erc721/IOptimismMintableERC721.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title IOptimismMintableERC721 + * @notice Interface for contracts that are compatible with the OptimismMintableERC721 standard. + * Tokens that follow this standard can be easily transferred across the ERC721 bridge. + */ +interface IOptimismMintableERC721 is IERC721 { + /** + * @notice Emitted when a token is minted. + * + * @param account Address of the account the token was minted to. + * @param tokenId Token ID of the minted token. + */ + event Mint(address indexed account, uint256 tokenId); + + /** + * @notice Emitted when a token is burned. + * + * @param account Address of the account the token was burned from. + * @param tokenId Token ID of the burned token. + */ + event Burn(address indexed account, uint256 tokenId); + + /** + * @notice Address of the token on the remote domain. + */ + function remoteToken() external returns (address); + + /** + * @notice Address of the ERC721 bridge on this network. + */ + function bridge() external returns (address); + + /** + * @notice Mints some token ID for a user. + * + * @param _to Address of the user to mint the token for. + * @param _tokenId Token ID to mint. + */ + function mint(address _to, uint256 _tokenId) external; + + /** + * @notice Burns a token ID from a user. + * + * @param _from Address of the user to burn the token from. + * @param _tokenId Token ID to burn. + */ + function burn(address _from, uint256 _tokenId) external; +} diff --git a/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721.sol b/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721.sol new file mode 100644 index 0000000000000000000000000000000000000000..3ecdf6b503cba4a63cc06bdcc6c6f06200d7571a --- /dev/null +++ b/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { IOptimismMintableERC721 } from "./IOptimismMintableERC721.sol"; + +/** + * @title OptimismMintableERC721 + * @notice This contract is the remote representation for some token that lives on another network, + * typically an Optimism representation of an Ethereum-based token. Standard reference + * implementation that can be extended or modified according to your needs. + */ +contract OptimismMintableERC721 is ERC721, IOptimismMintableERC721 { + /** + * @inheritdoc IOptimismMintableERC721 + */ + address public remoteToken; + + /** + * @inheritdoc IOptimismMintableERC721 + */ + address public bridge; + + /** + * @notice Base token URI for this token. + */ + string public baseTokenURI; + + /** + * @param _bridge Address of the bridge on this network. + * @param _remoteToken Address of the corresponding token on the other network. + * @param _name ERC721 name. + * @param _symbol ERC721 symbol. + */ + constructor( + address _bridge, + address _remoteToken, + string memory _name, + string memory _symbol + ) ERC721(_name, _symbol) { + remoteToken = _remoteToken; + bridge = _bridge; + + // Creates a base URI in the format specified by EIP-681: + // https://eips.ethereum.org/EIPS/eip-681 + baseTokenURI = string( + abi.encodePacked( + "ethereum:", + Strings.toHexString(uint160(_remoteToken)), + "@", + Strings.toString(block.chainid), + "/tokenURI?uint256=" + ) + ); + } + + /** + * @notice Modifier that prevents callers other than the bridge from calling the function. + */ + modifier onlyBridge() { + require(msg.sender == bridge, "OptimismMintableERC721: only bridge can call this function"); + _; + } + + /** + * @inheritdoc IOptimismMintableERC721 + */ + function mint(address _to, uint256 _tokenId) external virtual onlyBridge { + _mint(_to, _tokenId); + + emit Mint(_to, _tokenId); + } + + /** + * @inheritdoc IOptimismMintableERC721 + */ + function burn(address _from, uint256 _tokenId) external virtual onlyBridge { + _burn(_tokenId); + + emit Burn(_from, _tokenId); + } + + /** + * @notice Checks if a given interface ID is supported by this contract. + * + * @param _interfaceId The interface ID to check. + * + * @return True if the interface ID is supported, false otherwise. + */ + function supportsInterface(bytes4 _interfaceId) + public + view + override(ERC721, IERC165) + returns (bool) + { + bytes4 iface1 = type(IERC165).interfaceId; + bytes4 iface2 = type(IOptimismMintableERC721).interfaceId; + return + _interfaceId == iface1 || + _interfaceId == iface2 || + super.supportsInterface(_interfaceId); + } + + /** + * @notice Returns the base token URI. + * + * @return Base token URI. + */ + function _baseURI() internal view virtual override returns (string memory) { + return baseTokenURI; + } +} diff --git a/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721Factory.sol b/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721Factory.sol new file mode 100644 index 0000000000000000000000000000000000000000..bf4365c384a1ca8e2f7d1df22f0b62625f5cb969 --- /dev/null +++ b/packages/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721Factory.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { OptimismMintableERC721 } from "./OptimismMintableERC721.sol"; + +/** + * @title OptimismMintableERC721Factory + * @notice Factory contract for creating OptimismMintableERC721 contracts. + */ +contract OptimismMintableERC721Factory is OwnableUpgradeable { + /** + * @notice Contract version number. + */ + uint8 public constant VERSION = 1; + + /** + * @notice Emitted whenever a new OptimismMintableERC721 contract is created. + * + * @param remoteToken Address of the token on the remote domain. + * @param localToken Address of the token on the this domain. + */ + event OptimismMintableERC721Created(address indexed remoteToken, address indexed localToken); + + /** + * @notice Address of the ERC721 bridge on this network. + */ + address public bridge; + + /** + * @notice Tracks addresses created by this factory. + */ + mapping(address => bool) public isStandardOptimismMintableERC721; + + /** + * @param _bridge Address of the ERC721 bridge on this network. + */ + constructor(address _bridge) { + intialize(_bridge); + } + + /** + * @notice Initializes the factory. + * + * @param _bridge Address of the ERC721 bridge on this network. + */ + function intialize(address _bridge) public reinitializer(VERSION) { + bridge = _bridge; + + // Initialize upgradable OZ contracts + __Ownable_init(); + } + + /** + * @notice Creates an instance of the standard ERC721. + * + * @param _remoteToken Address of the corresponding token on the other domain. + * @param _name ERC721 name. + * @param _symbol ERC721 symbol. + */ + function createStandardOptimismMintableERC721( + address _remoteToken, + string memory _name, + string memory _symbol + ) external { + require( + _remoteToken != address(0), + "OptimismMintableERC721Factory: L1 token address cannot be address(0)" + ); + require( + bridge != address(0), + "OptimismMintableERC721Factory: bridge address must be initialized" + ); + + OptimismMintableERC721 localToken = new OptimismMintableERC721( + bridge, + _remoteToken, + _name, + _symbol + ); + + isStandardOptimismMintableERC721[address(localToken)] = true; + emit OptimismMintableERC721Created(_remoteToken, address(localToken)); + } +} diff --git a/packages/contracts-periphery/package.json b/packages/contracts-periphery/package.json index bdc9be415d18570551d4370b7787f27c191a114b..450fb98cea5dcac1e3a22efaa00013a8072811cd 100644 --- a/packages/contracts-periphery/package.json +++ b/packages/contracts-periphery/package.json @@ -62,6 +62,7 @@ "@nomiclabs/hardhat-waffle": "^2.0.1", "@rari-capital/solmate": "^6.3.0", "@openzeppelin/contracts": "4.6.0", + "@openzeppelin/contracts-upgradeable": "4.6.0", "@types/chai": "^4.2.18", "@types/mocha": "^8.2.2", "@types/node": "^17.0.21", diff --git a/packages/contracts-periphery/test/contracts/L1/messaging/L1ERC721Bridge.spec.ts b/packages/contracts-periphery/test/contracts/L1/messaging/L1ERC721Bridge.spec.ts index 6fac674609e0b645c6d52d0a753501b6a2b51edc..e785804a747de9f8e9ee01a8386e1e173951f892 100644 --- a/packages/contracts-periphery/test/contracts/L1/messaging/L1ERC721Bridge.spec.ts +++ b/packages/contracts-periphery/test/contracts/L1/messaging/L1ERC721Bridge.spec.ts @@ -89,11 +89,11 @@ describe('L1ERC721Bridge', () => { await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId) }) - it('depositERC721() escrows the deposit and sends the correct deposit message', async () => { + it('bridgeERC721() escrows the deposit and sends the correct deposit message', async () => { // alice calls deposit on the bridge and the L1 bridge calls transferFrom on the token. - // emits an ERC721DepositInitiated event with the correct arguments. + // emits an ERC721BridgeInitiated event with the correct arguments. await expect( - L1ERC721Bridge.connect(alice).depositERC721( + L1ERC721Bridge.connect(alice).bridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, tokenId, @@ -101,7 +101,7 @@ describe('L1ERC721Bridge', () => { NON_NULL_BYTES32 ) ) - .to.emit(L1ERC721Bridge, 'ERC721DepositInitiated') + .to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, @@ -129,9 +129,9 @@ describe('L1ERC721Bridge', () => { // the L1 bridge sends the correct message to the L1 messenger expect(depositCallToMessenger.args[1]).to.equal( - IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [ - L1ERC721.address, + IL2ERC721Bridge.encodeFunctionData('finalizeBridgeERC721', [ DUMMY_L2_ERC721_ADDRESS, + L1ERC721.address, aliceAddress, aliceAddress, tokenId, @@ -150,11 +150,11 @@ describe('L1ERC721Bridge', () => { ).to.equal(true) }) - it('depositERC721To() escrows the deposited NFT and sends the correct deposit message', async () => { + it('bridgeERC721To() escrows the deposited NFT and sends the correct deposit message', async () => { // depositor calls deposit on the bridge and the L1 bridge calls transferFrom on the token. - // emits an ERC721DepositInitiated event with the correct arguments. + // emits an ERC721BridgeInitiated event with the correct arguments. await expect( - L1ERC721Bridge.connect(alice).depositERC721To( + L1ERC721Bridge.connect(alice).bridgeERC721To( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, bobsAddress, @@ -163,7 +163,7 @@ describe('L1ERC721Bridge', () => { NON_NULL_BYTES32 ) ) - .to.emit(L1ERC721Bridge, 'ERC721DepositInitiated') + .to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, @@ -195,9 +195,9 @@ describe('L1ERC721Bridge', () => { // the L1 bridge sends the correct message to the L1 messenger expect(depositCallToMessenger.args[1]).to.equal( - IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [ - L1ERC721.address, + IL2ERC721Bridge.encodeFunctionData('finalizeBridgeERC721', [ DUMMY_L2_ERC721_ADDRESS, + L1ERC721.address, aliceAddress, bobsAddress, tokenId, @@ -216,22 +216,22 @@ describe('L1ERC721Bridge', () => { ).to.equal(true) }) - it('cannot depositERC721 from a contract account', async () => { + it('cannot bridgeERC721 from a contract account', async () => { await expect( - L1ERC721Bridge.depositERC721( + L1ERC721Bridge.bridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, tokenId, FINALIZATION_GAS, NON_NULL_BYTES32 ) - ).to.be.revertedWith('Account not EOA') + ).to.be.revertedWith('L1ERC721Bridge: account is not externally owned') }) describe('Handling ERC721.transferFrom() failures that revert', () => { - it('depositERC721(): will revert if ERC721.transferFrom() reverts', async () => { + it('bridgeERC721(): will revert if ERC721.transferFrom() reverts', async () => { await expect( - L1ERC721Bridge.connect(bob).depositERC721To( + L1ERC721Bridge.connect(bob).bridgeERC721To( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, bobsAddress, @@ -242,9 +242,9 @@ describe('L1ERC721Bridge', () => { ).to.be.revertedWith('ERC721: transfer from incorrect owner') }) - it('depositERC721To(): will revert if ERC721.transferFrom() reverts', async () => { + it('bridgeERC721To(): will revert if ERC721.transferFrom() reverts', async () => { await expect( - L1ERC721Bridge.connect(bob).depositERC721To( + L1ERC721Bridge.connect(bob).bridgeERC721To( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, bobsAddress, @@ -255,9 +255,9 @@ describe('L1ERC721Bridge', () => { ).to.be.revertedWith('ERC721: transfer from incorrect owner') }) - it('depositERC721To(): will revert if the L1 ERC721 is zero address', async () => { + it('bridgeERC721To(): will revert if the L1 ERC721 is zero address', async () => { await expect( - L1ERC721Bridge.connect(alice).depositERC721To( + L1ERC721Bridge.connect(alice).bridgeERC721To( constants.AddressZero, DUMMY_L2_ERC721_ADDRESS, bobsAddress, @@ -268,9 +268,9 @@ describe('L1ERC721Bridge', () => { ).to.be.revertedWith('function call to a non-contract account') }) - it('depositERC721To(): will revert if the L1 ERC721 has no code', async () => { + it('bridgeERC721To(): will revert if the L1 ERC721 has no code', async () => { await expect( - L1ERC721Bridge.connect(alice).depositERC721To( + L1ERC721Bridge.connect(alice).bridgeERC721To( bobsAddress, DUMMY_L2_ERC721_ADDRESS, bobsAddress, @@ -286,7 +286,7 @@ describe('L1ERC721Bridge', () => { describe('ERC721 withdrawals', () => { it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { await expect( - L1ERC721Bridge.connect(alice).finalizeERC721Withdrawal( + L1ERC721Bridge.connect(alice).finalizeBridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, constants.AddressZero, @@ -299,7 +299,7 @@ describe('L1ERC721Bridge', () => { it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC721)', async () => { await expect( - L1ERC721Bridge.finalizeERC721Withdrawal( + L1ERC721Bridge.finalizeBridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, constants.AddressZero, @@ -318,7 +318,7 @@ describe('L1ERC721Bridge', () => { // First Alice will send an NFT so that there's a balance to be withdrawn await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId) - await L1ERC721Bridge.connect(alice).depositERC721( + await L1ERC721Bridge.connect(alice).bridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, tokenId, @@ -336,7 +336,7 @@ describe('L1ERC721Bridge', () => { it('should revert if the l1/l2 token pair has a token ID that has not been escrowed in the l1 bridge', async () => { await expect( - L1ERC721Bridge.finalizeERC721Withdrawal( + L1ERC721Bridge.finalizeBridgeERC721( L1ERC721.address, DUMMY_L2_BRIDGE_ADDRESS, // incorrect l2 token address constants.AddressZero, @@ -351,9 +351,9 @@ describe('L1ERC721Bridge', () => { }) it('should credit funds to the withdrawer and not use too much gas', async () => { - // finalizing the withdrawal emits an ERC721DepositInitiated event with the correct arguments. + // finalizing the withdrawal emits an ERC721BridgeFinalized event with the correct arguments. await expect( - L1ERC721Bridge.finalizeERC721Withdrawal( + L1ERC721Bridge.finalizeBridgeERC721( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, NON_ZERO_ADDRESS, @@ -363,7 +363,7 @@ describe('L1ERC721Bridge', () => { { from: Fake__L1CrossDomainMessenger.address } ) ) - .to.emit(L1ERC721Bridge, 'ERC721WithdrawalFinalized') + .to.emit(L1ERC721Bridge, 'ERC721BridgeFinalized') .withArgs( L1ERC721.address, DUMMY_L2_ERC721_ADDRESS, diff --git a/packages/contracts-periphery/test/contracts/L2/messaging/L2ERC721Bridge.spec.ts b/packages/contracts-periphery/test/contracts/L2/messaging/L2ERC721Bridge.spec.ts index b556bb19ff2c45bb1e411526f0bad313cee24f39..f9b4d2fbace68e538a46bc7f6b48d5febcd62d95 100644 --- a/packages/contracts-periphery/test/contracts/L2/messaging/L2ERC721Bridge.spec.ts +++ b/packages/contracts-periphery/test/contracts/L2/messaging/L2ERC721Bridge.spec.ts @@ -10,7 +10,7 @@ import { NON_ZERO_ADDRESS, } from '../../../../../contracts/test/helpers' -const ERR_ALREADY_INITIALIZED = 'Contract has already been initialized.' +const ERR_ALREADY_INITIALIZED = 'Initializable: contract is already initialized' const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' const ERR_INVALID_X_DOMAIN_MSG_SENDER = 'OVM_XCHAIN: wrong sender of cross-domain message' @@ -53,14 +53,11 @@ describe('L2ERC721Bridge', () => { // Deploy the contract under test L2ERC721Bridge = await ( await ethers.getContractFactory('L2ERC721Bridge') - ).deploy(Fake__L2CrossDomainMessenger.address) - - // Initialize the contract - await L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS) + ).deploy(Fake__L2CrossDomainMessenger.address, DUMMY_L1BRIDGE_ADDRESS) // Deploy an L2 ERC721 L2ERC721 = await ( - await ethers.getContractFactory('L2StandardERC721') + await ethers.getContractFactory('OptimismMintableERC721') ).deploy( L2ERC721Bridge.address, DUMMY_L1ERC721_ADDRESS, @@ -73,16 +70,19 @@ describe('L2ERC721Bridge', () => { describe('initialize', () => { it('Should only be callable once', async () => { await expect( - L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS) + L2ERC721Bridge.initialize( + Fake__L2CrossDomainMessenger.address, + DUMMY_L1BRIDGE_ADDRESS + ) ).to.be.revertedWith(ERR_ALREADY_INITIALIZED) }) }) // test the transfer flow of moving a token from L1 to L2 - describe('finalizeERC721Deposit', () => { + describe('finalizeBridgeERC721', () => { it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { await expect( - L2ERC721Bridge.connect(alice).finalizeERC721Deposit( + L2ERC721Bridge.connect(alice).finalizeBridgeERC721( DUMMY_L1ERC721_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, @@ -99,7 +99,7 @@ describe('L2ERC721Bridge', () => { ) await expect( - L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( + L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721( DUMMY_L1ERC721_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, @@ -125,11 +125,11 @@ describe('L2ERC721Bridge', () => { DUMMY_L1BRIDGE_ADDRESS ) - // A failed attempt to finalize the deposit causes an ERC721DepositFailed event to be emitted. + // A failed attempt to finalize the deposit causes an ERC721BridgeFailed event to be emitted. await expect( - L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( - DUMMY_L1ERC721_ADDRESS, + L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721( NonCompliantERC721.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, @@ -139,10 +139,10 @@ describe('L2ERC721Bridge', () => { } ) ) - .to.emit(L2ERC721Bridge, 'ERC721DepositFailed') + .to.emit(L2ERC721Bridge, 'ERC721BridgeFailed') .withArgs( - DUMMY_L1ERC721_ADDRESS, NonCompliantERC721.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, @@ -155,7 +155,7 @@ describe('L2ERC721Bridge', () => { expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS) expect(withdrawalCallToMessenger.args[1]).to.equal( Factory__L1ERC721Bridge.interface.encodeFunctionData( - 'finalizeERC721Withdrawal', + 'finalizeBridgeERC721', [ DUMMY_L1ERC721_ADDRESS, NonCompliantERC721.address, @@ -181,9 +181,9 @@ describe('L2ERC721Bridge', () => { // Successfully finalizes the deposit. const expectedResult = expect( - L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( - DUMMY_L1ERC721_ADDRESS, + L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721( L2ERC721.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, @@ -194,12 +194,12 @@ describe('L2ERC721Bridge', () => { ) ) - // Depositing causes an ERC721DepositFinalized event to be emitted. + // Depositing causes an ERC721BridgeFinalized event to be emitted. await expectedResult.to - .emit(L2ERC721Bridge, 'ERC721DepositFinalized') + .emit(L2ERC721Bridge, 'ERC721BridgeFinalized') .withArgs( - DUMMY_L1ERC721_ADDRESS, L2ERC721.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, @@ -222,7 +222,7 @@ describe('L2ERC721Bridge', () => { beforeEach(async () => { Mock__L2Token = await ( - await smock.mock('L2StandardERC721') + await smock.mock('OptimismMintableERC721') ).deploy( L2ERC721Bridge.address, DUMMY_L1ERC721_ADDRESS, @@ -239,10 +239,11 @@ describe('L2ERC721Bridge', () => { }) }) - it('withdrawERC721() reverts when called by non-owner of nft', async () => { + it('bridgeERC721() reverts when called by non-owner of nft', async () => { await expect( - L2ERC721Bridge.connect(bob).withdrawERC721( + L2ERC721Bridge.connect(bob).bridgeERC721( Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 @@ -250,37 +251,39 @@ describe('L2ERC721Bridge', () => { ).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) }) - it('withdrawERC721() reverts if called by a contract', async () => { + it('bridgeERC721() reverts if called by a contract', async () => { await expect( - L2ERC721Bridge.connect(l2MessengerImpersonator).withdrawERC721( + L2ERC721Bridge.connect(l2MessengerImpersonator).bridgeERC721( Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 ) - ).to.be.revertedWith('Account not EOA') + ).to.be.revertedWith('L2ERC721Bridge: account is not externally owned') }) - it('withdrawERC721() burns and sends the correct withdrawal message', async () => { + it('bridgeERC721() burns and sends the correct withdrawal message', async () => { // Make sure that alice begins as the NFT owner expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) // Initiates a successful withdrawal. const expectedResult = expect( - L2ERC721Bridge.connect(alice).withdrawERC721( + L2ERC721Bridge.connect(alice).bridgeERC721( Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 ) ) - // A successful withdrawal causes an ERC721WithdrawalInitiated event to be emitted from the L2 ERC721 Bridge. + // A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge. await expectedResult.to - .emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated') + .emit(L2ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( - DUMMY_L1ERC721_ADDRESS, Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, aliceAddress, TOKEN_ID, @@ -311,7 +314,7 @@ describe('L2ERC721Bridge', () => { // Message data should be a call telling the L1ERC721Bridge to finalize the withdrawal expect(withdrawalCallToMessenger.args[1]).to.equal( Factory__L1ERC721Bridge.interface.encodeFunctionData( - 'finalizeERC721Withdrawal', + 'finalizeBridgeERC721', [ DUMMY_L1ERC721_ADDRESS, Mock__L2Token.address, @@ -326,10 +329,11 @@ describe('L2ERC721Bridge', () => { expect(withdrawalCallToMessenger.args[2]).to.equal(0) }) - it('withdrawERC721To() reverts when called by non-owner of nft', async () => { + it('bridgeERC721To() reverts when called by non-owner of nft', async () => { await expect( - L2ERC721Bridge.connect(bob).withdrawERC721To( + L2ERC721Bridge.connect(bob).bridgeERC721To( Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, bobsAddress, TOKEN_ID, 0, @@ -338,14 +342,15 @@ describe('L2ERC721Bridge', () => { ).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) }) - it('withdrawERC721To() burns and sends the correct withdrawal message', async () => { + it('bridgeERC721To() burns and sends the correct withdrawal message', async () => { // Make sure that alice begins as the NFT owner expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) // Initiates a successful withdrawal. const expectedResult = expect( - L2ERC721Bridge.connect(alice).withdrawERC721To( + L2ERC721Bridge.connect(alice).bridgeERC721To( Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, bobsAddress, TOKEN_ID, 0, @@ -353,12 +358,12 @@ describe('L2ERC721Bridge', () => { ) ) - // A successful withdrawal causes an ERC721WithdrawalInitiated event to be emitted from the L2 ERC721 Bridge. + // A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge. await expectedResult.to - .emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated') + .emit(L2ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( - DUMMY_L1ERC721_ADDRESS, Mock__L2Token.address, + DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, @@ -389,7 +394,7 @@ describe('L2ERC721Bridge', () => { // The message data should be a call telling the L1ERC721Bridge to finalize the withdrawal expect(withdrawalCallToMessenger.args[1]).to.equal( Factory__L1ERC721Bridge.interface.encodeFunctionData( - 'finalizeERC721Withdrawal', + 'finalizeBridgeERC721', [ DUMMY_L1ERC721_ADDRESS, Mock__L2Token.address, diff --git a/packages/contracts-periphery/test/contracts/libraries/utils/Lib_Strings.spec.ts b/packages/contracts-periphery/test/contracts/libraries/utils/Lib_Strings.spec.ts deleted file mode 100644 index 6d8b9dbc0720478e70da6490c223119ebdacc0f0..0000000000000000000000000000000000000000 --- a/packages/contracts-periphery/test/contracts/libraries/utils/Lib_Strings.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ethers } from 'hardhat' -import { Contract } from 'ethers' - -import { expect } from '../../../setup' -import { deploy } from '../../../helpers' - -const DUMMY_ADDRESS = ethers.utils.getAddress('0x' + 'abba'.repeat(10)) - -describe('Lib_Strings', () => { - let TestLib_Strings: Contract - before(async () => { - TestLib_Strings = await deploy('TestLib_Strings') - }) - - describe('addressToString', () => { - it('should return a string type', () => { - // uses the contract interface to find the function's return type - const returnType = - TestLib_Strings.interface.functions['addressToString(address)'] - .outputs[0].type - - expect(returnType).to.equal('string') - }) - - it('should convert an address to a lowercase ascii string without the 0x prefix', async () => { - const asciiString = DUMMY_ADDRESS.substring(2).toLowerCase() - - expect(await TestLib_Strings.addressToString(DUMMY_ADDRESS)).to.equal( - asciiString - ) - }) - }) - - describe('hexCharToAscii', () => { - for (let hex = 0; hex < 16; hex++) { - it(`should convert the hex character ${hex} to its ascii representation`, async () => { - // converts hex characters to ascii in decimal representation - const asciiDecimal = - hex < 10 - ? hex + 48 // 48 is 0x30 in decimal - : hex + 87 // 87 is 0x57 in decimal - - // converts decimal value to hexadecimal and prepends '0x' - const asciiHexadecimal = '0x' + asciiDecimal.toString(16) - - expect(await TestLib_Strings.hexCharToAscii(hex)).to.equal( - asciiHexadecimal - ) - }) - } - }) -}) diff --git a/packages/contracts-periphery/test/contracts/standards/L2StandardERC721.spec.ts b/packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721.spec.ts similarity index 58% rename from packages/contracts-periphery/test/contracts/standards/L2StandardERC721.spec.ts rename to packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721.spec.ts index dcc23fbb8a1c48df307d29d0b2ff4fbe070453d6..45bbdc509f4abd469bebe8bb699ef4444ca01bf9 100644 --- a/packages/contracts-periphery/test/contracts/standards/L2StandardERC721.spec.ts +++ b/packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721.spec.ts @@ -10,11 +10,11 @@ const TOKEN_ID = 10 const DUMMY_L1ERC721_ADDRESS: string = '0x2234223412342234223422342234223422342234' -describe('L2StandardERC721', () => { +describe('OptimismMintableERC721', () => { let l2BridgeImpersonator: Signer let alice: Signer let Fake__L2ERC721Bridge: FakeContract - let L2StandardERC721: Contract + let OptimismMintableERC721: Contract let l2BridgeImpersonatorAddress: string let aliceAddress: string let baseUri: string @@ -34,8 +34,8 @@ describe('L2StandardERC721', () => { '/tokenURI?uint256=' ) - L2StandardERC721 = await ( - await ethers.getContractFactory('L2StandardERC721') + OptimismMintableERC721 = await ( + await ethers.getContractFactory('OptimismMintableERC721') ).deploy( l2BridgeImpersonatorAddress, DUMMY_L1ERC721_ADDRESS, @@ -52,7 +52,7 @@ describe('L2StandardERC721', () => { ) // mint an nft to alice - await L2StandardERC721.connect(l2BridgeImpersonator).mint( + await OptimismMintableERC721.connect(l2BridgeImpersonator).mint( aliceAddress, TOKEN_ID, { @@ -63,56 +63,62 @@ describe('L2StandardERC721', () => { describe('constructor', () => { it('should be able to create a standard ERC721 contract with the correct parameters', async () => { - expect(await L2StandardERC721.l2Bridge()).to.equal( + expect(await OptimismMintableERC721.bridge()).to.equal( l2BridgeImpersonatorAddress ) - expect(await L2StandardERC721.l1Token()).to.equal(DUMMY_L1ERC721_ADDRESS) - expect(await L2StandardERC721.name()).to.equal('L2ERC721') - expect(await L2StandardERC721.symbol()).to.equal('ERC') - expect(await L2StandardERC721.baseTokenURI()).to.equal(baseUri) + expect(await OptimismMintableERC721.remoteToken()).to.equal( + DUMMY_L1ERC721_ADDRESS + ) + expect(await OptimismMintableERC721.name()).to.equal('L2ERC721') + expect(await OptimismMintableERC721.symbol()).to.equal('ERC') + expect(await OptimismMintableERC721.baseTokenURI()).to.equal(baseUri) // alice has been minted an nft - expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress) + expect(await OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal( + aliceAddress + ) }) }) describe('mint and burn', () => { it('should not allow anyone but the L2 bridge to mint and burn', async () => { await expect( - L2StandardERC721.connect(alice).mint(aliceAddress, 100) - ).to.be.revertedWith('Only L2 Bridge can mint and burn') + OptimismMintableERC721.connect(alice).mint(aliceAddress, 100) + ).to.be.revertedWith( + 'OptimismMintableERC721: only bridge can call this function' + ) await expect( - L2StandardERC721.connect(alice).burn(aliceAddress, 100) - ).to.be.revertedWith('Only L2 Bridge can mint and burn') + OptimismMintableERC721.connect(alice).burn(aliceAddress, 100) + ).to.be.revertedWith( + 'OptimismMintableERC721: only bridge can call this function' + ) }) }) describe('supportsInterface', () => { it('should return the correct interface support', async () => { - const supportsERC165 = await L2StandardERC721.supportsInterface( - 0x01ffc9a7 - ) - expect(supportsERC165).to.be.true + // ERC165 + expect(await OptimismMintableERC721.supportsInterface(0x01ffc9a7)).to.be + .true - const supportsL2TokenInterface = await L2StandardERC721.supportsInterface( - 0x1d1d8b63 - ) - expect(supportsL2TokenInterface).to.be.true + // OptimismMintablERC721 + expect(await OptimismMintableERC721.supportsInterface(0xec4fc8e3)).to.be + .true - const supportsERC721Interface = await L2StandardERC721.supportsInterface( - 0x80ac58cd - ) - expect(supportsERC721Interface).to.be.true + // ERC721 + expect(await OptimismMintableERC721.supportsInterface(0x80ac58cd)).to.be + .true - const badSupports = await L2StandardERC721.supportsInterface(0xffffffff) - expect(badSupports).to.be.false + // Some bad interface + expect(await OptimismMintableERC721.supportsInterface(0xffffffff)).to.be + .false }) }) describe('tokenURI', () => { it('should return the correct token uri', async () => { const tokenUri = baseUri.concat(TOKEN_ID.toString()) - expect(await L2StandardERC721.tokenURI(TOKEN_ID)).to.equal(tokenUri) + expect(await OptimismMintableERC721.tokenURI(TOKEN_ID)).to.equal(tokenUri) }) }) }) diff --git a/packages/contracts-periphery/test/contracts/L2/messaging/L2StandardERC721Factory.spec.ts b/packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721Factory.spec.ts similarity index 50% rename from packages/contracts-periphery/test/contracts/L2/messaging/L2StandardERC721Factory.spec.ts rename to packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721Factory.spec.ts index 17932675a53e8bb6655145b9f1b9366d0d5ba0e5..3a82f4b8b10e2be854e644fbcdb5e96cdabd7349 100644 --- a/packages/contracts-periphery/test/contracts/L2/messaging/L2StandardERC721Factory.spec.ts +++ b/packages/contracts-periphery/test/contracts/universal/OptimismMintableERC721Factory.spec.ts @@ -8,17 +8,17 @@ import { } from '@defi-wonderland/smock' /* Internal Imports */ -import { expect } from '../../../setup' +import { expect } from '../../setup' const DUMMY_L2_BRIDGE_ADDRESS: string = ethers.utils.getAddress( '0x' + 'acdc'.repeat(10) ) -describe('L2StandardERC721Factory', () => { +describe('OptimismMintableERC721Factory', () => { let signer: Signer let Factory__L1ERC721: MockContractFactory<ContractFactory> let L1ERC721: MockContract<Contract> - let L2StandardERC721Factory: Contract + let OptimismMintableERC721Factory: Contract let baseURI: string let chainId: number @@ -31,8 +31,8 @@ describe('L2StandardERC721Factory', () => { ) L1ERC721 = await Factory__L1ERC721.deploy('L1ERC721', 'ERC') - L2StandardERC721Factory = await ( - await ethers.getContractFactory('L2StandardERC721Factory') + OptimismMintableERC721Factory = await ( + await ethers.getContractFactory('OptimismMintableERC721Factory') ).deploy(DUMMY_L2_BRIDGE_ADDRESS) chainId = await signer.getChainId() @@ -46,71 +46,60 @@ describe('L2StandardERC721Factory', () => { }) it('should be deployed with the correct constructor argument', async () => { - expect(await L2StandardERC721Factory.l2ERC721Bridge()).to.equal( + expect(await OptimismMintableERC721Factory.bridge()).to.equal( DUMMY_L2_BRIDGE_ADDRESS ) }) it('should be able to create a standard ERC721 contract', async () => { - const tx = await L2StandardERC721Factory.createStandardL2ERC721( - L1ERC721.address, - 'L2ERC721', - 'ERC' - ) + const tx = + await OptimismMintableERC721Factory.createStandardOptimismMintableERC721( + L1ERC721.address, + 'L2ERC721', + 'ERC' + ) const receipt = await tx.wait() - // Get the StandardL2ERC721Created event + // Get the OptimismMintableERC721Created event const erc721CreatedEvent = receipt.events[0] // Expect there to be an event emitted for the standard token creation - expect(erc721CreatedEvent.event).to.be.eq('StandardL2ERC721Created') + expect(erc721CreatedEvent.event).to.be.eq('OptimismMintableERC721Created') // Get the L2 ERC721 address from the emitted event and check it was created correctly - const l2ERC721Address = erc721CreatedEvent.args._l2Token - const L2StandardERC721 = new Contract( + const l2ERC721Address = erc721CreatedEvent.args.localToken + const OptimismMintableERC721 = new Contract( l2ERC721Address, - (await ethers.getContractFactory('L2StandardERC721')).interface, + (await ethers.getContractFactory('OptimismMintableERC721')).interface, signer ) - expect(await L2StandardERC721.l2Bridge()).to.equal(DUMMY_L2_BRIDGE_ADDRESS) - expect(await L2StandardERC721.l1Token()).to.equal(L1ERC721.address) - expect(await L2StandardERC721.name()).to.equal('L2ERC721') - expect(await L2StandardERC721.symbol()).to.equal('ERC') - expect(await L2StandardERC721.baseTokenURI()).to.equal(baseURI) + expect(await OptimismMintableERC721.bridge()).to.equal( + DUMMY_L2_BRIDGE_ADDRESS + ) + expect(await OptimismMintableERC721.remoteToken()).to.equal( + L1ERC721.address + ) + expect(await OptimismMintableERC721.name()).to.equal('L2ERC721') + expect(await OptimismMintableERC721.symbol()).to.equal('ERC') + expect(await OptimismMintableERC721.baseTokenURI()).to.equal(baseURI) expect( - await L2StandardERC721Factory.isStandardERC721(L2StandardERC721.address) + await OptimismMintableERC721Factory.isStandardOptimismMintableERC721( + OptimismMintableERC721.address + ) ).to.equal(true) - expect( - await L2StandardERC721Factory.standardERC721Mapping(L1ERC721.address) - ).to.equal(l2ERC721Address) }) it('should not be able to create a standard token with a 0 address for l1 token', async () => { await expect( - L2StandardERC721Factory.createStandardL2ERC721( + OptimismMintableERC721Factory.createStandardOptimismMintableERC721( ethers.constants.AddressZero, 'L2ERC721', 'ERC' ) - ).to.be.revertedWith('Must provide L1 token address') - }) - - it('should not be able create two l2 standard tokens with the same l1 token', async () => { - // The first call will not revert - await L2StandardERC721Factory.createStandardL2ERC721( - L1ERC721.address, - 'L2ERC721', - 'ERC' + ).to.be.revertedWith( + 'OptimismMintableERC721Factory: L1 token address cannot be address(0)' ) - - await expect( - L2StandardERC721Factory.createStandardL2ERC721( - L1ERC721.address, - 'L2ERC721', - 'ERC' - ) - ).to.be.revertedWith('L2 Standard Token already exists for this L1 Token') }) }) diff --git a/yarn.lock b/yarn.lock index 0637d1b1e696aec3e87a66037875d151472a1cc5..3971985cac009194b9c0e42424ff77c5ac8c68ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2812,16 +2812,16 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.3.2.tgz#92df481362e366c388fc02133cf793029c744cea" integrity sha512-i/pOaOtcqDk4UqsrOv735uYyTbn6dvfiuVu5hstsgV6c4ZKUtu88/31zT2BzkCg+3JfcwOfgg2TtRKVKKZIGkQ== +"@openzeppelin/contracts-upgradeable@4.6.0", "@openzeppelin/contracts-upgradeable@^4.5.2": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.6.0.tgz#1bf55f230f008554d4c6fe25eb165b85112108b0" + integrity sha512-5OnVuO4HlkjSCJO165a4i2Pu1zQGzMs//o54LPrwUgxvEO2P3ax1QuaSI0cEHHTveA77guS0PnNugpR2JMsPfA== + "@openzeppelin/contracts-upgradeable@^4.3.2": version "4.4.0" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.4.0.tgz#85161d87c840c5bce2b6ed0c727b407e774852ae" integrity sha512-hIEyWJHu7bDTv6ckxOaV+K3+7mVzhjtyvp3QSaz56Rk5PscXtPAbkiNTb3yz6UJCWHPWpxVyULVgZ6RubuFEZg== -"@openzeppelin/contracts-upgradeable@^4.5.2": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.6.0.tgz#1bf55f230f008554d4c6fe25eb165b85112108b0" - integrity sha512-5OnVuO4HlkjSCJO165a4i2Pu1zQGzMs//o54LPrwUgxvEO2P3ax1QuaSI0cEHHTveA77guS0PnNugpR2JMsPfA== - "@openzeppelin/contracts@3.4.1-solc-0.7-2": version "3.4.1-solc-0.7-2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92"