Commit da1633a3 authored by sam-goldman's avatar sam-goldman Committed by GitHub

feat: nft bridge from l1 to optimism (#2662)

* 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: default avatarMark Tyneway <mark.tyneway@gmail.com>
parent 98e83d63
---
'@eth-optimism/contracts-periphery': patch
---
ERC721 bridge from Eth Mainnet to Optimism
// 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 {}
}
......@@ -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",
......
/* 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)
}
)
})
// 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
pragma solidity ^0.8.9;
/* Interface Imports */
import { IL1ERC721Bridge } from "./IL1ERC721Bridge.sol";
import { IL2ERC721Bridge } from "../../L2/messaging/IL2ERC721Bridge.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/* Library Imports */
import {
CrossDomainEnabled
} from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title L1ERC721Bridge
* @dev The L1 ERC721 Bridge is a contract which stores deposited L1 NFTs that are in use
* on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits and listening
* to it for newly finalized withdrawals.
*/
contract L1ERC721Bridge is IL1ERC721Bridge, CrossDomainEnabled {
/********************************
* External Contract References *
********************************/
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;
/***************
* Constructor *
***************/
// This contract lives behind a proxy, so the constructor parameters will go unused.
constructor(address _l1messenger, address _l2ERC721Bridge) CrossDomainEnabled(address(0)) {
_initialize(_l1messenger, _l2ERC721Bridge);
}
/******************
* Initialization *
******************/
/**
* @param _l1messenger L1 Messenger address being used for cross-chain communications.
* @param _l2ERC721Bridge L2 ERC721 bridge address.
*/
function _initialize(address _l1messenger, address _l2ERC721Bridge) internal {
messenger = _l1messenger;
l2ERC721Bridge = _l2ERC721Bridge;
}
/**************
* Depositing *
**************/
/**
* @inheritdoc IL1ERC721Bridge
*/
function depositERC721(
address _l1Token,
address _l2Token,
uint256 _tokenId,
uint32 _l2Gas,
bytes calldata _data
) external virtual {
// Modifier requiring sender to be EOA. This check could be bypassed by a malicious
// contract via initcode, but it takes care of the user error we want to avoid.
require(!Address.isContract(msg.sender), "Account not EOA");
_initiateERC721Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _tokenId, _l2Gas, _data);
}
/**
* @inheritdoc IL1ERC721Bridge
*/
function depositERC721To(
address _l1Token,
address _l2Token,
address _to,
uint256 _tokenId,
uint32 _l2Gas,
bytes calldata _data
) external virtual {
_initiateERC721Deposit(_l1Token, _l2Token, msg.sender, _to, _tokenId, _l2Gas, _data);
}
/**
* @dev Performs the logic for deposits by informing the L2 Deposited Token
* contract of the deposit and calling a handler to lock the L1 NFT. (e.g. transferFrom)
*
* @param _l1Token Address of the L1 ERC721 we are depositing
* @param _l2Token Address of the L1 respective L2 ERC721
* @param _from Account to pull the deposit from on L1
* @param _to Account to give the deposit to on L2
* @param _tokenId Token ID of the ERC721 to deposit.
* @param _l2Gas Gas limit required to complete the deposit on L2.
* @param _data Optional data to forward to L2. This data is provided
* solely as a convenience for external contracts. Aside from enforcing a maximum
* length, these contracts provide no guarantees about its content.
*/
function _initiateERC721Deposit(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _tokenId,
uint32 _l2Gas,
bytes calldata _data
) internal {
// When a deposit is initiated on L1, the L1 Bridge transfers the NFT to itself for future
// withdrawals.
// slither-disable-next-line reentrancy-events, reentrancy-benign
IERC721(_l1Token).transferFrom(_from, address(this), _tokenId);
// Construct calldata for _l2Token.finalizeERC721Deposit(_to, _tokenId)
bytes memory message = abi.encodeWithSelector(
IL2ERC721Bridge.finalizeERC721Deposit.selector,
_l1Token,
_l2Token,
_from,
_to,
_tokenId,
_data
);
// 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);
}
/*************************
* Cross-chain Functions *
*************************/
/**
* @inheritdoc IL1ERC721Bridge
*/
function finalizeERC721Withdrawal(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _tokenId,
bytes calldata _data
) external onlyFromCrossDomainAccount(l2ERC721Bridge) {
// Checks that the L1/L2 token pair has a token ID that is escrowed in the L1 Bridge
require(
deposits[_l1Token][_l2Token][_tokenId] == true,
"Token ID is not escrowed in the L1 Bridge"
);
deposits[_l1Token][_l2Token][_tokenId] = false;
// When a withdrawal is finalized on L1, the L1 Bridge transfers the NFT to the withdrawer
// slither-disable-next-line reentrancy-events
IERC721(_l1Token).transferFrom(address(this), _to, _tokenId);
// slither-disable-next-line reentrancy-events
emit ERC721WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _tokenId, _data);
}
}
// 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
pragma solidity ^0.8.9;
/* Interface Imports */
import { IL1ERC721Bridge } from "../../L1/messaging/IL1ERC721Bridge.sol";
import { IL1ERC721Bridge } from "../../L1/messaging/IL1ERC721Bridge.sol";
import { IL2ERC721Bridge } from "./IL2ERC721Bridge.sol";
/* Library Imports */
import {
CrossDomainEnabled
} from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
/* Contract Imports */
import { IL2StandardERC721 } from "../../standards/IL2StandardERC721.sol";
/**
* @title L2ERC721Bridge
* @dev The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to
* enable ERC721 transitions between L1 and L2.
* This contract acts as a minter for new tokens when it hears about deposits into the L1 ERC721
* bridge.
* This contract also acts as a burner of the token intended for withdrawal, informing the L1
* bridge to release the L1 NFT.
*/
contract L2ERC721Bridge is IL2ERC721Bridge, CrossDomainEnabled {
/********************************
* External Contract References *
********************************/
address public l1ERC721Bridge;
/***************
* Constructor *
***************/
/**
* @param _l2CrossDomainMessenger Cross-domain messenger used by this contract.
*/
constructor(address _l2CrossDomainMessenger) CrossDomainEnabled(_l2CrossDomainMessenger) {}
/******************
* Initialization *
******************/
/**
* @param _l1ERC721Bridge Address of the L1 bridge deployed to the main chain.
*/
// slither-disable-next-line external-function
function initialize(address _l1ERC721Bridge) public {
require(l1ERC721Bridge == address(0), "Contract has already been initialized.");
l1ERC721Bridge = _l1ERC721Bridge;
}
/***************
* Withdrawing *
***************/
/**
* @inheritdoc IL2ERC721Bridge
*/
function withdrawERC721(
address _l2Token,
uint256 _tokenId,
uint32 _l1Gas,
bytes calldata _data
) external virtual {
// Modifier requiring sender to be EOA. This check could be bypassed by a malicious
// contract via initcode, but it takes care of the user error we want to avoid.
require(!Address.isContract(msg.sender), "Account not EOA");
_initiateWithdrawal(_l2Token, msg.sender, msg.sender, _tokenId, _l1Gas, _data);
}
/**
* @inheritdoc IL2ERC721Bridge
*/
function withdrawERC721To(
address _l2Token,
address _to,
uint256 _tokenId,
uint32 _l1Gas,
bytes calldata _data
) external virtual {
_initiateWithdrawal(_l2Token, msg.sender, _to, _tokenId, _l1Gas, _data);
}
/**
* @dev Performs the logic for withdrawals by burning the token and informing
* the L1 token Gateway of the withdrawal.
* @param _l2Token Address of L2 token where withdrawal is initiated.
* @param _from Account to pull the withdrawal from on L2.
* @param _to Account to give the withdrawal to on L1.
* @param _tokenId Token ID of the token to withdraw.
* @param _l1Gas Unused, but included for potential forward compatibility considerations.
* @param _data Optional data to forward to L1. This data is provided
* solely as a convenience for external contracts. Aside from enforcing a maximum
* length, these contracts provide no guarantees about its content.
*/
function _initiateWithdrawal(
address _l2Token,
address _from,
address _to,
uint256 _tokenId,
uint32 _l1Gas,
bytes calldata _data
) internal {
// Check that the withdrawal is being initiated by the NFT owner
require(
_from == IL2StandardERC721(_l2Token).ownerOf(_tokenId),
"Withdrawal is not being initiated by NFT owner"
);
// When a withdrawal is initiated, we burn the withdrawer's NFT to prevent subsequent L2
// usage
// slither-disable-next-line reentrancy-events
IL2StandardERC721(_l2Token).burn(_from, _tokenId);
// Construct calldata for l1ERC721Bridge.finalizeERC721Withdrawal(_to, _tokenId)
// slither-disable-next-line reentrancy-events
address l1Token = IL2StandardERC721(_l2Token).l1Token();
bytes memory message = abi.encodeWithSelector(
IL1ERC721Bridge.finalizeERC721Withdrawal.selector,
l1Token,
_l2Token,
_from,
_to,
_tokenId,
_data
);
// 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
*/
function finalizeERC721Deposit(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _tokenId,
bytes calldata _data
) external virtual onlyFromCrossDomainAccount(l1ERC721Bridge) {
// Check the target token is compliant and
// verify the deposited token on L1 matches the L2 deposited token representation here
if (
// slither-disable-next-line reentrancy-events
ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
_l1Token == IL2StandardERC721(_l2Token).l1Token()
) {
// When a deposit is finalized, we give the NFT with the same tokenId to the account
// on L2.
// slither-disable-next-line reentrancy-events
IL2StandardERC721(_l2Token).mint(_to, _tokenId);
// slither-disable-next-line reentrancy-events
emit ERC721DepositFinalized(_l1Token, _l2Token, _from, _to, _tokenId, _data);
} else {
// Either the L2 token which is being deposited-into disagrees about the correct address
// of its L1 token, or does not support the correct interface.
// This should only happen if there is a malicious L2 token, or if a user somehow
// specified the wrong L2 token address to deposit into.
// In either case, we stop the process here and construct a withdrawal
// message so that users can get their NFT out in some cases.
// There is no way to prevent malicious token contracts altogether, but this does limit
// user error and mitigate some forms of malicious contract behavior.
bytes memory message = abi.encodeWithSelector(
IL1ERC721Bridge.finalizeERC721Withdrawal.selector,
_l1Token,
_l2Token,
_to, // switched the _to and _from here to bounce back the deposit to the sender
_from,
_tokenId,
_data
);
// Send message up to L1 bridge
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l1ERC721Bridge, 0, message);
// slither-disable-next-line reentrancy-events
emit ERC721DepositFailed(_l1Token, _l2Token, _from, _to, _tokenId, _data);
}
}
}
// 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;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Lib_Strings } from "../libraries/utils/Lib_Strings.sol";
import "./IL2StandardERC721.sol";
contract L2StandardERC721 is IL2StandardERC721, ERC721 {
address public l1Token;
address public l2Bridge;
string public baseTokenURI;
/**
* @param _l2Bridge Address of the L2 standard bridge.
* @param _l1Token Address of the corresponding L1 token.
* @param _name ERC721 name.
* @param _symbol ERC721 symbol.
*/
constructor(
address _l2Bridge,
address _l1Token,
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
l1Token = _l1Token;
l2Bridge = _l2Bridge;
// Creates a base URI in the format specified by EIP-681:
// https://eips.ethereum.org/EIPS/eip-681
baseTokenURI = string(
abi.encodePacked(
"ethereum:0x",
Lib_Strings.addressToString(_l1Token),
"@",
Strings.toString(block.chainid),
"/tokenURI?uint256="
)
);
}
modifier onlyL2Bridge() {
require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
_;
}
// slither-disable-next-line external-function
function supportsInterface(bytes4 _interfaceId)
public
view
override(ERC721, IERC165)
returns (bool)
{
bytes4 iface1 = type(IERC165).interfaceId;
bytes4 iface2 = type(IL2StandardERC721).interfaceId;
return
_interfaceId == iface1 ||
_interfaceId == iface2 ||
super.supportsInterface(_interfaceId);
}
// slither-disable-next-line external-function
function mint(address _to, uint256 _tokenId) public virtual onlyL2Bridge {
_mint(_to, _tokenId);
emit Mint(_to, _tokenId);
}
// slither-disable-next-line external-function
function burn(address _from, uint256 _tokenId) public virtual onlyL2Bridge {
_burn(_tokenId);
emit Burn(_from, _tokenId);
}
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}
}
// 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);
}
}
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
const deployFn: DeployFunction = async (hre) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Artifact__L1CrossDomainMessenger = require(`@eth-optimism/contracts/deployments/${hre.network.name}/Proxy__OVM_L1CrossDomainMessenger.json`)
const { deployer } = await hre.getNamedAccounts()
const L2ERC721Bridge = await hre.companionNetworks['l2'].deployments.get(
'L2ERC721Bridge'
)
await hre.deployments.deploy('L1ERC721Bridge', {
from: deployer,
args: [Artifact__L1CrossDomainMessenger.address, L2ERC721Bridge.address],
log: true,
})
}
deployFn.tags = ['L1ERC721Bridge']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { predeploys } from '@eth-optimism/contracts'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
await hre.deployments.deploy('L2ERC721Bridge', {
from: deployer,
args: [predeploys.L2CrossDomainMessenger],
log: true,
})
}
deployFn.tags = ['L2ERC721Bridge']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const L2ERC721Bridge = await hre.deployments.get('L2ERC721Bridge')
await hre.deployments.deploy('L2StandardERC721Factory', {
from: deployer,
args: [L2ERC721Bridge.address],
log: true,
})
}
deployFn.tags = ['L2StandardERC721Factory']
export default deployFn
/* Imports: External */
import { Contract } from 'ethers'
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const signer = hre.ethers.provider.getSigner(deployer)
const L1ERC721Bridge = await hre.companionNetworks['l1'].deployments.get(
'L1ERC721Bridge'
)
const Deployment__L2ERC721Bridge = await hre.deployments.get('L2ERC721Bridge')
const L2ERC721Bridge = new Contract(
Deployment__L2ERC721Bridge.address,
Deployment__L2ERC721Bridge.abi,
signer
)
const tx = await L2ERC721Bridge.initialize(L1ERC721Bridge.address)
await tx.wait()
// Ensures that the L2 bridge has been initialized with the correct parameters
await awaitCondition(
async () => {
return (
hexStringEquals(
await L2ERC721Bridge.messenger(),
predeploys.L2CrossDomainMessenger
) &&
hexStringEquals(
await L2ERC721Bridge.l1ERC721Bridge(),
L1ERC721Bridge.address
)
)
},
5000,
100
)
}
deployFn.tags = ['initialize-l2-erc721-bridge']
export default deployFn
......@@ -21,6 +21,9 @@ const config: HardhatUserConfig = {
optimism: {
chainId: 10,
url: 'https://mainnet.optimism.io',
companionNetworks: {
l1: 'mainnet',
},
verify: {
etherscan: {
apiKey: getenv('OPTIMISTIC_ETHERSCAN_API_KEY'),
......@@ -30,6 +33,9 @@ const config: HardhatUserConfig = {
'optimism-kovan': {
chainId: 69,
url: 'https://kovan.optimism.io',
companionNetworks: {
l1: 'kovan',
},
verify: {
etherscan: {
apiKey: getenv('OPTIMISTIC_ETHERSCAN_API_KEY'),
......@@ -39,6 +45,9 @@ const config: HardhatUserConfig = {
ethereum: {
chainId: 1,
url: `https://mainnet.infura.io/v3/${getenv('INFURA_PROJECT_ID')}`,
companionNetworks: {
l2: 'optimism',
},
verify: {
etherscan: {
apiKey: getenv('ETHEREUM_ETHERSCAN_API_KEY'),
......@@ -66,6 +75,9 @@ const config: HardhatUserConfig = {
kovan: {
chainId: 42,
url: `https://kovan.infura.io/v3/${getenv('INFURA_PROJECT_ID')}`,
companionNetworks: {
l2: 'optimism-kovan',
},
verify: {
etherscan: {
apiKey: getenv('ETHEREUM_ETHERSCAN_API_KEY'),
......
......@@ -54,6 +54,7 @@
},
"devDependencies": {
"@defi-wonderland/smock": "^2.0.7",
"@eth-optimism/contracts": "^0.5.26",
"@eth-optimism/core-utils": "^0.8.6",
"@ethersproject/hardware-wallets": "^5.6.1",
"@nomiclabs/hardhat-ethers": "^2.0.2",
......
/* Imports */
import { ethers } from 'hardhat'
import { Signer, ContractFactory, Contract, constants } from 'ethers'
import { Interface } from 'ethers/lib/utils'
import {
smock,
MockContractFactory,
FakeContract,
MockContract,
} from '@defi-wonderland/smock'
import ICrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/libraries/bridge/ICrossDomainMessenger.sol/ICrossDomainMessenger.json'
import { expect } from '../../../setup'
import {
NON_NULL_BYTES32,
NON_ZERO_ADDRESS,
} from '../../../../../contracts/test/helpers'
const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated'
const ERR_INVALID_X_DOMAIN_MSG_SENDER =
'OVM_XCHAIN: wrong sender of cross-domain message'
const DUMMY_L2_ERC721_ADDRESS = ethers.utils.getAddress(
'0x' + 'abba'.repeat(10)
)
const DUMMY_L2_BRIDGE_ADDRESS = ethers.utils.getAddress(
'0x' + 'acdc'.repeat(10)
)
const FINALIZATION_GAS = 1_200_000
describe('L1ERC721Bridge', () => {
// init signers
let l1MessengerImpersonator: Signer
let alice: Signer
let bob: Signer
let bobsAddress
let aliceAddress
let tokenId
let aliceInitialBalance
// we can just make up this string since it's on the "other" Layer
let Factory__L1ERC721: MockContractFactory<ContractFactory>
let IL2ERC721Bridge: Interface
before(async () => {
;[l1MessengerImpersonator, alice, bob] = await ethers.getSigners()
// deploy an ERC721 contract on L1
Factory__L1ERC721 = await smock.mock(
'@openzeppelin/contracts/token/ERC721/ERC721.sol:ERC721'
)
// get an L2ERC721Bridge Interface
IL2ERC721Bridge = (await ethers.getContractFactory('L2ERC721Bridge'))
.interface
aliceAddress = await alice.getAddress()
bobsAddress = await bob.getAddress()
aliceInitialBalance = 5
tokenId = 10
})
let L1ERC721: MockContract<Contract>
let L1ERC721Bridge: Contract
let Fake__L1CrossDomainMessenger: FakeContract
beforeEach(async () => {
// Get a new mock L1 messenger
Fake__L1CrossDomainMessenger = await smock.fake<Contract>(
new ethers.utils.Interface(ICrossDomainMessenger.abi),
{ address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Fake__L1CrossDomainMessenger.address} to mock calls
)
// Deploy the contract under test
L1ERC721Bridge = await (
await ethers.getContractFactory('L1ERC721Bridge')
).deploy(Fake__L1CrossDomainMessenger.address, DUMMY_L2_BRIDGE_ADDRESS)
L1ERC721 = await Factory__L1ERC721.deploy('L1ERC721', 'ERC')
await L1ERC721.setVariable('_owners', {
[tokenId]: aliceAddress,
})
await L1ERC721.setVariable('_balances', {
[aliceAddress]: aliceInitialBalance,
})
})
describe('ERC721 deposits', () => {
beforeEach(async () => {
await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId)
})
it('depositERC721() escrows the deposit and sends the correct deposit message', async () => {
// alice calls deposit on the bridge and the L1 bridge calls transferFrom on the token.
// emits an ERC721DepositInitiated event with the correct arguments.
await expect(
L1ERC721Bridge.connect(alice).depositERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
)
.to.emit(L1ERC721Bridge, 'ERC721DepositInitiated')
.withArgs(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
aliceAddress,
aliceAddress,
tokenId,
NON_NULL_BYTES32
)
const depositCallToMessenger =
Fake__L1CrossDomainMessenger.sendMessage.getCall(0)
// alice's balance decreases by 1
const depositerBalance = await L1ERC721.balanceOf(aliceAddress)
expect(depositerBalance).to.equal(aliceInitialBalance - 1)
// bridge's balance increases by 1
const bridgeBalance = await L1ERC721.balanceOf(L1ERC721Bridge.address)
expect(bridgeBalance).to.equal(1)
// Check the correct cross-chain call was sent:
// Message should be sent to the L2 bridge
expect(depositCallToMessenger.args[0]).to.equal(DUMMY_L2_BRIDGE_ADDRESS)
// Message data should be a call telling the L2DepositedERC721 to finalize the deposit
// the L1 bridge sends the correct message to the L1 messenger
expect(depositCallToMessenger.args[1]).to.equal(
IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
aliceAddress,
aliceAddress,
tokenId,
NON_NULL_BYTES32,
])
)
expect(depositCallToMessenger.args[2]).to.equal(FINALIZATION_GAS)
// Updates the deposits mapping
expect(
await L1ERC721Bridge.deposits(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId
)
).to.equal(true)
})
it('depositERC721To() escrows the deposited NFT and sends the correct deposit message', async () => {
// depositor calls deposit on the bridge and the L1 bridge calls transferFrom on the token.
// emits an ERC721DepositInitiated event with the correct arguments.
await expect(
L1ERC721Bridge.connect(alice).depositERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
)
.to.emit(L1ERC721Bridge, 'ERC721DepositInitiated')
.withArgs(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
aliceAddress,
bobsAddress,
tokenId,
NON_NULL_BYTES32
)
const depositCallToMessenger =
Fake__L1CrossDomainMessenger.sendMessage.getCall(0)
// alice's balance decreases by 1
const depositerBalance = await L1ERC721.balanceOf(aliceAddress)
expect(depositerBalance).to.equal(aliceInitialBalance - 1)
// bridge's balance is increased
const bridgeBalance = await L1ERC721.balanceOf(L1ERC721Bridge.address)
expect(bridgeBalance).to.equal(1)
// bridge is owner of tokenId
const tokenIdOwner = await L1ERC721.ownerOf(tokenId)
expect(tokenIdOwner).to.equal(L1ERC721Bridge.address)
// Check the correct cross-chain call was sent:
// Message should be sent to the L2DepositedERC721 on L2
expect(depositCallToMessenger.args[0]).to.equal(DUMMY_L2_BRIDGE_ADDRESS)
// Message data should be a call telling the L2DepositedERC721 to finalize the deposit
// the L1 bridge sends the correct message to the L1 messenger
expect(depositCallToMessenger.args[1]).to.equal(
IL2ERC721Bridge.encodeFunctionData('finalizeERC721Deposit', [
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
aliceAddress,
bobsAddress,
tokenId,
NON_NULL_BYTES32,
])
)
expect(depositCallToMessenger.args[2]).to.equal(FINALIZATION_GAS)
// Updates the deposits mapping
expect(
await L1ERC721Bridge.deposits(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId
)
).to.equal(true)
})
it('cannot depositERC721 from a contract account', async () => {
await expect(
L1ERC721Bridge.depositERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('Account not EOA')
})
describe('Handling ERC721.transferFrom() failures that revert', () => {
it('depositERC721(): will revert if ERC721.transferFrom() reverts', async () => {
await expect(
L1ERC721Bridge.connect(bob).depositERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721: transfer from incorrect owner')
})
it('depositERC721To(): will revert if ERC721.transferFrom() reverts', async () => {
await expect(
L1ERC721Bridge.connect(bob).depositERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721: transfer from incorrect owner')
})
it('depositERC721To(): will revert if the L1 ERC721 is zero address', async () => {
await expect(
L1ERC721Bridge.connect(alice).depositERC721To(
constants.AddressZero,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('function call to a non-contract account')
})
it('depositERC721To(): will revert if the L1 ERC721 has no code', async () => {
await expect(
L1ERC721Bridge.connect(alice).depositERC721To(
bobsAddress,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('function call to a non-contract account')
})
})
})
describe('ERC721 withdrawals', () => {
it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => {
await expect(
L1ERC721Bridge.connect(alice).finalizeERC721Withdrawal(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero,
constants.AddressZero,
tokenId,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_MESSENGER)
})
it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC721)', async () => {
await expect(
L1ERC721Bridge.finalizeERC721Withdrawal(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero,
constants.AddressZero,
tokenId,
NON_NULL_BYTES32,
{
from: Fake__L1CrossDomainMessenger.address,
}
)
).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER)
})
describe('withdrawal attempts that pass the onlyFromCrossDomainAccount check', () => {
beforeEach(async () => {
// First Alice will send an NFT so that there's a balance to be withdrawn
await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId)
await L1ERC721Bridge.connect(alice).depositERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
// make sure bridge owns NFT
expect(await L1ERC721.ownerOf(tokenId)).to.equal(L1ERC721Bridge.address)
Fake__L1CrossDomainMessenger.xDomainMessageSender.returns(
DUMMY_L2_BRIDGE_ADDRESS
)
})
it('should revert if the l1/l2 token pair has a token ID that has not been escrowed in the l1 bridge', async () => {
await expect(
L1ERC721Bridge.finalizeERC721Withdrawal(
L1ERC721.address,
DUMMY_L2_BRIDGE_ADDRESS, // incorrect l2 token address
constants.AddressZero,
constants.AddressZero,
tokenId,
NON_NULL_BYTES32,
{
from: Fake__L1CrossDomainMessenger.address,
}
)
).to.be.revertedWith('Token ID is not escrowed in the L1 Bridge')
})
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.
await expect(
L1ERC721Bridge.finalizeERC721Withdrawal(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
tokenId,
NON_NULL_BYTES32,
{ from: Fake__L1CrossDomainMessenger.address }
)
)
.to.emit(L1ERC721Bridge, 'ERC721WithdrawalFinalized')
.withArgs(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
tokenId,
NON_NULL_BYTES32
)
// NFT is transferred to new owner
expect(await L1ERC721.ownerOf(tokenId)).to.equal(NON_ZERO_ADDRESS)
// deposits state variable is updated
expect(
await L1ERC721Bridge.deposits(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId
)
).to.equal(false)
})
})
})
})
/* Imports */
import { ethers } from 'hardhat'
import { Signer, ContractFactory, Contract, constants } from 'ethers'
import { smock, FakeContract, MockContract } from '@defi-wonderland/smock'
import ICrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/libraries/bridge/ICrossDomainMessenger.sol/ICrossDomainMessenger.json'
import { expect } from '../../../setup'
import {
NON_NULL_BYTES32,
NON_ZERO_ADDRESS,
} from '../../../../../contracts/test/helpers'
const ERR_ALREADY_INITIALIZED = 'Contract has already been initialized.'
const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated'
const ERR_INVALID_X_DOMAIN_MSG_SENDER =
'OVM_XCHAIN: wrong sender of cross-domain message'
const DUMMY_L1BRIDGE_ADDRESS: string =
'0x1234123412341234123412341234123412341234'
const DUMMY_L1ERC721_ADDRESS: string =
'0x2234223412342234223422342234223422342234'
const ERR_INVALID_WITHDRAWAL: string =
'Withdrawal is not being initiated by NFT owner'
const ALICE_INITIAL_BALANCE: number = 10
const TOKEN_ID: number = 10
describe('L2ERC721Bridge', () => {
let alice: Signer
let aliceAddress: string
let bob: Signer
let bobsAddress: string
let l2MessengerImpersonator: Signer
let Factory__L1ERC721Bridge: ContractFactory
before(async () => {
// Create a special signer which will enable us to send messages from the L2Messenger contract
;[l2MessengerImpersonator, alice, bob] = await ethers.getSigners()
aliceAddress = await alice.getAddress()
bobsAddress = await bob.getAddress()
Factory__L1ERC721Bridge = await ethers.getContractFactory('L1ERC721Bridge')
})
let L2ERC721Bridge: Contract
let L2ERC721: Contract
let Fake__L2CrossDomainMessenger: FakeContract
beforeEach(async () => {
// Get a new fake L2 messenger
Fake__L2CrossDomainMessenger = await smock.fake<Contract>(
new ethers.utils.Interface(ICrossDomainMessenger.abi),
// This allows us to use an ethers override {from: Fake__L2CrossDomainMessenger.address} to mock calls
{ address: await l2MessengerImpersonator.getAddress() }
)
// Deploy the contract under test
L2ERC721Bridge = await (
await ethers.getContractFactory('L2ERC721Bridge')
).deploy(Fake__L2CrossDomainMessenger.address)
// Initialize the contract
await L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS)
// Deploy an L2 ERC721
L2ERC721 = await (
await ethers.getContractFactory('L2StandardERC721')
).deploy(
L2ERC721Bridge.address,
DUMMY_L1ERC721_ADDRESS,
'L2Token',
'L2T',
{ gasLimit: 4_000_000 } // Necessary to avoid an out-of-gas error
)
})
describe('initialize', () => {
it('Should only be callable once', async () => {
await expect(
L2ERC721Bridge.initialize(DUMMY_L1BRIDGE_ADDRESS)
).to.be.revertedWith(ERR_ALREADY_INITIALIZED)
})
})
// test the transfer flow of moving a token from L1 to L2
describe('finalizeERC721Deposit', () => {
it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => {
await expect(
L2ERC721Bridge.connect(alice).finalizeERC721Deposit(
DUMMY_L1ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
TOKEN_ID,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_MESSENGER)
})
it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L1ERC721Bridge)', async () => {
Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
NON_ZERO_ADDRESS
)
await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit(
DUMMY_L1ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
TOKEN_ID,
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER)
})
it('should initialize a withdrawal if the L2 token is not compliant', async () => {
// Deploy a non compliant ERC721
const NonCompliantERC721 = await (
await ethers.getContractFactory(
'@openzeppelin/contracts/token/ERC721/ERC721.sol:ERC721'
)
).deploy('L2Token', 'L2T')
Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
DUMMY_L1BRIDGE_ADDRESS
)
// A failed attempt to finalize the deposit causes an ERC721DepositFailed event to be emitted.
await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit(
DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
)
.to.emit(L2ERC721Bridge, 'ERC721DepositFailed')
.withArgs(
DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32
)
const withdrawalCallToMessenger =
Fake__L2CrossDomainMessenger.sendMessage.getCall(0)
expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS)
expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal',
[
DUMMY_L1ERC721_ADDRESS,
NonCompliantERC721.address,
bobsAddress,
aliceAddress,
TOKEN_ID,
NON_NULL_BYTES32,
]
)
)
expect(withdrawalCallToMessenger.args[2]).to.equal(0)
})
it('should credit funds to the depositor', async () => {
Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
DUMMY_L1BRIDGE_ADDRESS
)
// Assert that nobody owns the L2 token initially
await expect(L2ERC721.ownerOf(TOKEN_ID)).to.be.revertedWith(
'ERC721: owner query for nonexistent token'
)
// Successfully finalizes the deposit.
const expectedResult = expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeERC721Deposit(
DUMMY_L1ERC721_ADDRESS,
L2ERC721.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
)
// Depositing causes an ERC721DepositFinalized event to be emitted.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721DepositFinalized')
.withArgs(
DUMMY_L1ERC721_ADDRESS,
L2ERC721.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32
)
// Causes a Transfer event to be emitted from the L2 ERC721.
await expectedResult.to
.emit(L2ERC721, 'Transfer')
.withArgs(constants.AddressZero, bobsAddress, TOKEN_ID)
// Bob is now the owner of the L2 ERC721
const tokenIdOwner = await L2ERC721.ownerOf(TOKEN_ID)
tokenIdOwner.should.equal(bobsAddress)
})
})
describe('withdrawals', () => {
let Mock__L2Token: MockContract<Contract>
beforeEach(async () => {
Mock__L2Token = await (
await smock.mock('L2StandardERC721')
).deploy(
L2ERC721Bridge.address,
DUMMY_L1ERC721_ADDRESS,
'L2Token',
'L2T',
{ gasLimit: 4_000_000 } // Necessary to avoid an out-of-gas error
)
await Mock__L2Token.setVariable('_owners', {
[TOKEN_ID]: aliceAddress,
})
await Mock__L2Token.setVariable('_balances', {
[aliceAddress]: ALICE_INITIAL_BALANCE,
})
})
it('withdrawERC721() reverts when called by non-owner of nft', async () => {
await expect(
L2ERC721Bridge.connect(bob).withdrawERC721(
Mock__L2Token.address,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL)
})
it('withdrawERC721() reverts if called by a contract', async () => {
await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).withdrawERC721(
Mock__L2Token.address,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith('Account not EOA')
})
it('withdrawERC721() burns and sends the correct withdrawal message', async () => {
// Make sure that alice begins as the NFT owner
expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal.
const expectedResult = expect(
L2ERC721Bridge.connect(alice).withdrawERC721(
Mock__L2Token.address,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
)
// A successful withdrawal causes an ERC721WithdrawalInitiated event to be emitted from the L2 ERC721 Bridge.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated')
.withArgs(
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address,
aliceAddress,
aliceAddress,
TOKEN_ID,
NON_NULL_BYTES32
)
// A withdrawal also causes a Transfer event to be emitted the L2 ERC721, signifying that the L2 token
// has been burnt.
await expectedResult.to
.emit(Mock__L2Token, 'Transfer')
.withArgs(aliceAddress, constants.AddressZero, TOKEN_ID)
// Assert Alice's balance went down
const aliceBalance = await Mock__L2Token.balanceOf(aliceAddress)
expect(aliceBalance).to.equal(ALICE_INITIAL_BALANCE - 1)
// Assert that the token isn't owned by anyone
await expect(Mock__L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith(
'ERC721: owner query for nonexistent token'
)
const withdrawalCallToMessenger =
Fake__L2CrossDomainMessenger.sendMessage.getCall(0)
// Assert the correct cross-chain call was sent:
// Message should be sent to the L1ERC721Bridge on L1
expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS)
// Message data should be a call telling the L1ERC721Bridge to finalize the withdrawal
expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal',
[
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address,
aliceAddress,
aliceAddress,
TOKEN_ID,
NON_NULL_BYTES32,
]
)
)
// gaslimit should be correct
expect(withdrawalCallToMessenger.args[2]).to.equal(0)
})
it('withdrawERC721To() reverts when called by non-owner of nft', async () => {
await expect(
L2ERC721Bridge.connect(bob).withdrawERC721To(
Mock__L2Token.address,
bobsAddress,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL)
})
it('withdrawERC721To() burns and sends the correct withdrawal message', async () => {
// Make sure that alice begins as the NFT owner
expect(await Mock__L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal.
const expectedResult = expect(
L2ERC721Bridge.connect(alice).withdrawERC721To(
Mock__L2Token.address,
bobsAddress,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
)
// A successful withdrawal causes an ERC721WithdrawalInitiated event to be emitted from the L2 ERC721 Bridge.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721WithdrawalInitiated')
.withArgs(
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32
)
// A withdrawal also causes a Transfer event to be emitted the L2 ERC721, signifying that the L2 token
// has been burnt.
await expectedResult.to
.emit(Mock__L2Token, 'Transfer')
.withArgs(aliceAddress, constants.AddressZero, TOKEN_ID)
// Assert Alice's balance went down
const aliceBalance = await Mock__L2Token.balanceOf(aliceAddress)
expect(aliceBalance).to.equal(ALICE_INITIAL_BALANCE - 1)
// Assert that the token isn't owned by anyone
await expect(Mock__L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith(
'ERC721: owner query for nonexistent token'
)
const withdrawalCallToMessenger =
Fake__L2CrossDomainMessenger.sendMessage.getCall(0)
// Assert the correct cross-chain call was sent.
// Message should be sent to the L1ERC721Bridge on L1
expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS)
// The message data should be a call telling the L1ERC721Bridge to finalize the withdrawal
expect(withdrawalCallToMessenger.args[1]).to.equal(
Factory__L1ERC721Bridge.interface.encodeFunctionData(
'finalizeERC721Withdrawal',
[
DUMMY_L1ERC721_ADDRESS,
Mock__L2Token.address,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32,
]
)
)
// gas value is ignored and set to 0.
expect(withdrawalCallToMessenger.args[2]).to.equal(0)
})
})
})
/* External Imports */
import { ethers } from 'hardhat'
import { Signer, ContractFactory, Contract } from 'ethers'
import {
smock,
MockContractFactory,
MockContract,
} from '@defi-wonderland/smock'
/* Internal Imports */
import { expect } from '../../../setup'
const DUMMY_L2_BRIDGE_ADDRESS: string = ethers.utils.getAddress(
'0x' + 'acdc'.repeat(10)
)
describe('L2StandardERC721Factory', () => {
let signer: Signer
let Factory__L1ERC721: MockContractFactory<ContractFactory>
let L1ERC721: MockContract<Contract>
let L2StandardERC721Factory: Contract
let baseURI: string
let chainId: number
beforeEach(async () => {
;[signer] = await ethers.getSigners()
// deploy an ERC721 contract on L1
Factory__L1ERC721 = await smock.mock(
'@openzeppelin/contracts/token/ERC721/ERC721.sol:ERC721'
)
L1ERC721 = await Factory__L1ERC721.deploy('L1ERC721', 'ERC')
L2StandardERC721Factory = await (
await ethers.getContractFactory('L2StandardERC721Factory')
).deploy(DUMMY_L2_BRIDGE_ADDRESS)
chainId = await signer.getChainId()
baseURI = ''.concat(
'ethereum:',
L1ERC721.address.toLowerCase(),
'@',
chainId.toString(),
'/tokenURI?uint256='
)
})
it('should be deployed with the correct constructor argument', async () => {
expect(await L2StandardERC721Factory.l2ERC721Bridge()).to.equal(
DUMMY_L2_BRIDGE_ADDRESS
)
})
it('should be able to create a standard ERC721 contract', async () => {
const tx = await L2StandardERC721Factory.createStandardL2ERC721(
L1ERC721.address,
'L2ERC721',
'ERC'
)
const receipt = await tx.wait()
// Get the StandardL2ERC721Created event
const erc721CreatedEvent = receipt.events[0]
// Expect there to be an event emitted for the standard token creation
expect(erc721CreatedEvent.event).to.be.eq('StandardL2ERC721Created')
// Get the L2 ERC721 address from the emitted event and check it was created correctly
const l2ERC721Address = erc721CreatedEvent.args._l2Token
const L2StandardERC721 = new Contract(
l2ERC721Address,
(await ethers.getContractFactory('L2StandardERC721')).interface,
signer
)
expect(await L2StandardERC721.l2Bridge()).to.equal(DUMMY_L2_BRIDGE_ADDRESS)
expect(await L2StandardERC721.l1Token()).to.equal(L1ERC721.address)
expect(await L2StandardERC721.name()).to.equal('L2ERC721')
expect(await L2StandardERC721.symbol()).to.equal('ERC')
expect(await L2StandardERC721.baseTokenURI()).to.equal(baseURI)
expect(
await L2StandardERC721Factory.isStandardERC721(L2StandardERC721.address)
).to.equal(true)
expect(
await L2StandardERC721Factory.standardERC721Mapping(L1ERC721.address)
).to.equal(l2ERC721Address)
})
it('should not be able to create a standard token with a 0 address for l1 token', async () => {
await expect(
L2StandardERC721Factory.createStandardL2ERC721(
ethers.constants.AddressZero,
'L2ERC721',
'ERC'
)
).to.be.revertedWith('Must provide L1 token address')
})
it('should not be able create two l2 standard tokens with the same l1 token', async () => {
// The first call will not revert
await L2StandardERC721Factory.createStandardL2ERC721(
L1ERC721.address,
'L2ERC721',
'ERC'
)
await expect(
L2StandardERC721Factory.createStandardL2ERC721(
L1ERC721.address,
'L2ERC721',
'ERC'
)
).to.be.revertedWith('L2 Standard Token already exists for this L1 Token')
})
})
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
)
})
}
})
})
/* External Imports */
import { ethers } from 'hardhat'
import { Signer, Contract } from 'ethers'
import { smock, FakeContract } from '@defi-wonderland/smock'
/* Internal Imports */
import { expect } from '../../setup'
const TOKEN_ID = 10
const DUMMY_L1ERC721_ADDRESS: string =
'0x2234223412342234223422342234223422342234'
describe('L2StandardERC721', () => {
let l2BridgeImpersonator: Signer
let alice: Signer
let Fake__L2ERC721Bridge: FakeContract
let L2StandardERC721: Contract
let l2BridgeImpersonatorAddress: string
let aliceAddress: string
let baseUri: string
let chainId: number
before(async () => {
;[l2BridgeImpersonator, alice] = await ethers.getSigners()
l2BridgeImpersonatorAddress = await l2BridgeImpersonator.getAddress()
aliceAddress = await alice.getAddress()
chainId = await alice.getChainId()
baseUri = ''.concat(
'ethereum:',
DUMMY_L1ERC721_ADDRESS,
'@',
chainId.toString(),
'/tokenURI?uint256='
)
L2StandardERC721 = await (
await ethers.getContractFactory('L2StandardERC721')
).deploy(
l2BridgeImpersonatorAddress,
DUMMY_L1ERC721_ADDRESS,
'L2ERC721',
'ERC',
{ gasLimit: 4_000_000 } // Necessary to avoid an out-of-gas error
)
// Get a new fake L2 bridge
Fake__L2ERC721Bridge = await smock.fake<Contract>(
'L2ERC721Bridge',
// This allows us to use an ethers override {from: Fake__L2ERC721Bridge.address} to mock calls
{ address: await l2BridgeImpersonator.getAddress() }
)
// mint an nft to alice
await L2StandardERC721.connect(l2BridgeImpersonator).mint(
aliceAddress,
TOKEN_ID,
{
from: Fake__L2ERC721Bridge.address,
}
)
})
describe('constructor', () => {
it('should be able to create a standard ERC721 contract with the correct parameters', async () => {
expect(await L2StandardERC721.l2Bridge()).to.equal(
l2BridgeImpersonatorAddress
)
expect(await L2StandardERC721.l1Token()).to.equal(DUMMY_L1ERC721_ADDRESS)
expect(await L2StandardERC721.name()).to.equal('L2ERC721')
expect(await L2StandardERC721.symbol()).to.equal('ERC')
expect(await L2StandardERC721.baseTokenURI()).to.equal(baseUri)
// alice has been minted an nft
expect(await L2StandardERC721.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
})
})
describe('mint and burn', () => {
it('should not allow anyone but the L2 bridge to mint and burn', async () => {
await expect(
L2StandardERC721.connect(alice).mint(aliceAddress, 100)
).to.be.revertedWith('Only L2 Bridge can mint and burn')
await expect(
L2StandardERC721.connect(alice).burn(aliceAddress, 100)
).to.be.revertedWith('Only L2 Bridge can mint and burn')
})
})
describe('supportsInterface', () => {
it('should return the correct interface support', async () => {
const supportsERC165 = await L2StandardERC721.supportsInterface(
0x01ffc9a7
)
expect(supportsERC165).to.be.true
const supportsL2TokenInterface = await L2StandardERC721.supportsInterface(
0x1d1d8b63
)
expect(supportsL2TokenInterface).to.be.true
const supportsERC721Interface = await L2StandardERC721.supportsInterface(
0x80ac58cd
)
expect(supportsERC721Interface).to.be.true
const badSupports = await L2StandardERC721.supportsInterface(0xffffffff)
expect(badSupports).to.be.false
})
})
describe('tokenURI', () => {
it('should return the correct token uri', async () => {
const tokenUri = baseUri.concat(TOKEN_ID.toString())
expect(await L2StandardERC721.tokenURI(TOKEN_ID)).to.equal(tokenUri)
})
})
})
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