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