Skip to content

Commit

Permalink
feat: nft bridge from l1 to optimism (#2662)
Browse files Browse the repository at this point in the history
* feat: nft bridge from eth network to optimism

* Update yarn.lock

* feat: added event tests

* fixed import path

* feat: added integration tests to nft bridge

* fix: removed constructor logic from l2 bridge so it can exist as a proxy

* fix: updated pr based on maurelian's comments

* deploy scripts

Co-authored-by: Mark Tyneway <mark.tyneway@gmail.com>
  • Loading branch information
sam-goldman and tynes committed Jun 13, 2022
1 parent 98e83d6 commit da1633a
Show file tree
Hide file tree
Showing 24 changed files with 2,305 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/gentle-shrimps-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/contracts-periphery': patch
---

ERC721 bridge from Eth Mainnet to Optimism
22 changes: 22 additions & 0 deletions integration-tests/contracts/FakeL2StandardERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract FakeL2StandardERC721 is ERC721 {

address public immutable l1Token;
address public immutable l2Bridge;

constructor(address _l1Token, address _l2Bridge) ERC721("FakeERC721", "FAKE") {
l1Token = _l1Token;
l2Bridge = _l2Bridge;
}

function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}

// Burn will be called by the L2 Bridge to burn the NFT we are bridging to L1
function burn(address, uint256) external {}
}
3 changes: 2 additions & 1 deletion integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.5.4",
"@eth-optimism/contracts": "0.5.27",
"@eth-optimism/contracts": "^0.5.26",
"@eth-optimism/contracts-periphery": "^0.1.1",
"@eth-optimism/core-utils": "0.8.6",
"@eth-optimism/sdk": "1.1.8",
"@ethersproject/abstract-provider": "^5.6.1",
Expand Down
286 changes: 286 additions & 0 deletions integration-tests/test/nft-bridge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/* Imports: External */
import { Contract, ContractFactory, utils, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import { predeploys } from '@eth-optimism/contracts'
import Artifact__TestERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/testing/helpers/TestERC721.sol/TestERC721.json'
import Artifact__L1ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L1/messaging/L1ERC721Bridge.sol/L1ERC721Bridge.json'
import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/messaging/L2ERC721Bridge.sol/L2ERC721Bridge.json'
import Artifact__L2StandardERC721Factory from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/messaging/L2StandardERC721Factory.sol/L2StandardERC721Factory.json'
import Artifact__L2StandardERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/standards/L2StandardERC721.sol/L2StandardERC721.json'

/* Imports: Internal */
import { expect } from './shared/setup'
import { OptimismEnv } from './shared/env'
import { withdrawalTest } from './shared/utils'

const TOKEN_ID: number = 1
const FINALIZATION_GAS: number = 1_200_000
const NON_NULL_BYTES: string = '0x1111'

describe('ERC721 Bridge', () => {
let env: OptimismEnv
before(async () => {
env = await OptimismEnv.new()
})

let aliceWalletL1: Wallet
let aliceWalletL2: Wallet
let aliceAddress: string
let bobWalletL1: Wallet
let bobWalletL2: Wallet
let bobAddress: string
before(async () => {
const alice = Wallet.createRandom()
aliceWalletL1 = alice.connect(env.l1Wallet.provider)
aliceWalletL2 = alice.connect(env.l2Wallet.provider)
aliceAddress = aliceWalletL1.address

const tx = await env.l2Wallet.sendTransaction({
to: aliceAddress,
value: utils.parseEther('0.01'),
})
await tx.wait()

bobWalletL1 = env.l1Wallet
bobWalletL2 = env.l2Wallet
bobAddress = env.l1Wallet.address
})

let Factory__L1ERC721: ContractFactory
let Factory__L1ERC721Bridge: ContractFactory
let Factory__L2ERC721Bridge: ContractFactory
let Factory__L2StandardERC721Factory: ContractFactory
before(async () => {
Factory__L1ERC721 = await ethers.getContractFactory(
Artifact__TestERC721.abi,
Artifact__TestERC721.bytecode,
bobWalletL1
)
Factory__L1ERC721Bridge = await ethers.getContractFactory(
Artifact__L1ERC721Bridge.abi,
Artifact__L1ERC721Bridge.bytecode,
bobWalletL1
)
Factory__L2ERC721Bridge = await ethers.getContractFactory(
Artifact__L2ERC721Bridge.abi,
Artifact__L2ERC721Bridge.bytecode,
bobWalletL2
)
Factory__L2StandardERC721Factory = await ethers.getContractFactory(
Artifact__L2StandardERC721Factory.abi,
Artifact__L2StandardERC721Factory.bytecode,
bobWalletL2
)
})

let L1ERC721: Contract
let L1ERC721Bridge: Contract
let L2ERC721Bridge: Contract
let L2StandardERC721Factory: Contract
let L2StandardERC721: Contract
beforeEach(async () => {
L1ERC721 = await Factory__L1ERC721.deploy()
await L1ERC721.deployed()

L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy(
predeploys.L2CrossDomainMessenger
)
await L2ERC721Bridge.deployed()

L1ERC721Bridge = await Factory__L1ERC721Bridge.deploy(
env.messenger.contracts.l1.L1CrossDomainMessenger.address,
L2ERC721Bridge.address
)
await L1ERC721Bridge.deployed()

L2StandardERC721Factory = await Factory__L2StandardERC721Factory.deploy(
L2ERC721Bridge.address
)
await L2StandardERC721Factory.deployed()

// Create a L2 Standard ERC721 with the Standard ERC721 Factory
const tx = await L2StandardERC721Factory.createStandardL2ERC721(
L1ERC721.address,
'L2ERC721',
'L2'
)
await tx.wait()

// Retrieve the deployed L2 Standard ERC721
const L2StandardERC721Address =
await L2StandardERC721Factory.standardERC721Mapping(L1ERC721.address)
L2StandardERC721 = await ethers.getContractAt(
Artifact__L2StandardERC721.abi,
L2StandardERC721Address
)
await L2StandardERC721.deployed()

// Initialize the L2 bridge contract
const tx1 = await L2ERC721Bridge.initialize(L1ERC721Bridge.address)
await tx1.wait()

// Mint an L1 ERC721 to Bob on L1
const tx2 = await L1ERC721.mint(bobAddress, TOKEN_ID)
await tx2.wait()

// Approve the L1 Bridge to operate the NFT
const tx3 = await L1ERC721.approve(L1ERC721Bridge.address, TOKEN_ID)
await tx3.wait()
})

it('depositERC721', async () => {
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721(
L1ERC721.address,
L2StandardERC721.address,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES
)
)

// The L1 Bridge now owns the L1 NFT
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)

// Bob owns the NFT on L2
expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)
})

