Commit 61a30273 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(ctp): simplify and standardize ERC721 bridge (#2773)

* feat(ctp): simplify and standardize ERC721 bridge

Significantly simplifies the ERC721 bridge contracts and standardizes
them according to our new seaport-style standard. Removes some
unnecessary code and updates the interface for the bridge to match the
expected interface for the ERC20 bridge after Bedrock.

* integration-tests: fixup

* integration-tests: fix constructor

* fix: circular dep in itests

* tests: fixes

* fix(ctp): add version to contracts

* fix: test failures

* fix: tests

* itests: additional check

* whoops

* fix: address review feedback

* fix: rename data to extraData
Co-authored-by: default avatarMark Tyneway <mark.tyneway@gmail.com>
parent 8a8efd51
---
'@eth-optimism/contracts-periphery': patch
---
Simplify, cleanup, and standardize ERC721 bridge contracts.
...@@ -3,14 +3,14 @@ pragma solidity ^0.8.9; ...@@ -3,14 +3,14 @@ pragma solidity ^0.8.9;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract FakeL2StandardERC721 is ERC721 { contract FakeOptimismMintableERC721 is ERC721 {
address public immutable l1Token; address public immutable remoteToken;
address public immutable l2Bridge; address public immutable bridge;
constructor(address _l1Token, address _l2Bridge) ERC721("FakeERC721", "FAKE") { constructor(address _remoteToken, address _bridge) ERC721("FakeERC721", "FAKE") {
l1Token = _l1Token; remoteToken = _remoteToken;
l2Bridge = _l2Bridge; bridge = _bridge;
} }
function mint(address to, uint256 tokenId) public { function mint(address to, uint256 tokenId) public {
......
...@@ -5,8 +5,8 @@ import { predeploys } from '@eth-optimism/contracts' ...@@ -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__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__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__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__OptimismMintableERC721Factory from '@eth-optimism/contracts-periphery/artifacts/contracts/universal/op-erc721/OptimismMintableERC721Factory.sol/OptimismMintableERC721Factory.json'
import Artifact__L2StandardERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/standards/L2StandardERC721.sol/L2StandardERC721.json' import Artifact__OptimismMintableERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/universal/op-erc721/OptimismMintableERC721.sol/OptimismMintableERC721.json'
/* Imports: Internal */ /* Imports: Internal */
import { expect } from './shared/setup' import { expect } from './shared/setup'
...@@ -49,7 +49,7 @@ describe('ERC721 Bridge', () => { ...@@ -49,7 +49,7 @@ describe('ERC721 Bridge', () => {
let Factory__L1ERC721: ContractFactory let Factory__L1ERC721: ContractFactory
let Factory__L1ERC721Bridge: ContractFactory let Factory__L1ERC721Bridge: ContractFactory
let Factory__L2ERC721Bridge: ContractFactory let Factory__L2ERC721Bridge: ContractFactory
let Factory__L2StandardERC721Factory: ContractFactory let Factory__OptimismMintableERC721Factory: ContractFactory
before(async () => { before(async () => {
Factory__L1ERC721 = await ethers.getContractFactory( Factory__L1ERC721 = await ethers.getContractFactory(
Artifact__TestERC721.abi, Artifact__TestERC721.abi,
...@@ -66,9 +66,9 @@ describe('ERC721 Bridge', () => { ...@@ -66,9 +66,9 @@ describe('ERC721 Bridge', () => {
Artifact__L2ERC721Bridge.bytecode, Artifact__L2ERC721Bridge.bytecode,
bobWalletL2 bobWalletL2
) )
Factory__L2StandardERC721Factory = await ethers.getContractFactory( Factory__OptimismMintableERC721Factory = await ethers.getContractFactory(
Artifact__L2StandardERC721Factory.abi, Artifact__OptimismMintableERC721Factory.abi,
Artifact__L2StandardERC721Factory.bytecode, Artifact__OptimismMintableERC721Factory.bytecode,
bobWalletL2 bobWalletL2
) )
}) })
...@@ -76,48 +76,58 @@ describe('ERC721 Bridge', () => { ...@@ -76,48 +76,58 @@ describe('ERC721 Bridge', () => {
let L1ERC721: Contract let L1ERC721: Contract
let L1ERC721Bridge: Contract let L1ERC721Bridge: Contract
let L2ERC721Bridge: Contract let L2ERC721Bridge: Contract
let L2StandardERC721Factory: Contract let OptimismMintableERC721Factory: Contract
let L2StandardERC721: Contract let OptimismMintableERC721: Contract
beforeEach(async () => { beforeEach(async () => {
L1ERC721 = await Factory__L1ERC721.deploy() L1ERC721 = await Factory__L1ERC721.deploy()
await L1ERC721.deployed() await L1ERC721.deployed()
L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy(
predeploys.L2CrossDomainMessenger
)
await L2ERC721Bridge.deployed()
L1ERC721Bridge = await Factory__L1ERC721Bridge.deploy( L1ERC721Bridge = await Factory__L1ERC721Bridge.deploy(
env.messenger.contracts.l1.L1CrossDomainMessenger.address, 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() 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 L2ERC721Bridge.address
) )
await L2StandardERC721Factory.deployed()
// Create a L2 Standard ERC721 with the Standard ERC721 Factory // Create a L2 Standard ERC721 with the Standard ERC721 Factory
const tx = await L2StandardERC721Factory.createStandardL2ERC721( const tx =
L1ERC721.address, await OptimismMintableERC721Factory.createStandardOptimismMintableERC721(
'L2ERC721', L1ERC721.address,
'L2' 'L2ERC721',
) 'L2'
await tx.wait() )
const receipt = await tx.wait()
// Retrieve the deployed L2 Standard ERC721 // Get the OptimismMintableERC721Created event
const L2StandardERC721Address = const erc721CreatedEvent = receipt.events[0]
await L2StandardERC721Factory.standardERC721Mapping(L1ERC721.address) expect(erc721CreatedEvent.event).to.be.eq('OptimismMintableERC721Created')
L2StandardERC721 = await ethers.getContractAt(
Artifact__L2StandardERC721.abi,
L2StandardERC721Address
)
await L2StandardERC721.deployed()
// Initialize the L2 bridge contract OptimismMintableERC721 = await ethers.getContractAt(
const tx1 = await L2ERC721Bridge.initialize(L1ERC721Bridge.address) Artifact__OptimismMintableERC721.abi,
await tx1.wait() erc721CreatedEvent.args.localToken
)
await OptimismMintableERC721.deployed()
// Mint an L1 ERC721 to Bob on L1 // Mint an L1 ERC721 to Bob on L1
const tx2 = await L1ERC721.mint(bobAddress, TOKEN_ID) const tx2 = await L1ERC721.mint(bobAddress, TOKEN_ID)
...@@ -128,11 +138,11 @@ describe('ERC721 Bridge', () => { ...@@ -128,11 +138,11 @@ describe('ERC721 Bridge', () => {
await tx3.wait() await tx3.wait()
}) })
it('depositERC721', async () => { it('bridgeERC721', async () => {
await env.messenger.waitForMessageReceipt( await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721( await L1ERC721Bridge.bridgeERC721(
L1ERC721.address, L1ERC721.address,
L2StandardERC721.address, OptimismMintableERC721.address,
TOKEN_ID, TOKEN_ID,
FINALIZATION_GAS, FINALIZATION_GAS,
NON_NULL_BYTES NON_NULL_BYTES
...@@ -143,14 +153,14 @@ describe('ERC721 Bridge', () => { ...@@ -143,14 +153,14 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
// Bob owns the NFT on L2 // 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 env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721To( await L1ERC721Bridge.bridgeERC721To(
L1ERC721.address, L1ERC721.address,
L2StandardERC721.address, OptimismMintableERC721.address,
aliceAddress, aliceAddress,
TOKEN_ID, TOKEN_ID,
FINALIZATION_GAS, FINALIZATION_GAS,
...@@ -162,15 +172,17 @@ describe('ERC721 Bridge', () => { ...@@ -162,15 +172,17 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
// Alice owns the NFT on L2 // 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 // Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt( await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721( await L1ERC721Bridge.bridgeERC721(
L1ERC721.address, L1ERC721.address,
L2StandardERC721.address, OptimismMintableERC721.address,
TOKEN_ID, TOKEN_ID,
FINALIZATION_GAS, FINALIZATION_GAS,
NON_NULL_BYTES NON_NULL_BYTES
...@@ -181,10 +193,11 @@ describe('ERC721 Bridge', () => { ...@@ -181,10 +193,11 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
// Also check that Bob owns the NFT on L2 initially // 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( const tx = await L2ERC721Bridge.bridgeERC721(
L2StandardERC721.address, OptimismMintableERC721.address,
L1ERC721.address,
TOKEN_ID, TOKEN_ID,
0, 0,
NON_NULL_BYTES NON_NULL_BYTES
...@@ -196,15 +209,15 @@ describe('ERC721 Bridge', () => { ...@@ -196,15 +209,15 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)
// L2 NFT is burned // 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 // Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt( await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721( await L1ERC721Bridge.bridgeERC721(
L1ERC721.address, L1ERC721.address,
L2StandardERC721.address, OptimismMintableERC721.address,
TOKEN_ID, TOKEN_ID,
FINALIZATION_GAS, FINALIZATION_GAS,
NON_NULL_BYTES NON_NULL_BYTES
...@@ -215,10 +228,11 @@ describe('ERC721 Bridge', () => { ...@@ -215,10 +228,11 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
// Also check that Bob owns the NFT on L2 initially // 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( const tx = await L2ERC721Bridge.bridgeERC721To(
L2StandardERC721.address, OptimismMintableERC721.address,
L1ERC721.address,
aliceAddress, aliceAddress,
TOKEN_ID, TOKEN_ID,
0, 0,
...@@ -231,7 +245,7 @@ describe('ERC721 Bridge', () => { ...@@ -231,7 +245,7 @@ describe('ERC721 Bridge', () => {
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress) expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// L2 NFT is burned // L2 NFT is burned
await expect(L2StandardERC721.ownerOf(TOKEN_ID)).to.be.reverted await expect(OptimismMintableERC721.ownerOf(TOKEN_ID)).to.be.reverted
}) })
withdrawalTest( withdrawalTest(
...@@ -239,9 +253,9 @@ describe('ERC721 Bridge', () => { ...@@ -239,9 +253,9 @@ describe('ERC721 Bridge', () => {
async () => { async () => {
// First, deposit the legitimate L1 NFT. // First, deposit the legitimate L1 NFT.
await env.messenger.waitForMessageReceipt( await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721( await L1ERC721Bridge.bridgeERC721(
L1ERC721.address, L1ERC721.address,
L2StandardERC721.address, OptimismMintableERC721.address,
TOKEN_ID, TOKEN_ID,
FINALIZATION_GAS, FINALIZATION_GAS,
NON_NULL_BYTES NON_NULL_BYTES
...@@ -253,25 +267,26 @@ describe('ERC721 Bridge', () => { ...@@ -253,25 +267,26 @@ describe('ERC721 Bridge', () => {
// Deploy a fake L2 ERC721, which: // Deploy a fake L2 ERC721, which:
// - Returns the address of the legitimate L1 token from its l1Token() getter. // - Returns the address of the legitimate L1 token from its l1Token() getter.
// - Allows the L2 bridge to call its burn() function. // - Allows the L2 bridge to call its burn() function.
const FakeL2StandardERC721 = await ( const FakeOptimismMintableERC721 = await (
await ethers.getContractFactory('FakeL2StandardERC721', bobWalletL2) await ethers.getContractFactory('FakeOptimismMintableERC721', bobWalletL2)
).deploy(L1ERC721.address, L2ERC721Bridge.address) ).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 // 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() await tx.wait()
// Check that Alice owns the NFT from the fake ERC721 contract // 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 aliceAddress
) )
// Alice withdraws the NFT from the fake contract to L1, hoping to receive the legitimate L1 NFT. // Alice withdraws the NFT from the fake contract to L1, hoping to receive the legitimate L1 NFT.
const withdrawalTx = await L2ERC721Bridge.connect( const withdrawalTx = await L2ERC721Bridge.connect(
aliceWalletL2 aliceWalletL2
).withdrawERC721( ).bridgeERC721(
FakeL2StandardERC721.address, FakeOptimismMintableERC721.address,
L1ERC721.address,
TOKEN_ID, TOKEN_ID,
0, 0,
NON_NULL_BYTES NON_NULL_BYTES
......
ignores: [ ignores: [
"@openzeppelin/contracts", "@openzeppelin/contracts",
"@openzeppelin/contracts-upgradeable",
"@rari-capital/solmate", "@rari-capital/solmate",
"@types/node", "@types/node",
"hardhat-deploy", "hardhat-deploy",
......
// 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;
}
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.9; 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 { import {
CrossDomainEnabled CrossDomainEnabled
} from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; } 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 { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { L2ERC721Bridge } from "../../L2/messaging/L2ERC721Bridge.sol";
/** /**
* @title L1ERC721Bridge * @title L1ERC721Bridge
* @dev The L1 ERC721 Bridge is a contract which stores deposited L1 NFTs that are in use * @notice The L1 ERC721 bridge is a contract which works together with the L2 ERC721 bridge to
* on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits and listening * make it possible to transfer ERC721 tokens between Optimism and Ethereum. This contract
* to it for newly finalized withdrawals. * acts as an escrow for ERC721 tokens deposted into L2.
*/ */
contract L1ERC721Bridge is IL1ERC721Bridge, CrossDomainEnabled { contract L1ERC721Bridge is CrossDomainEnabled, OwnableUpgradeable {
/******************************** /**
* External Contract References * * @notice Contract version number.
********************************/ */
uint8 public constant VERSION = 1;
address public l2ERC721Bridge;
// 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)) { * @notice Address of the bridge on the other network.
_initialize(_l1messenger, _l2ERC721Bridge); */
} address public otherBridge;
/****************** // Maps L1 token to L2 token to token ID to a boolean indicating if the token is deposited
* Initialization * /**
******************/ * @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 _messenger Address of the CrossDomainMessenger on this network.
* @param _l2ERC721Bridge L2 ERC721 bridge address. * @param _otherBridge Address of the ERC721 bridge on the other network.
*/ */
function _initialize(address _l1messenger, address _l2ERC721Bridge) internal { constructor(address _messenger, address _otherBridge) CrossDomainEnabled(address(0)) {
messenger = _l1messenger; initialize(_messenger, _otherBridge);
l2ERC721Bridge = _l2ERC721Bridge;
} }
/**************
* 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( function initialize(address _messenger, address _otherBridge) public reinitializer(VERSION) {
address _l1Token, messenger = _messenger;
address _l2Token, otherBridge = _otherBridge;
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");
_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( function bridgeERC721(
address _l1Token, address _localToken,
address _l2Token, address _remoteToken,
address _to,
uint256 _tokenId, uint256 _tokenId,
uint32 _l2Gas, uint32 _minGasLimit,
bytes calldata _data bytes calldata _extraData
) external virtual { ) external {
_initiateERC721Deposit(_l1Token, _l2Token, msg.sender, _to, _tokenId, _l2Gas, _data); // 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 * @notice Initiates a bridge of an NFT to some recipient's account on L2.
* contract of the deposit and calling a handler to lock the L1 NFT. (e.g. transferFrom)
* *
* @param _l1Token Address of the L1 ERC721 we are depositing * @param _localToken Address of the ERC721 on this domain.
* @param _l2Token Address of the L1 respective L2 ERC721 * @param _remoteToken Address of the ERC721 on the remote domain.
* @param _from Account to pull the deposit from on L1 * @param _to Address to receive the token on the other domain.
* @param _to Account to give the deposit to on L2 * @param _tokenId Token ID to bridge.
* @param _tokenId Token ID of the ERC721 to deposit. * @param _minGasLimit Minimum gas limit for the bridge message on the other domain.
* @param _l2Gas Gas limit required to complete the deposit on L2. * @param _extraData Optional data to forward to L2. Data supplied here will not be used to
* @param _data Optional data to forward to L2. This data is provided * execute any code on L2 and is only emitted as extra data for the
* solely as a convenience for external contracts. Aside from enforcing a maximum * convenience of off-chain tooling.
* length, these contracts provide no guarantees about its content.
*/ */
function _initiateERC721Deposit( function bridgeERC721To(
address _l1Token, address _localToken,
address _l2Token, address _remoteToken,
address _from,
address _to, address _to,
uint256 _tokenId, uint256 _tokenId,
uint32 _l2Gas, uint32 _minGasLimit,
bytes calldata _data bytes calldata _extraData
) internal { ) external {
// When a deposit is initiated on L1, the L1 Bridge transfers the NFT to itself for future _initiateBridgeERC721(
// withdrawals. _localToken,
// slither-disable-next-line reentrancy-events, reentrancy-benign _remoteToken,
IERC721(_l1Token).transferFrom(_from, address(this), _tokenId); msg.sender,
// Construct calldata for _l2Token.finalizeERC721Deposit(_to, _tokenId)
bytes memory message = abi.encodeWithSelector(
IL2ERC721Bridge.finalizeERC721Deposit.selector,
_l1Token,
_l2Token,
_from,
_to, _to,
_tokenId, _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 { ...@@ -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( function finalizeBridgeERC721(
address _l1Token, address _localToken,
address _l2Token, address _remoteToken,
address _from, address _from,
address _to, address _to,
uint256 _tokenId, uint256 _tokenId,
bytes calldata _data bytes calldata _extraData
) external onlyFromCrossDomainAccount(l2ERC721Bridge) { ) external onlyFromCrossDomainAccount(otherBridge) {
// Checks that the L1/L2 token pair has a token ID that is escrowed in the L1 Bridge // Checks that the L1/L2 token pair has a token ID that is escrowed in the L1 Bridge
require( require(
deposits[_l1Token][_l2Token][_tokenId] == true, deposits[_localToken][_remoteToken][_tokenId] == true,
"Token ID is not escrowed in the L1 Bridge" "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 // When a withdrawal is finalized on L1, the L1 Bridge transfers the NFT to the withdrawer
// slither-disable-next-line reentrancy-events // 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 // 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);
} }
} }
// 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;
}
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.9; 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 { import {
CrossDomainEnabled CrossDomainEnabled
} from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; } 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 { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { L1ERC721Bridge } from "../../L1/messaging/L1ERC721Bridge.sol";
/* Contract Imports */ import { IOptimismMintableERC721 } from "../../universal/op-erc721/IOptimismMintableERC721.sol";
import { IL2StandardERC721 } from "../../standards/IL2StandardERC721.sol";
/** /**
* @title L2ERC721Bridge * @title L2ERC721Bridge
* @dev The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to * @notice The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to
* enable ERC721 transitions between L1 and L2. * make it possible to transfer ERC721 tokens between Optimism and Ethereum. This contract
* This contract acts as a minter for new tokens when it hears about deposits into the L1 ERC721 * acts as a minter for new tokens when it hears about deposits into the L1 ERC721 bridge.
* bridge. * This contract also acts as a burner for tokens being withdrawn.
* This contract also acts as a burner of the token intended for withdrawal, informing the L1
* bridge to release the L1 NFT.
*/ */
contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled { contract L2ERC721Bridge is CrossDomainEnabled, OwnableUpgradeable {
/******************************** /**
* External Contract References * * @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 constructor(address _messenger, address _otherBridge) CrossDomainEnabled(address(0)) {
function initialize(address _l1ERC721Bridge) public { initialize(_messenger, _otherBridge);
require(l1ERC721Bridge == address(0), "Contract has already been initialized.");
l1ERC721Bridge = _l1ERC721Bridge;
} }
/***************
* 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( function initialize(address _messenger, address _otherBridge) public reinitializer(VERSION) {
address _l2Token, messenger = _messenger;
uint256 _tokenId, otherBridge = _otherBridge;
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");
_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( function bridgeERC721(
address _l2Token, address _localToken,
address _to, address _remoteToken,
uint256 _tokenId, uint256 _tokenId,
uint32 _l1Gas, uint32 _minGasLimit,
bytes calldata _data bytes calldata _extraData
) external virtual { ) external {
_initiateWithdrawal(_l2Token, msg.sender, _to, _tokenId, _l1Gas, _data); // 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 * @notice Initiates a bridge of an NFT to some recipient's account on L1.
* the L1 token Gateway of the withdrawal. *
* @param _l2Token Address of L2 token where withdrawal is initiated. * @param _localToken Address of the ERC721 on this domain.
* @param _from Account to pull the withdrawal from on L2. * @param _remoteToken Address of the ERC721 on the remote domain.
* @param _to Account to give the withdrawal to on L1. * @param _to Address to receive the token on the other domain.
* @param _tokenId Token ID of the token to withdraw. * @param _tokenId Token ID to bridge.
* @param _l1Gas Unused, but included for potential forward compatibility considerations. * @param _minGasLimit Minimum gas limit for the bridge message on the other domain.
* @param _data Optional data to forward to L1. This data is provided * @param _extraData Optional data to forward to L1. Data supplied here will not be used to
* solely as a convenience for external contracts. Aside from enforcing a maximum * execute any code on L1 and is only emitted as extra data for the
* length, these contracts provide no guarantees about its content. * convenience of off-chain tooling.
*/ */
function _initiateWithdrawal( function bridgeERC721To(
address _l2Token, address _localToken,
address _from, address _remoteToken,
address _to, address _to,
uint256 _tokenId, uint256 _tokenId,
uint32 _l1Gas, uint32 _minGasLimit,
bytes calldata _data bytes calldata _extraData
) internal { ) external {
// Check that the withdrawal is being initiated by the NFT owner _initiateBridgeERC721(
require( _localToken,
_from == IL2StandardERC721(_l2Token).ownerOf(_tokenId), _remoteToken,
"Withdrawal is not being initiated by NFT owner" msg.sender,
);
// 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,
_to, _to,
_tokenId, _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( function finalizeBridgeERC721(
address _l1Token, address _localToken,
address _l2Token, address _remoteToken,
address _from, address _from,
address _to, address _to,
uint256 _tokenId, uint256 _tokenId,
bytes calldata _data bytes calldata _extraData
) external virtual onlyFromCrossDomainAccount(l1ERC721Bridge) { ) external onlyFromCrossDomainAccount(otherBridge) {
// Check the target token is compliant and // Check the target token is compliant and verify the deposited token on L1 matches the L2
// verify the deposited token on L1 matches the L2 deposited token representation here // deposited token representation.
if ( if (
// slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && ERC165Checker.supportsInterface(
_l1Token == IL2StandardERC721(_l2Token).l1Token() _localToken,
type(IOptimismMintableERC721).interfaceId
) && _remoteToken == IOptimismMintableERC721(_localToken).remoteToken()
) { ) {
// When a deposit is finalized, we give the NFT with the same tokenId to the account // When a deposit is finalized, we give the NFT with the same tokenId to the account
// on L2. // on L2.
// slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
IL2StandardERC721(_l2Token).mint(_to, _tokenId); IOptimismMintableERC721(_localToken).mint(_to, _tokenId);
// slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
emit ERC721DepositFinalized(_l1Token, _l2Token, _from, _to, _tokenId, _data); emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
} else { } else {
// Either the L2 token which is being deposited-into disagrees about the correct address // 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. // of its L1 token, or does not support the correct interface.
...@@ -177,20 +217,80 @@ contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled { ...@@ -177,20 +217,80 @@ contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled {
// There is no way to prevent malicious token contracts altogether, but this does limit // There is no way to prevent malicious token contracts altogether, but this does limit
// user error and mitigate some forms of malicious contract behavior. // user error and mitigate some forms of malicious contract behavior.
bytes memory message = abi.encodeWithSelector( bytes memory message = abi.encodeWithSelector(
IL1ERC721Bridge.finalizeERC721Withdrawal.selector, L1ERC721Bridge.finalizeBridgeERC721.selector,
_l1Token, _remoteToken,
_l2Token, _localToken,
_to, // switched the _to and _from here to bounce back the deposit to the sender _to, // switched the _to and _from here to bounce back the deposit to the sender
_from, _from,
_tokenId, _tokenId,
_data _extraData
); );
// Send message up to L1 bridge // Send message up to L1 bridge
// slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l1ERC721Bridge, 0, message); sendCrossDomainMessage(otherBridge, 0, message);
// slither-disable-next-line reentrancy-events // 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);
}
} }
// 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));
}
}
// 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);
}
}
// 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);
}
// 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);
}
}
// 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;
}
...@@ -4,35 +4,51 @@ pragma solidity ^0.8.9; ...@@ -4,35 +4,51 @@ pragma solidity ^0.8.9;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Lib_Strings } from "../libraries/utils/Lib_Strings.sol"; import { IOptimismMintableERC721 } from "./IOptimismMintableERC721.sol";
import "./IL2StandardERC721.sol";
contract L2StandardERC721 is IL2StandardERC721, ERC721 { /**
address public l1Token; * @title OptimismMintableERC721
address public l2Bridge; * @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; string public baseTokenURI;
/** /**
* @param _l2Bridge Address of the L2 standard bridge. * @param _bridge Address of the bridge on this network.
* @param _l1Token Address of the corresponding L1 token. * @param _remoteToken Address of the corresponding token on the other network.
* @param _name ERC721 name. * @param _name ERC721 name.
* @param _symbol ERC721 symbol. * @param _symbol ERC721 symbol.
*/ */
constructor( constructor(
address _l2Bridge, address _bridge,
address _l1Token, address _remoteToken,
string memory _name, string memory _name,
string memory _symbol string memory _symbol
) ERC721(_name, _symbol) { ) ERC721(_name, _symbol) {
l1Token = _l1Token; remoteToken = _remoteToken;
l2Bridge = _l2Bridge; bridge = _bridge;
// Creates a base URI in the format specified by EIP-681: // Creates a base URI in the format specified by EIP-681:
// https://eips.ethereum.org/EIPS/eip-681 // https://eips.ethereum.org/EIPS/eip-681
baseTokenURI = string( baseTokenURI = string(
abi.encodePacked( abi.encodePacked(
"ethereum:0x", "ethereum:",
Lib_Strings.addressToString(_l1Token), Strings.toHexString(uint160(_remoteToken)),
"@", "@",
Strings.toString(block.chainid), Strings.toString(block.chainid),
"/tokenURI?uint256=" "/tokenURI?uint256="
...@@ -40,12 +56,39 @@ contract L2StandardERC721 is IL2StandardERC721, ERC721 { ...@@ -40,12 +56,39 @@ contract L2StandardERC721 is IL2StandardERC721, ERC721 {
); );
} }
modifier onlyL2Bridge() { /**
require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); * @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");
_; _;
} }
// slither-disable-next-line external-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) function supportsInterface(bytes4 _interfaceId)
public public
view view
...@@ -53,27 +96,18 @@ contract L2StandardERC721 is IL2StandardERC721, ERC721 { ...@@ -53,27 +96,18 @@ contract L2StandardERC721 is IL2StandardERC721, ERC721 {
returns (bool) returns (bool)
{ {
bytes4 iface1 = type(IERC165).interfaceId; bytes4 iface1 = type(IERC165).interfaceId;
bytes4 iface2 = type(IL2StandardERC721).interfaceId; bytes4 iface2 = type(IOptimismMintableERC721).interfaceId;
return return
_interfaceId == iface1 || _interfaceId == iface1 ||
_interfaceId == iface2 || _interfaceId == iface2 ||
super.supportsInterface(_interfaceId); super.supportsInterface(_interfaceId);
} }
// slither-disable-next-line external-function /**
function mint(address _to, uint256 _tokenId) public virtual onlyL2Bridge { * @notice Returns the base token URI.
_mint(_to, _tokenId); *
* @return Base token URI.
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) { function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI; return baseTokenURI;
} }
......
// 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));
}
}
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
"@rari-capital/solmate": "^6.3.0", "@rari-capital/solmate": "^6.3.0",
"@openzeppelin/contracts": "4.6.0", "@openzeppelin/contracts": "4.6.0",
"@openzeppelin/contracts-upgradeable": "4.6.0",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
......
...@@ -89,11 +89,11 @@ describe('L1ERC721Bridge', () => { ...@@ -89,11 +89,11 @@ describe('L1ERC721Bridge', () => {
await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId) 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. // 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( await expect(
L1ERC721Bridge.connect(alice).depositERC721( L1ERC721Bridge.connect(alice).bridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
tokenId, tokenId,
...@@ -101,7 +101,7 @@ describe('L1ERC721Bridge', () => { ...@@ -101,7 +101,7 @@ describe('L1ERC721Bridge', () => {
NON_NULL_BYTES32 NON_NULL_BYTES32
) )
) )
.to.emit(L1ERC721Bridge, 'ERC721DepositInitiated') .to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs( .withArgs(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
...@@ -129,9 +129,9 @@ describe('L1ERC721Bridge', () => { ...@@ -129,9 +129,9 @@ describe('L1ERC721Bridge', () => {
// the L1 bridge sends the correct message to the L1 messenger // the L1 bridge sends the correct message to the L1 messenger
expect(depositCallToMessenger.args[1]).to.equal( expect(depositCallToMessenger.args[1]).to.equal(
IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [ IL2ERC721Bridge.encodeFunctionData('finalizeBridgeERC721', [
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
L1ERC721.address,
aliceAddress, aliceAddress,
aliceAddress, aliceAddress,
tokenId, tokenId,
...@@ -150,11 +150,11 @@ describe('L1ERC721Bridge', () => { ...@@ -150,11 +150,11 @@ describe('L1ERC721Bridge', () => {
).to.equal(true) ).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. // 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( await expect(
L1ERC721Bridge.connect(alice).depositERC721To( L1ERC721Bridge.connect(alice).bridgeERC721To(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
bobsAddress, bobsAddress,
...@@ -163,7 +163,7 @@ describe('L1ERC721Bridge', () => { ...@@ -163,7 +163,7 @@ describe('L1ERC721Bridge', () => {
NON_NULL_BYTES32 NON_NULL_BYTES32
) )
) )
.to.emit(L1ERC721Bridge, 'ERC721DepositInitiated') .to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs( .withArgs(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
...@@ -195,9 +195,9 @@ describe('L1ERC721Bridge', () => { ...@@ -195,9 +195,9 @@ describe('L1ERC721Bridge', () => {
// the L1 bridge sends the correct message to the L1 messenger // the L1 bridge sends the correct message to the L1 messenger
expect(depositCallToMessenger.args[1]).to.equal( expect(depositCallToMessenger.args[1]).to.equal(
IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [ IL2ERC721Bridge.encodeFunctionData('finalizeBridgeERC721', [
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
L1ERC721.address,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
tokenId, tokenId,
...@@ -216,22 +216,22 @@ describe('L1ERC721Bridge', () => { ...@@ -216,22 +216,22 @@ describe('L1ERC721Bridge', () => {
).to.equal(true) ).to.equal(true)
}) })
it('cannot depositERC721 from a contract account', async () => { it('cannot bridgeERC721 from a contract account', async () => {
await expect( await expect(
L1ERC721Bridge.depositERC721( L1ERC721Bridge.bridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
tokenId, tokenId,
FINALIZATION_GAS, FINALIZATION_GAS,
NON_NULL_BYTES32 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', () => { 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( await expect(
L1ERC721Bridge.connect(bob).depositERC721To( L1ERC721Bridge.connect(bob).bridgeERC721To(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
bobsAddress, bobsAddress,
...@@ -242,9 +242,9 @@ describe('L1ERC721Bridge', () => { ...@@ -242,9 +242,9 @@ describe('L1ERC721Bridge', () => {
).to.be.revertedWith('ERC721: transfer from incorrect owner') ).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( await expect(
L1ERC721Bridge.connect(bob).depositERC721To( L1ERC721Bridge.connect(bob).bridgeERC721To(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
bobsAddress, bobsAddress,
...@@ -255,9 +255,9 @@ describe('L1ERC721Bridge', () => { ...@@ -255,9 +255,9 @@ describe('L1ERC721Bridge', () => {
).to.be.revertedWith('ERC721: transfer from incorrect owner') ).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( await expect(
L1ERC721Bridge.connect(alice).depositERC721To( L1ERC721Bridge.connect(alice).bridgeERC721To(
constants.AddressZero, constants.AddressZero,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
bobsAddress, bobsAddress,
...@@ -268,9 +268,9 @@ describe('L1ERC721Bridge', () => { ...@@ -268,9 +268,9 @@ describe('L1ERC721Bridge', () => {
).to.be.revertedWith('function call to a non-contract account') ).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( await expect(
L1ERC721Bridge.connect(alice).depositERC721To( L1ERC721Bridge.connect(alice).bridgeERC721To(
bobsAddress, bobsAddress,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
bobsAddress, bobsAddress,
...@@ -286,7 +286,7 @@ describe('L1ERC721Bridge', () => { ...@@ -286,7 +286,7 @@ describe('L1ERC721Bridge', () => {
describe('ERC721 withdrawals', () => { describe('ERC721 withdrawals', () => {
it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => {
await expect( await expect(
L1ERC721Bridge.connect(alice).finalizeERC721Withdrawal( L1ERC721Bridge.connect(alice).finalizeBridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero, constants.AddressZero,
...@@ -299,7 +299,7 @@ describe('L1ERC721Bridge', () => { ...@@ -299,7 +299,7 @@ describe('L1ERC721Bridge', () => {
it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC721)', async () => { it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC721)', async () => {
await expect( await expect(
L1ERC721Bridge.finalizeERC721Withdrawal( L1ERC721Bridge.finalizeBridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero, constants.AddressZero,
...@@ -318,7 +318,7 @@ describe('L1ERC721Bridge', () => { ...@@ -318,7 +318,7 @@ describe('L1ERC721Bridge', () => {
// First Alice will send an NFT so that there's a balance to be withdrawn // First Alice will send an NFT so that there's a balance to be withdrawn
await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId) await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId)
await L1ERC721Bridge.connect(alice).depositERC721( await L1ERC721Bridge.connect(alice).bridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
tokenId, tokenId,
...@@ -336,7 +336,7 @@ describe('L1ERC721Bridge', () => { ...@@ -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 () => { 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( await expect(
L1ERC721Bridge.finalizeERC721Withdrawal( L1ERC721Bridge.finalizeBridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_BRIDGE_ADDRESS, // incorrect l2 token address DUMMY_L2_BRIDGE_ADDRESS, // incorrect l2 token address
constants.AddressZero, constants.AddressZero,
...@@ -351,9 +351,9 @@ describe('L1ERC721Bridge', () => { ...@@ -351,9 +351,9 @@ describe('L1ERC721Bridge', () => {
}) })
it('should credit funds to the withdrawer and not use too much gas', async () => { 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( await expect(
L1ERC721Bridge.finalizeERC721Withdrawal( L1ERC721Bridge.finalizeBridgeERC721(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
...@@ -363,7 +363,7 @@ describe('L1ERC721Bridge', () => { ...@@ -363,7 +363,7 @@ describe('L1ERC721Bridge', () => {
{ from: Fake__L1CrossDomainMessenger.address } { from: Fake__L1CrossDomainMessenger.address }
) )
) )
.to.emit(L1ERC721Bridge, 'ERC721WithdrawalFinalized') .to.emit(L1ERC721Bridge, 'ERC721BridgeFinalized')
.withArgs( .withArgs(
L1ERC721.address, L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS, DUMMY_L2_ERC721_ADDRESS,
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
} from '../../../../../contracts/test/helpers' } 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_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated'
const ERR_INVALID_X_DOMAIN_MSG_SENDER = const ERR_INVALID_X_DOMAIN_MSG_SENDER =
'OVM_XCHAIN: wrong sender of cross-domain message' 'OVM_XCHAIN: wrong sender of cross-domain message'
...@@ -53,14 +53,11 @@ describe('L2ERC721Bridge', () => { ...@@ -53,14 +53,11 @@ describe('L2ERC721Bridge', () => {
// Deploy the contract under test // Deploy the contract under test
L2ERC721Bridge = await ( L2ERC721Bridge = await (
await ethers.getContractFactory('L2ERC721Bridge') await ethers.getContractFactory('L2ERC721Bridge')
).deploy(Fake__L2CrossDomainMessenger.address) ).deploy(Fake__L2CrossDomainMessenger.address, DUMMY_L1BRIDGE_ADDRESS)
// Initialize the contract
await L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS)
// Deploy an L2 ERC721 // Deploy an L2 ERC721
L2ERC721 = await ( L2ERC721 = await (
await ethers.getContractFactory('L2StandardERC721') await ethers.getContractFactory('OptimismMintableERC721')
).deploy( ).deploy(
L2ERC721Bridge.address, L2ERC721Bridge.address,
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
...@@ -73,16 +70,19 @@ describe('L2ERC721Bridge', () => { ...@@ -73,16 +70,19 @@ describe('L2ERC721Bridge', () => {
describe('initialize', () => { describe('initialize', () => {
it('Should only be callable once', async () => { it('Should only be callable once', async () => {
await expect( await expect(
L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS) L2ERC721Bridge.initialize(
Fake__L2CrossDomainMessenger.address,
DUMMY_L1BRIDGE_ADDRESS
)
).to.be.revertedWith(ERR_ALREADY_INITIALIZED) ).to.be.revertedWith(ERR_ALREADY_INITIALIZED)
}) })
}) })
// test the transfer flow of moving a token from L1 to L2 // 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 () => { it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => {
await expect( await expect(
L2ERC721Bridge.connect(alice).finalizeERC721Deposit( L2ERC721Bridge.connect(alice).finalizeBridgeERC721(
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
...@@ -99,7 +99,7 @@ describe('L2ERC721Bridge', () => { ...@@ -99,7 +99,7 @@ describe('L2ERC721Bridge', () => {
) )
await expect( await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721(
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
...@@ -125,11 +125,11 @@ describe('L2ERC721Bridge', () => { ...@@ -125,11 +125,11 @@ describe('L2ERC721Bridge', () => {
DUMMY_L1BRIDGE_ADDRESS 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( await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721(
DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address, NonCompliantERC721.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
...@@ -139,10 +139,10 @@ describe('L2ERC721Bridge', () => { ...@@ -139,10 +139,10 @@ describe('L2ERC721Bridge', () => {
} }
) )
) )
.to.emit(L2ERC721Bridge, 'ERC721DepositFailed') .to.emit(L2ERC721Bridge, 'ERC721BridgeFailed')
.withArgs( .withArgs(
DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address, NonCompliantERC721.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
...@@ -155,7 +155,7 @@ describe('L2ERC721Bridge', () => { ...@@ -155,7 +155,7 @@ describe('L2ERC721Bridge', () => {
expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS) expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS)
expect(withdrawalCallToMessenger.args[1]).to.equal( expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData( Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal', 'finalizeBridgeERC721',
[ [
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address, NonCompliantERC721.address,
...@@ -181,9 +181,9 @@ describe('L2ERC721Bridge', () => { ...@@ -181,9 +181,9 @@ describe('L2ERC721Bridge', () => {
// Successfully finalizes the deposit. // Successfully finalizes the deposit.
const expectedResult = expect( const expectedResult = expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit( L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721(
DUMMY_L1ERC721_ADDRESS,
L2ERC721.address, L2ERC721.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
...@@ -194,12 +194,12 @@ describe('L2ERC721Bridge', () => { ...@@ -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 await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721DepositFinalized') .emit(L2ERC721Bridge, 'ERC721BridgeFinalized')
.withArgs( .withArgs(
DUMMY_L1ERC721_ADDRESS,
L2ERC721.address, L2ERC721.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
...@@ -222,7 +222,7 @@ describe('L2ERC721Bridge', () => { ...@@ -222,7 +222,7 @@ describe('L2ERC721Bridge', () => {
beforeEach(async () => { beforeEach(async () => {
Mock__L2Token = await ( Mock__L2Token = await (
await smock.mock('L2StandardERC721') await smock.mock('OptimismMintableERC721')
).deploy( ).deploy(
L2ERC721Bridge.address, L2ERC721Bridge.address,
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
...@@ -239,10 +239,11 @@ describe('L2ERC721Bridge', () => { ...@@ -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( await expect(
L2ERC721Bridge.connect(bob).withdrawERC721( L2ERC721Bridge.connect(bob).bridgeERC721(
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID, TOKEN_ID,
0, 0,
NON_NULL_BYTES32 NON_NULL_BYTES32
...@@ -250,37 +251,39 @@ describe('L2ERC721Bridge', () => { ...@@ -250,37 +251,39 @@ describe('L2ERC721Bridge', () => {
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) ).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( await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).withdrawERC721( L2ERC721Bridge.connect(l2MessengerImpersonator).bridgeERC721(
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID, TOKEN_ID,
0, 0,
NON_NULL_BYTES32 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 // Make sure that alice begins as the NFT owner
expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal. // Initiates a successful withdrawal.
const expectedResult = expect( const expectedResult = expect(
L2ERC721Bridge.connect(alice).withdrawERC721( L2ERC721Bridge.connect(alice).bridgeERC721(
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID, TOKEN_ID,
0, 0,
NON_NULL_BYTES32 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 await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated') .emit(L2ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs( .withArgs(
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
aliceAddress, aliceAddress,
TOKEN_ID, TOKEN_ID,
...@@ -311,7 +314,7 @@ describe('L2ERC721Bridge', () => { ...@@ -311,7 +314,7 @@ describe('L2ERC721Bridge', () => {
// Message data should be a call telling the L1ERC721Bridge to finalize the withdrawal // Message data should be a call telling the L1ERC721Bridge to finalize the withdrawal
expect(withdrawalCallToMessenger.args[1]).to.equal( expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData( Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal', 'finalizeBridgeERC721',
[ [
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address, Mock__L2Token.address,
...@@ -326,10 +329,11 @@ describe('L2ERC721Bridge', () => { ...@@ -326,10 +329,11 @@ describe('L2ERC721Bridge', () => {
expect(withdrawalCallToMessenger.args[2]).to.equal(0) 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( await expect(
L2ERC721Bridge.connect(bob).withdrawERC721To( L2ERC721Bridge.connect(bob).bridgeERC721To(
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
0, 0,
...@@ -338,14 +342,15 @@ describe('L2ERC721Bridge', () => { ...@@ -338,14 +342,15 @@ describe('L2ERC721Bridge', () => {
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) ).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 // Make sure that alice begins as the NFT owner
expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal. // Initiates a successful withdrawal.
const expectedResult = expect( const expectedResult = expect(
L2ERC721Bridge.connect(alice).withdrawERC721To( L2ERC721Bridge.connect(alice).bridgeERC721To(
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
0, 0,
...@@ -353,12 +358,12 @@ describe('L2ERC721Bridge', () => { ...@@ -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 await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated') .emit(L2ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs( .withArgs(
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address, Mock__L2Token.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress, aliceAddress,
bobsAddress, bobsAddress,
TOKEN_ID, TOKEN_ID,
...@@ -389,7 +394,7 @@ describe('L2ERC721Bridge', () => { ...@@ -389,7 +394,7 @@ describe('L2ERC721Bridge', () => {
// The message data should be a call telling the L1ERC721Bridge to finalize the withdrawal // The message data should be a call telling the L1ERC721Bridge to finalize the withdrawal
expect(withdrawalCallToMessenger.args[1]).to.equal( expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData( Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal', 'finalizeBridgeERC721',
[ [
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address, Mock__L2Token.address,
......
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
)
})
}
})
})
...@@ -10,11 +10,11 @@ const TOKEN_ID = 10 ...@@ -10,11 +10,11 @@ const TOKEN_ID = 10
const DUMMY_L1ERC721_ADDRESS: string = const DUMMY_L1ERC721_ADDRESS: string =
'0x2234223412342234223422342234223422342234' '0x2234223412342234223422342234223422342234'
describe('L2StandardERC721', () => { describe('OptimismMintableERC721', () => {
let l2BridgeImpersonator: Signer let l2BridgeImpersonator: Signer
let alice: Signer let alice: Signer
let Fake__L2ERC721Bridge: FakeContract let Fake__L2ERC721Bridge: FakeContract
let L2StandardERC721: Contract let OptimismMintableERC721: Contract
let l2BridgeImpersonatorAddress: string let l2BridgeImpersonatorAddress: string
let aliceAddress: string let aliceAddress: string
let baseUri: string let baseUri: string
...@@ -34,8 +34,8 @@ describe('L2StandardERC721', () => { ...@@ -34,8 +34,8 @@ describe('L2StandardERC721', () => {
'/tokenURI?uint256=' '/tokenURI?uint256='
) )
L2StandardERC721 = await ( OptimismMintableERC721 = await (
await ethers.getContractFactory('L2StandardERC721') await ethers.getContractFactory('OptimismMintableERC721')
).deploy( ).deploy(
l2BridgeImpersonatorAddress, l2BridgeImpersonatorAddress,
DUMMY_L1ERC721_ADDRESS, DUMMY_L1ERC721_ADDRESS,
...@@ -52,7 +52,7 @@ describe('L2StandardERC721', () => { ...@@ -52,7 +52,7 @@ describe('L2StandardERC721', () => {
) )
// mint an nft to alice // mint an nft to alice
await L2StandardERC721.connect(l2BridgeImpersonator).mint( await OptimismMintableERC721.connect(l2BridgeImpersonator).mint(
aliceAddress, aliceAddress,
TOKEN_ID, TOKEN_ID,
{ {
...@@ -63,56 +63,62 @@ describe('L2StandardERC721', () => { ...@@ -63,56 +63,62 @@ describe('L2StandardERC721', () => {
describe('constructor', () => { describe('constructor', () => {
it('should be able to create a standard ERC721 contract with the correct parameters', async () => { 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 l2BridgeImpersonatorAddress
) )
expect(await L2StandardERC721.l1Token()).to.equal(DUMMY_L1ERC721_ADDRESS) expect(await OptimismMintableERC721.remoteToken()).to.equal(
expect(await L2StandardERC721.name()).to.equal('L2ERC721') DUMMY_L1ERC721_ADDRESS
expect(await L2StandardERC721.symbol()).to.equal('ERC') )
expect(await L2StandardERC721.baseTokenURI()).to.equal(baseUri) 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 // 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', () => { describe('mint and burn', () => {
it('should not allow anyone but the L2 bridge to mint and burn', async () => { it('should not allow anyone but the L2 bridge to mint and burn', async () => {
await expect( await expect(
L2StandardERC721.connect(alice).mint(aliceAddress, 100) OptimismMintableERC721.connect(alice).mint(aliceAddress, 100)
).to.be.revertedWith('Only L2 Bridge can mint and burn') ).to.be.revertedWith(
'OptimismMintableERC721: only bridge can call this function'
)
await expect( await expect(
L2StandardERC721.connect(alice).burn(aliceAddress, 100) OptimismMintableERC721.connect(alice).burn(aliceAddress, 100)
).to.be.revertedWith('Only L2 Bridge can mint and burn') ).to.be.revertedWith(
'OptimismMintableERC721: only bridge can call this function'
)
}) })
}) })
describe('supportsInterface', () => { describe('supportsInterface', () => {
it('should return the correct interface support', async () => { it('should return the correct interface support', async () => {
const supportsERC165 = await L2StandardERC721.supportsInterface( // ERC165
0x01ffc9a7 expect(await OptimismMintableERC721.supportsInterface(0x01ffc9a7)).to.be
) .true
expect(supportsERC165).to.be.true
const supportsL2TokenInterface = await L2StandardERC721.supportsInterface( // OptimismMintablERC721
0x1d1d8b63 expect(await OptimismMintableERC721.supportsInterface(0xec4fc8e3)).to.be
) .true
expect(supportsL2TokenInterface).to.be.true
const supportsERC721Interface = await L2StandardERC721.supportsInterface( // ERC721
0x80ac58cd expect(await OptimismMintableERC721.supportsInterface(0x80ac58cd)).to.be
) .true
expect(supportsERC721Interface).to.be.true
const badSupports = await L2StandardERC721.supportsInterface(0xffffffff) // Some bad interface
expect(badSupports).to.be.false expect(await OptimismMintableERC721.supportsInterface(0xffffffff)).to.be
.false
}) })
}) })
describe('tokenURI', () => { describe('tokenURI', () => {
it('should return the correct token uri', async () => { it('should return the correct token uri', async () => {
const tokenUri = baseUri.concat(TOKEN_ID.toString()) 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)
}) })
}) })
}) })
...@@ -8,17 +8,17 @@ import { ...@@ -8,17 +8,17 @@ import {
} from '@defi-wonderland/smock' } from '@defi-wonderland/smock'
/* Internal Imports */ /* Internal Imports */
import { expect } from '../../../setup' import { expect } from '../../setup'
const DUMMY_L2_BRIDGE_ADDRESS: string = ethers.utils.getAddress( const DUMMY_L2_BRIDGE_ADDRESS: string = ethers.utils.getAddress(
'0x' + 'acdc'.repeat(10) '0x' + 'acdc'.repeat(10)
) )
describe('L2StandardERC721Factory', () => { describe('OptimismMintableERC721Factory', () => {
let signer: Signer let signer: Signer
let Factory__L1ERC721: MockContractFactory<ContractFactory> let Factory__L1ERC721: MockContractFactory<ContractFactory>
let L1ERC721: MockContract<Contract> let L1ERC721: MockContract<Contract>
let L2StandardERC721Factory: Contract let OptimismMintableERC721Factory: Contract
let baseURI: string let baseURI: string
let chainId: number let chainId: number
...@@ -31,8 +31,8 @@ describe('L2StandardERC721Factory', () => { ...@@ -31,8 +31,8 @@ describe('L2StandardERC721Factory', () => {
) )
L1ERC721 = await Factory__L1ERC721.deploy('L1ERC721', 'ERC') L1ERC721 = await Factory__L1ERC721.deploy('L1ERC721', 'ERC')
L2StandardERC721Factory = await ( OptimismMintableERC721Factory = await (
await ethers.getContractFactory('L2StandardERC721Factory') await ethers.getContractFactory('OptimismMintableERC721Factory')
).deploy(DUMMY_L2_BRIDGE_ADDRESS) ).deploy(DUMMY_L2_BRIDGE_ADDRESS)
chainId = await signer.getChainId() chainId = await signer.getChainId()
...@@ -46,71 +46,60 @@ describe('L2StandardERC721Factory', () => { ...@@ -46,71 +46,60 @@ describe('L2StandardERC721Factory', () => {
}) })
it('should be deployed with the correct constructor argument', async () => { 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 DUMMY_L2_BRIDGE_ADDRESS
) )
}) })
it('should be able to create a standard ERC721 contract', async () => { it('should be able to create a standard ERC721 contract', async () => {
const tx = await L2StandardERC721Factory.createStandardL2ERC721( const tx =
L1ERC721.address, await OptimismMintableERC721Factory.createStandardOptimismMintableERC721(
'L2ERC721', L1ERC721.address,
'ERC' 'L2ERC721',
) 'ERC'
)
const receipt = await tx.wait() const receipt = await tx.wait()
// Get the StandardL2ERC721Created event // Get the OptimismMintableERC721Created event
const erc721CreatedEvent = receipt.events[0] const erc721CreatedEvent = receipt.events[0]
// Expect there to be an event emitted for the standard token creation // 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 // Get the L2 ERC721 address from the emitted event and check it was created correctly
const l2ERC721Address = erc721CreatedEvent.args._l2Token const l2ERC721Address = erc721CreatedEvent.args.localToken
const L2StandardERC721 = new Contract( const OptimismMintableERC721 = new Contract(
l2ERC721Address, l2ERC721Address,
(await ethers.getContractFactory('L2StandardERC721')).interface, (await ethers.getContractFactory('OptimismMintableERC721')).interface,
signer signer
) )
expect(await L2StandardERC721.l2Bridge()).to.equal(DUMMY_L2_BRIDGE_ADDRESS) expect(await OptimismMintableERC721.bridge()).to.equal(
expect(await L2StandardERC721.l1Token()).to.equal(L1ERC721.address) DUMMY_L2_BRIDGE_ADDRESS
expect(await L2StandardERC721.name()).to.equal('L2ERC721') )
expect(await L2StandardERC721.symbol()).to.equal('ERC') expect(await OptimismMintableERC721.remoteToken()).to.equal(
expect(await L2StandardERC721.baseTokenURI()).to.equal(baseURI) L1ERC721.address
)
expect(await OptimismMintableERC721.name()).to.equal('L2ERC721')
expect(await OptimismMintableERC721.symbol()).to.equal('ERC')
expect(await OptimismMintableERC721.baseTokenURI()).to.equal(baseURI)
expect( expect(
await L2StandardERC721Factory.isStandardERC721(L2StandardERC721.address) await OptimismMintableERC721Factory.isStandardOptimismMintableERC721(
OptimismMintableERC721.address
)
).to.equal(true) ).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 () => { it('should not be able to create a standard token with a 0 address for l1 token', async () => {
await expect( await expect(
L2StandardERC721Factory.createStandardL2ERC721( OptimismMintableERC721Factory.createStandardOptimismMintableERC721(
ethers.constants.AddressZero, ethers.constants.AddressZero,
'L2ERC721', 'L2ERC721',
'ERC' 'ERC'
) )
).to.be.revertedWith('Must provide L1 token address') ).to.be.revertedWith(
}) 'OptimismMintableERC721Factory: L1 token address cannot be address(0)'
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'
) )
await expect(
L2StandardERC721Factory.createStandardL2ERC721(
L1ERC721.address,
'L2ERC721',
'ERC'
)
).to.be.revertedWith('L2 Standard Token already exists for this L1 Token')
}) })
}) })
...@@ -2812,16 +2812,16 @@ ...@@ -2812,16 +2812,16 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.3.2.tgz#92df481362e366c388fc02133cf793029c744cea" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.3.2.tgz#92df481362e366c388fc02133cf793029c744cea"
integrity sha512-i/pOaOtcqDk4UqsrOv735uYyTbn6dvfiuVu5hstsgV6c4ZKUtu88/31zT2BzkCg+3JfcwOfgg2TtRKVKKZIGkQ== 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": "@openzeppelin/contracts-upgradeable@^4.3.2":
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.4.0.tgz#85161d87c840c5bce2b6ed0c727b407e774852ae" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.4.0.tgz#85161d87c840c5bce2b6ed0c727b407e774852ae"
integrity sha512-hIEyWJHu7bDTv6ckxOaV+K3+7mVzhjtyvp3QSaz56Rk5PscXtPAbkiNTb3yz6UJCWHPWpxVyULVgZ6RubuFEZg== 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": "@openzeppelin/contracts@3.4.1-solc-0.7-2":
version "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" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment