@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
312 lines (269 loc) • 10.3 kB
text/typescript
/* Imports: External */
import { Contract, ContractFactory, utils, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import { getChainId } from '@eth-optimism/core-utils'
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-bedrock/artifacts/contracts/L1/L1ERC721Bridge.sol/L1ERC721Bridge.json'
import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-bedrock/artifacts/contracts/L2/L2ERC721Bridge.sol/L2ERC721Bridge.json'
import Artifact__OptimismMintableERC721Factory from '@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC721Factory.sol/OptimismMintableERC721Factory.json'
import Artifact__OptimismMintableERC721 from '@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC721.sol/OptimismMintableERC721.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 = 600_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__OptimismMintableERC721Factory: 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__OptimismMintableERC721Factory = await ethers.getContractFactory(
Artifact__OptimismMintableERC721Factory.abi,
Artifact__OptimismMintableERC721Factory.bytecode,
bobWalletL2
)
})
let L1ERC721: Contract
let L1ERC721Bridge: Contract
let L2ERC721Bridge: Contract
let OptimismMintableERC721Factory: Contract
let OptimismMintableERC721: Contract
beforeEach(async () => {
L1ERC721 = await Factory__L1ERC721.deploy()
await L1ERC721.deployed()
L1ERC721Bridge = await Factory__L1ERC721Bridge.deploy(
env.messenger.contracts.l1.L1CrossDomainMessenger.address,
ethers.utils.getContractAddress({
from: await Factory__L2ERC721Bridge.signer.getAddress(),
nonce: await Factory__L2ERC721Bridge.signer.getTransactionCount(),
})
)
await L1ERC721Bridge.deployed()
L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy(
predeploys.L2CrossDomainMessenger,
L1ERC721Bridge.address
)
await L2ERC721Bridge.deployed()
OptimismMintableERC721Factory =
await Factory__OptimismMintableERC721Factory.deploy(
L2ERC721Bridge.address,
await getChainId(env.l1Wallet.provider)
)
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
)
// Create a L2 Standard ERC721 with the Standard ERC721 Factory
const tx = await OptimismMintableERC721Factory.createOptimismMintableERC721(
L1ERC721.address,
'L2ERC721',
'L2'
)
const receipt = await tx.wait()
// Get the OptimismMintableERC721Created event
const erc721CreatedEvent = receipt.events[0]
expect(erc721CreatedEvent.event).to.be.eq('OptimismMintableERC721Created')
OptimismMintableERC721 = await ethers.getContractAt(
Artifact__OptimismMintableERC721.abi,
erc721CreatedEvent.args.localToken
)
// 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('bridgeERC721', async () => {
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.bridgeERC721(
L1ERC721.address,
OptimismMintableERC721.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 OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)
})
it('bridgeERC721To', async () => {
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.bridgeERC721To(
L1ERC721.address,
OptimismMintableERC721.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 OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(
aliceAddress
)
})
withdrawalTest('bridgeERC721', async () => {
// Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.bridgeERC721(
L1ERC721.address,
OptimismMintableERC721.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 OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)
const tx = await L2ERC721Bridge.bridgeERC721(
OptimismMintableERC721.address,
L1ERC721.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(OptimismMintableERC721.ownerOf(TOKEN_ID)).to.be.reverted
})
withdrawalTest('bridgeERC721To', async () => {
// Deposit an NFT into L2 so that there's something to withdraw
await env.messenger.waitForMessageReceipt(
await L1ERC721Bridge.bridgeERC721(
L1ERC721.address,
OptimismMintableERC721.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 OptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(bobAddress)
const tx = await L2ERC721Bridge.bridgeERC721To(
OptimismMintableERC721.address,
L1ERC721.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(OptimismMintableERC721.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.bridgeERC721(
L1ERC721.address,
OptimismMintableERC721.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 FakeOptimismMintableERC721 = await (
await ethers.getContractFactory(
'FakeOptimismMintableERC721',
bobWalletL2
)
).deploy(
L2ERC721Bridge.address,
L1ERC721.address,
await getChainId(env.l1Wallet.provider)
)
await FakeOptimismMintableERC721.deployed()
// Use the fake contract to mint Alice an NFT with the same token ID
const tx = await FakeOptimismMintableERC721.safeMint(
aliceAddress,
TOKEN_ID
)
await tx.wait()
// Check that Alice owns the NFT from the fake ERC721 contract
expect(await FakeOptimismMintableERC721.ownerOf(TOKEN_ID)).to.equal(
aliceAddress
)
// Alice withdraws the NFT from the fake contract to L1, hoping to receive the legitimate L1 NFT.
const withdrawalTx = await L2ERC721Bridge.connect(
aliceWalletL2
).bridgeERC721(
FakeOptimismMintableERC721.address,
L1ERC721.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)
}
)
})