it('depositERC721To', async () => {
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721To(
L1ERC721.address,
L2StandardERC721.address,
aliceAddress,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES
)
)

// The L1 Bridge now owns the L1 NFT
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)

// Alice owns the NFT on L2
expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
})

withdrawalTest('withdrawERC721', async () => {
// Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721(
L1ERC721.address,
L2StandardERC721.address,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES
)
)

// First, check that the L1 Bridge now owns the L1 NFT
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)

// Also check that Bob owns the NFT on L2 initially
expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)

const tx = await L2ERC721Bridge.withdrawERC721(
L2StandardERC721.address,
TOKEN_ID,
0,
NON_NULL_BYTES
)
await tx.wait()
await env.relayXDomainMessages(tx)

// L1 NFT has been sent back to Bob
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)

// L2 NFT is burned
await expect(L2StandardERC721.ownerOf(TOKEN_ID)).to.be.reverted
})

withdrawalTest('withdrawERC721To', async () => {
// Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721(
L1ERC721.address,
L2StandardERC721.address,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES
)
)

// First, check that the L1 Bridge now owns the L1 NFT
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)

// Also check that Bob owns the NFT on L2 initially
expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)

const tx = await L2ERC721Bridge.withdrawERC721To(
L2StandardERC721.address,
aliceAddress,
TOKEN_ID,
0,
NON_NULL_BYTES
)
await tx.wait()
await env.relayXDomainMessages(tx)

// L1 NFT has been sent to Alice
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress)

// L2 NFT is burned
await expect(L2StandardERC721.ownerOf(TOKEN_ID)).to.be.reverted
})

withdrawalTest(
'should not allow an arbitrary L2 NFT to be withdrawn in exchange for a legitimate L1 NFT',
async () => {
// First, deposit the legitimate L1 NFT.
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.depositERC721(
L1ERC721.address,
L2StandardERC721.address,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES
)
)
// Check that the L1 Bridge owns the L1 NFT initially
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)

// Deploy a fake L2 ERC721, which:
// - Returns the address of the legitimate L1 token from its l1Token() getter.
// - Allows the L2 bridge to call its burn() function.
const FakeL2StandardERC721 = await (
await ethers.getContractFactory('FakeL2StandardERC721', bobWalletL2)
).deploy(L1ERC721.address, L2ERC721Bridge.address)
await FakeL2StandardERC721.deployed()

// Use the fake contract to mint Alice an NFT with the same token ID
const tx = await FakeL2StandardERC721.mint(aliceAddress, TOKEN_ID)
await tx.wait()

// Check that Alice owns the NFT from the fake ERC721 contract
expect(await FakeL2StandardERC721.ownerOf(TOKEN_ID)).to.equal(
aliceAddress
)

// Alice withdraws the NFT from the fake contract to L1, hoping to receive the legitimate L1 NFT.
const withdrawalTx = await L2ERC721Bridge.connect(
aliceWalletL2
).withdrawERC721(
FakeL2StandardERC721.address,
TOKEN_ID,
0,
NON_NULL_BYTES
)
await withdrawalTx.wait()
await env.relayXDomainMessages(withdrawalTx)

// The legitimate NFT on L1 is still held in the bridge.
expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
}
)
})

0 comments on commit da1633a

Please sign in to comment.