Commit 2749015e authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #4439 from ethereum-optimism/chore/delete-nft-bridge

ctp: delete ERC721Bridge from ctp
parents 7b9dca65 98c0c0ef
......@@ -2,7 +2,7 @@
pragma solidity ^0.8.9;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { OptimismMintableERC721 } from "@eth-optimism/contracts-periphery/contracts/universal/op-erc721/OptimismMintableERC721.sol";
import { OptimismMintableERC721 } from "@eth-optimism/contracts-bedrock/contracts/universal/OptimismMintableERC721.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
contract FakeOptimismMintableERC721 is OptimismMintableERC721 {
......
......@@ -30,6 +30,7 @@
"devDependencies": {
"@babel/eslint-parser": "^7.5.4",
"@eth-optimism/contracts": "^0.5.39",
"@eth-optimism/contracts-bedrock": "0.11.0",
"@eth-optimism/contracts-periphery": "^1.0.4",
"@eth-optimism/core-utils": "0.12.0",
"@eth-optimism/sdk": "1.8.0",
......
......@@ -4,10 +4,10 @@ import { ethers } from 'hardhat'
import { getChainId } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import Artifact__TestERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/testing/helpers/TestERC721.sol/TestERC721.json'
import Artifact__L1ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L1/L1ERC721Bridge.sol/L1ERC721Bridge.json'
import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/L2ERC721Bridge.sol/L2ERC721Bridge.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'
import Artifact__L1ERC721Bridge from '@eth-optimism/contracts-bedrock/artifacts/contracts/L1/L1ERC721Bridge.sol/L1ERC721Bridge.json'
import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-bedrock/artifacts/contracts/L2/L2ERC721Bridge.sol/L2ERC721Bridge.json'
import Artifact__OptimismMintableERC721Factory from '@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC721Factory.sol/OptimismMintableERC721Factory.json'
import Artifact__OptimismMintableERC721 from '@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC721.sol/OptimismMintableERC721.json'
/* Imports: Internal */
import { expect } from './shared/setup'
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ERC721Bridge } from "../universal/op-erc721/ERC721Bridge.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { L2ERC721Bridge } from "../L2/L2ERC721Bridge.sol";
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
/**
* @title L1ERC721Bridge
* @notice The L1 ERC721 bridge is a contract which works together with the L2 ERC721 bridge to
* make it possible to transfer ERC721 tokens from Ethereum to Optimism. This contract
* acts as an escrow for ERC721 tokens deposited into L2.
*/
contract L1ERC721Bridge is ERC721Bridge, Semver {
/**
* @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;
/**
* @custom:semver 1.0.0
*
* @param _messenger Address of the CrossDomainMessenger on this network.
* @param _otherBridge Address of the ERC721 bridge on the other network.
*/
constructor(address _messenger, address _otherBridge)
Semver(1, 0, 0)
ERC721Bridge(_messenger, _otherBridge)
{}
/*************************
* Cross-chain Functions *
*************************/
/**
* @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 finalizeBridgeERC721(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _tokenId,
bytes calldata _extraData
) external onlyOtherBridge {
require(_localToken != address(this), "L1ERC721Bridge: local token cannot be self");
// Checks that the L1/L2 NFT pair has a token ID that is escrowed in the L1 Bridge.
require(
deposits[_localToken][_remoteToken][_tokenId] == true,
"L1ERC721Bridge: Token ID is not escrowed in the L1 Bridge"
);
// Mark that the token ID for this L1/L2 token pair is no longer escrowed in the L1
// Bridge.
deposits[_localToken][_remoteToken][_tokenId] = false;
// When a withdrawal is finalized on L1, the L1 Bridge transfers the NFT to the
// withdrawer.
IERC721(_localToken).safeTransferFrom(address(this), _to, _tokenId);
// slither-disable-next-line reentrancy-events
emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
}
/**
* @inheritdoc ERC721Bridge
*/
function _initiateBridgeERC721(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _tokenId,
uint32 _minGasLimit,
bytes calldata _extraData
) internal override {
require(_remoteToken != address(0), "ERC721Bridge: remote token cannot be address(0)");
// 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
messenger.sendMessage(otherBridge, message, _minGasLimit);
emit ERC721BridgeInitiated(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ERC721Bridge } from "../universal/op-erc721/ERC721Bridge.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { L1ERC721Bridge } from "../L1/L1ERC721Bridge.sol";
import { IOptimismMintableERC721 } from "../universal/op-erc721/IOptimismMintableERC721.sol";
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
/**
* @title L2ERC721Bridge
* @notice The L2 ERC721 bridge is a contract which works together with the L1 ERC721 bridge to
* make it possible to transfer ERC721 tokens from Ethereum to Optimism. 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.
* **WARNING**: Do not bridge an ERC721 that was originally deployed on Optimism. This
* bridge ONLY supports ERC721s originally deployed on Ethereum. Users will need to
* wait for the one-week challenge period to elapse before their Optimism-native NFT
* can be refunded on L2.
*/
contract L2ERC721Bridge is ERC721Bridge, Semver {
/**
* @custom:semver 1.0.0
*
* @param _messenger Address of the CrossDomainMessenger on this network.
* @param _otherBridge Address of the ERC721 bridge on the other network.
*/
constructor(address _messenger, address _otherBridge)
Semver(1, 0, 0)
ERC721Bridge(_messenger, _otherBridge)
{}
/**
* @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 finalizeBridgeERC721(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _tokenId,
bytes calldata _extraData
) external onlyOtherBridge {
require(_localToken != address(this), "L2ERC721Bridge: local token cannot be self");
// Note that supportsInterface makes a callback to the _localToken address which is user
// provided.
require(
ERC165Checker.supportsInterface(_localToken, type(IOptimismMintableERC721).interfaceId),
"L2ERC721Bridge: local token interface is not compliant"
);
require(
_remoteToken == IOptimismMintableERC721(_localToken).remoteToken(),
"L2ERC721Bridge: wrong remote token for Optimism Mintable ERC721 local token"
);
// When a deposit is finalized, we give the NFT with the same tokenId to the account
// on L2. Note that safeMint makes a callback to the _to address which is user provided.
IOptimismMintableERC721(_localToken).safeMint(_to, _tokenId);
// slither-disable-next-line reentrancy-events
emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
}
/**
* @inheritdoc ERC721Bridge
*/
function _initiateBridgeERC721(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _tokenId,
uint32 _minGasLimit,
bytes calldata _extraData
) internal override {
require(_remoteToken != address(0), "ERC721Bridge: remote token cannot be address(0)");
// 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"
);
// 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"
);
// 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);
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
messenger.sendMessage(otherBridge, message, _minGasLimit);
// slither-disable-next-line reentrancy-events
emit ERC721BridgeInitiated(_localToken, remoteToken, _from, _to, _tokenId, _extraData);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {
CrossDomainMessenger
} from "@eth-optimism/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title ERC721Bridge
* @notice ERC721Bridge is a base contract for the L1 and L2 ERC721 bridges.
*/
abstract contract ERC721Bridge {
/**
* @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
);
/**
* @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
);
/**
* @notice Messenger contract on this domain.
*/
CrossDomainMessenger public immutable messenger;
/**
* @notice Address of the bridge on the other network.
*/
address public immutable otherBridge;
/**
* @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades.
*/
uint256[49] private __gap;
/**
* @notice Ensures that the caller is a cross-chain message from the other bridge.
*/
modifier onlyOtherBridge() {
require(
msg.sender == address(messenger) && messenger.xDomainMessageSender() == otherBridge,
"ERC721Bridge: function can only be called from the other bridge"
);
_;
}
/**
* @param _messenger Address of the CrossDomainMessenger on this network.
* @param _otherBridge Address of the ERC721 bridge on the other network.
*/
constructor(address _messenger, address _otherBridge) {
require(_messenger != address(0), "ERC721Bridge: messenger cannot be address(0)");
require(_otherBridge != address(0), "ERC721Bridge: other bridge cannot be address(0)");
messenger = CrossDomainMessenger(_messenger);
otherBridge = _otherBridge;
}
/**
* @notice Initiates a bridge of an NFT to the caller's account on the other chain. Note that
* this function can only be called by EOAs. Smart contract wallets should use the
* `bridgeERC721To` function after ensuring that the recipient address on the remote
* chain exists. Also note that the current owner of the token on this chain must
* approve this contract to operate the NFT before it can be bridged.
* **WARNING**: Do not bridge an ERC721 that was originally deployed on Optimism. This
* bridge only supports ERC721s originally deployed on Ethereum. Users will need to
* wait for the one-week challenge period to elapse before their Optimism-native NFT
* can be refunded 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 the other chain. Data supplied here will not
* be used to execute any code on the other chain and is only emitted as
* extra data for the convenience of off-chain tooling.
*/
function bridgeERC721(
address _localToken,
address _remoteToken,
uint256 _tokenId,
uint32 _minGasLimit,
bytes calldata _extraData
) external {
// Modifier requiring sender to be EOA. This prevents against a user error that would occur
// if the sender is a smart contract wallet that has a different address on the remote chain
// (or doesn't have an address on the remote chain at all). The user would fail to receive
// the NFT if they use this function because it sends the NFT to the same address as the
// caller. 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), "ERC721Bridge: account is not externally owned");
_initiateBridgeERC721(
_localToken,
_remoteToken,
msg.sender,
msg.sender,
_tokenId,
_minGasLimit,
_extraData
);
}
/**
* @notice Initiates a bridge of an NFT to some recipient's account on the other chain. Note
* that the current owner of the token on this chain must approve this contract to
* operate the NFT before it can be bridged.
* **WARNING**: Do not bridge an ERC721 that was originally deployed on Optimism. This
* bridge only supports ERC721s originally deployed on Ethereum. Users will need to
* wait for the one-week challenge period to elapse before their Optimism-native NFT
* can be refunded on L2.
*
* @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 the other chain. Data supplied here will not
* be used to execute any code on the other chain and is only emitted as
* extra data for the convenience of off-chain tooling.
*/
function bridgeERC721To(
address _localToken,
address _remoteToken,
address _to,
uint256 _tokenId,
uint32 _minGasLimit,
bytes calldata _extraData
) external {
require(_to != address(0), "ERC721Bridge: nft recipient cannot be address(0)");
_initiateBridgeERC721(
_localToken,
_remoteToken,
msg.sender,
_to,
_tokenId,
_minGasLimit,
_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 the other domain. Data supplied here will
* not be used to execute any code on the other domain 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 virtual;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {
IERC721Enumerable
} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.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 IERC721Enumerable {
/**
* @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 Chain ID of the chain where the remote token is deployed.
*/
function remoteChainId() external view returns (uint256);
/**
* @notice Address of the token on the remote domain.
*/
function remoteToken() external view returns (address);
/**
* @notice Address of the ERC721 bridge on this network.
*/
function bridge() external view returns (address);
/**
* @notice Mints some token ID for a user, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* @param _to Address of the user to mint the token for.
* @param _tokenId Token ID to mint.
*/
function safeMint(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;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {
ERC721Enumerable
} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
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 { IOptimismMintableERC721 } from "./IOptimismMintableERC721.sol";
/**
* @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 ERC721Enumerable, IOptimismMintableERC721 {
/**
* @inheritdoc IOptimismMintableERC721
*/
uint256 public immutable remoteChainId;
/**
* @inheritdoc IOptimismMintableERC721
*/
address public immutable remoteToken;
/**
* @inheritdoc IOptimismMintableERC721
*/
address public immutable bridge;
/**
* @notice Base token URI for this token.
*/
string public baseTokenURI;
/**
* @param _bridge Address of the bridge on this network.
* @param _remoteChainId Chain ID where the remote token is deployed.
* @param _remoteToken Address of the corresponding token on the other network.
* @param _name ERC721 name.
* @param _symbol ERC721 symbol.
*/
constructor(
address _bridge,
uint256 _remoteChainId,
address _remoteToken,
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
require(_bridge != address(0), "OptimismMintableERC721: bridge cannot be address(0)");
require(_remoteChainId != 0, "OptimismMintableERC721: remote chain id cannot be zero");
require(
_remoteToken != address(0),
"OptimismMintableERC721: remote token cannot be address(0)"
);
remoteChainId = _remoteChainId;
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:",
Strings.toHexString(uint160(_remoteToken), 20),
"@",
Strings.toString(_remoteChainId),
"/tokenURI?uint256="
)
);
}
/**
* @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");
_;
}
/**
* @inheritdoc IOptimismMintableERC721
*/
function safeMint(address _to, uint256 _tokenId) external virtual onlyBridge {
_safeMint(_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
override(ERC721Enumerable, IERC165)
returns (bool)
{
bytes4 iface1 = type(IERC165).interfaceId;
bytes4 iface2 = type(IOptimismMintableERC721).interfaceId;
return
_interfaceId == iface1 ||
_interfaceId == iface2 ||
super.supportsInterface(_interfaceId);
}
/**
* @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.15;
import { OptimismMintableERC721 } from "./OptimismMintableERC721.sol";
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
/**
* @title OptimismMintableERC721Factory
* @notice Factory contract for creating OptimismMintableERC721 contracts.
*/
contract OptimismMintableERC721Factory is Semver {
/**
* @notice Emitted whenever a new OptimismMintableERC721 contract is created.
*
* @param localToken Address of the token on the this domain.
* @param remoteToken Address of the token on the remote domain.
* @param deployer Address of the initiator of the deployment
*/
event OptimismMintableERC721Created(
address indexed localToken,
address indexed remoteToken,
address deployer
);
/**
* @notice Address of the ERC721 bridge on this network.
*/
address public immutable bridge;
/**
* @notice Chain ID for the remote network.
*/
uint256 public immutable remoteChainId;
/**
* @notice Tracks addresses created by this factory.
*/
mapping(address => bool) public isOptimismMintableERC721;
/**
* @custom:semver 1.0.0
*
* @param _bridge Address of the ERC721 bridge on this network.
*/
constructor(address _bridge, uint256 _remoteChainId) Semver(1, 0, 0) {
require(
_bridge != address(0),
"OptimismMintableERC721Factory: bridge cannot be address(0)"
);
require(
_remoteChainId != 0,
"OptimismMintableERC721Factory: remote chain id cannot be zero"
);
bridge = _bridge;
remoteChainId = _remoteChainId;
}
/**
* @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 createOptimismMintableERC721(
address _remoteToken,
string memory _name,
string memory _symbol
) external returns (address) {
require(
_remoteToken != address(0),
"OptimismMintableERC721Factory: L1 token address cannot be address(0)"
);
address localToken = address(
new OptimismMintableERC721(bridge, remoteChainId, _remoteToken, _name, _symbol)
);
isOptimismMintableERC721[localToken] = true;
emit OptimismMintableERC721Created(localToken, _remoteToken, msg.sender);
return localToken;
}
}
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import fetch from 'node-fetch'
import {
isTargetL1Network,
predeploy,
getProxyAdmin,
validateERC721Bridge,
} from '../../src/nft-bridge-deploy-helpers'
// Handle the `ops` deployment
const getL1CrossDomainMessengerProxyDeployment = async (
hre: HardhatRuntimeEnvironment
) => {
const network = hre.network.name
if (network === 'ops-l1') {
const res = await fetch(
'http://localhost:8080/deployments/local/Proxy__OVM_L1CrossDomainMessenger.json'
)
return res.json()
} else {
return hre.deployments.get('Proxy__OVM_L1CrossDomainMessenger')
}
}
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { deploy } = hre.deployments
const { getAddress } = hre.ethers.utils
if (!isTargetL1Network(hre.network.name)) {
console.log(`Deploying to unsupported network ${hre.network.name}`)
return
}
console.log(`Deploying L1ERC721Bridge to ${hre.network.name}`)
console.log(`Using deployer ${deployer}`)
const Deployment__L1ERC721BridgeProxy = await hre.deployments.get(
'L1ERC721BridgeProxy'
)
const L1ERC721BridgeProxy = await hre.ethers.getContractAt(
'Proxy',
Deployment__L1ERC721BridgeProxy.address
)
const admin = await L1ERC721BridgeProxy.callStatic.admin()
if (getAddress(admin) !== getAddress(deployer)) {
throw new Error('deployer is not proxy admin')
}
// Get the address of the currently deployed L1CrossDomainMessenger.
// This should be the address of the proxy
const Deployment__L1CrossDomainMessengerProxy =
await getL1CrossDomainMessengerProxyDeployment(hre)
const L1CrossDomainMessengerProxyAddress =
Deployment__L1CrossDomainMessengerProxy.address
// Deploy the L1ERC721Bridge. The arguments are
// - messenger
// - otherBridge
// Since this is the L1ERC721Bridge, the otherBridge is the
// predeploy address
await deploy('L1ERC721Bridge', {
from: deployer,
args: [L1CrossDomainMessengerProxyAddress, predeploy],
log: true,
waitConfirmations: 1,
})
const Deployment__L1ERC721Bridge = await hre.deployments.get('L1ERC721Bridge')
console.log(
`L1ERC721Bridge deployed to ${Deployment__L1ERC721Bridge.address}`
)
await validateERC721Bridge(hre, Deployment__L1ERC721Bridge.address, {
messenger: L1CrossDomainMessengerProxyAddress,
otherBridge: predeploy,
})
{
// Upgrade the Proxy to the newly deployed implementation
const tx = await L1ERC721BridgeProxy.upgradeTo(
Deployment__L1ERC721Bridge.address
)
const receipt = await tx.wait()
console.log(`L1ERC721BridgeProxy upgraded: ${receipt.transactionHash}`)
}
{
// Set the admin correctly
const newAdmin = getProxyAdmin(hre.network.name)
const tx = await L1ERC721BridgeProxy.changeAdmin(newAdmin)
const receipt = await tx.wait()
console.log(`L1ERC721BridgeProxy admin updated: ${receipt.transactionHash}`)
}
await validateERC721Bridge(hre, L1ERC721BridgeProxy.address, {
messenger: L1CrossDomainMessengerProxyAddress,
otherBridge: predeploy,
})
}
deployFn.tags = ['L1ERC721BridgeImplementation']
deployFn.dependencies = ['L1ERC721BridgeProxy']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import { isTargetL1Network } from '../../src/nft-bridge-deploy-helpers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { deploy } = hre.deployments
if (!isTargetL1Network(hre.network.name)) {
console.log(`Deploying to unsupported network ${hre.network.name}`)
return
}
console.log(`Deploying L1ERC721BridgeProxy to ${hre.network.name}`)
console.log(`Using deployer ${deployer}`)
await deploy('L1ERC721BridgeProxy', {
contract: 'Proxy',
from: deployer,
args: [deployer],
log: true,
waitConfirmations: 1,
})
const Deployment__L1ERC721BridgeProxy = await hre.deployments.get(
'L1ERC721BridgeProxy'
)
console.log(
`L1ERC721BridgeProxy deployed to ${Deployment__L1ERC721BridgeProxy.address}`
)
}
deployFn.tags = ['L1ERC721BridgeProxy']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import { predeploys } from '@eth-optimism/contracts'
import {
isTargetL2Network,
predeploy,
validateERC721Bridge,
getProxyAdmin,
} from '../../src/nft-bridge-deploy-helpers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { getAddress } = hre.ethers.utils
if (!isTargetL2Network(hre.network.name)) {
console.log(`Deploying to unsupported network ${hre.network.name}`)
return
}
console.log(`Deploying L2ERC721Bridge to ${hre.network.name}`)
console.log(`Using deployer ${deployer}`)
const L2ERC721BridgeProxy = await hre.ethers.getContractAt('Proxy', predeploy)
// Check to make sure that the admin of the proxy is the deployer.
// The deployer of the L2ERC721Bridge should be the same as the
// admin of the L2ERC721BridgeProxy so that it is easy to upgrade
// the implementation. The admin is then changed depending on the
// network after the L2ERC721BridgeProxy is upgraded to the implementation
const admin = await L2ERC721BridgeProxy.callStatic.admin()
if (getAddress(admin) !== getAddress(deployer)) {
throw new Error(`Unexpected admin ${admin}`)
}
const Deployment__L1ERC721Bridge = await hre.companionNetworks[
'l1'
].deployments.get('L1ERC721BridgeProxy')
const L1ERC721BridgeAddress = Deployment__L1ERC721Bridge.address
// Deploy the L2ERC721Bridge implementation
await hre.deployments.deploy('L2ERC721Bridge', {
from: deployer,
args: [predeploys.L2CrossDomainMessenger, L1ERC721BridgeAddress],
log: true,
waitConfirmations: 1,
})
const Deployment__L2ERC721Bridge = await hre.deployments.get('L2ERC721Bridge')
console.log(
`L2ERC721Bridge deployed to ${Deployment__L2ERC721Bridge.address}`
)
await validateERC721Bridge(hre, Deployment__L2ERC721Bridge.address, {
messenger: predeploys.L2CrossDomainMessenger,
otherBridge: L1ERC721BridgeAddress,
})
{
// Upgrade the implementation of the proxy to the newly deployed
// L2ERC721Bridge
const tx = await L2ERC721BridgeProxy.upgradeTo(
Deployment__L2ERC721Bridge.address
)
const receipt = await tx.wait()
console.log(
`Upgraded the implementation of the L2ERC721BridgeProxy: ${receipt.transactionhash}`
)
}
await validateERC721Bridge(hre, L2ERC721BridgeProxy.address, {
messenger: predeploys.L2CrossDomainMessenger,
otherBridge: L1ERC721BridgeAddress,
})
{
const newAdmin = getProxyAdmin(hre.network.name)
console.log(`Changing admin to ${newAdmin}`)
const tx = await L2ERC721BridgeProxy.changeAdmin(newAdmin)
const receipt = await tx.wait()
console.log(
`Changed admin of the L2ERC721BridgeProxy: ${receipt.transactionHash}`
)
}
}
deployFn.tags = ['L2ERC721BridgeImplementation']
deployFn.dependencies = ['L2ERC721BridgeProxy', 'L1ERC721BridgeProxy']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
const predeploy = '0x4200000000000000000000000000000000000014'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { getAddress } = hre.ethers.utils
console.log(`Deploying L2ERC721BridgeProxy to ${hre.network.name}`)
console.log(`Using deployer ${deployer}`)
// Check to make sure that the Proxy has not been deployed yet
const pre = await hre.ethers.provider.getCode(predeploy, 'latest')
if (pre !== '0x') {
console.log(`Code already deployed to ${predeploy}`)
return
}
// A special deployer account must be used
const mainnetDeployer = getAddress(
'0x53A6eecC2dD4795Fcc68940ddc6B4d53Bd88Bd9E'
)
const goerliDeployer = getAddress(
'0x5c679a57e018f5f146838138d3e032ef4913d551'
)
const kovanDeployer = getAddress('0xa81224490b9fa4930a2e920550cd1c9106bb6d9e')
const localDeployer = getAddress('0xdfc82d475833a50de90c642770f34a9db7deb725')
// Deploy the L2ERC721BridgeProxy as a predeploy address
if (hre.network.name === 'optimism') {
if (getAddress(deployer) !== mainnetDeployer) {
throw new Error(`Incorrect deployer: ${deployer}`)
}
} else if (hre.network.name === 'optimism-goerli') {
if (getAddress(deployer) !== goerliDeployer) {
throw new Error(`Incorrect deployer: ${deployer}`)
}
} else if (hre.network.name === 'ops-l2') {
if (getAddress(deployer) !== localDeployer) {
throw new Error(`Incorrect deployer: ${deployer}`)
}
} else if (hre.network.name === 'optimism-kovan') {
if (getAddress(deployer) !== kovanDeployer) {
throw new Error(`Incorrect deployer: ${deployer}`)
}
} else {
throw new Error(`Unknown network: ${hre.network.name}`)
}
// Set the deployer as the admin of the Proxy. This is
// temporary, the admin will be updated when deploying
// the implementation
await hre.deployments.deploy('L2ERC721BridgeProxy', {
contract: 'Proxy',
from: deployer,
args: [deployer],
log: true,
waitConfirmations: 1,
})
// Check that the Proxy was deployed to the correct address
const code = await hre.ethers.provider.getCode(predeploy)
if (code === '0x') {
throw new Error('Code is not set at expected predeploy address')
}
console.log(`L2ERC721BridgeProxy deployed to ${predeploy}`)
}
deployFn.tags = ['L2ERC721BridgeProxy']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import { getProxyAdmin, predeploy } from '../../src/nft-bridge-deploy-helpers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { getAddress } = hre.ethers.utils
console.log(`Deploying OptimismMintableERC721Factory to ${hre.network.name}`)
console.log(`Using deployer ${deployer}`)
const Deployment__OptimismMintableERC721FactoryProxy =
await hre.deployments.get('OptimismMintableERC721FactoryProxy')
const OptimismMintableERC721FactoryProxy = await hre.ethers.getContractAt(
'Proxy',
Deployment__OptimismMintableERC721FactoryProxy.address
)
// Check that the admin of the OptimismMintableERC721FactoryProxy is the
// deployer. This makes it easy to upgrade the implementation of the proxy
// and then transfer the admin privilege after deploying the implementation
const admin = await OptimismMintableERC721FactoryProxy.callStatic.admin()
if (getAddress(admin) !== getAddress(deployer)) {
throw new Error('deployer is not proxy admin')
}
let remoteChainId: number
if (hre.network.name === 'optimism') {
remoteChainId = 1
} else if (hre.network.name === 'optimism-goerli') {
remoteChainId = 5
} else if (hre.network.name === 'ops-l2') {
remoteChainId = 31337
} else if (hre.network.name === 'optimism-kovan') {
remoteChainId = 42
} else {
remoteChainId = hre.deployConfig.remoteChainId
}
if (typeof remoteChainId !== 'number') {
throw new Error('remoteChainId not defined')
}
await hre.deployments.deploy('OptimismMintableERC721Factory', {
from: deployer,
args: [predeploy, remoteChainId],
log: true,
waitConfirmations: 1,
})
const Deployment__OptimismMintableERC721Factory = await hre.deployments.get(
'OptimismMintableERC721Factory'
)
console.log(
`OptimismMintableERC721Factory deployed to ${Deployment__OptimismMintableERC721Factory.address}`
)
{
// Upgrade the Proxy to the newly deployed implementation
const tx = await OptimismMintableERC721FactoryProxy.upgradeTo(
Deployment__OptimismMintableERC721Factory.address
)
const receipt = await tx.wait()
console.log(
`OptimismMintableERC721FactoryProxy upgraded: ${receipt.transactionHash}`
)
}
{
const newAdmin = getProxyAdmin(hre.network.name)
const tx = await OptimismMintableERC721FactoryProxy.changeAdmin(newAdmin)
const receipt = await tx.wait()
console.log(
`OptimismMintableERC721FactoryProxy admin updated: ${receipt.transactionHash}`
)
}
}
deployFn.tags = ['OptimismMintableERC721FactoryImplementation']
deployFn.dependencies = ['OptimismMintableERC721FactoryProxy']
export default deployFn
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { deploy } = hre.deployments
console.log(
`Deploying OptimismMintableERC721FactoryProxy to ${hre.network.name}`
)
console.log(`Using deployer ${deployer}`)
// Deploy the OptimismMintableERC721FactoryProxy with
// the deployer as the admin. The admin and implementation
// will be updated with the deployment of the implementation
await deploy('OptimismMintableERC721FactoryProxy', {
contract: 'Proxy',
from: deployer,
args: [deployer],
log: true,
waitConfirmations: 1,
})
const Deployment__OptimismMintableERC721FactoryProxy =
await hre.deployments.get('OptimismMintableERC721FactoryProxy')
console.log(
`OptimismMintableERC721FactoryProxy deployed to ${Deployment__OptimismMintableERC721FactoryProxy.address}`
)
}
deployFn.tags = ['OptimismMintableERC721FactoryProxy']
export default deployFn
......@@ -55,7 +55,6 @@
},
"devDependencies": {
"@defi-wonderland/smock": "^2.0.7",
"@eth-optimism/contracts": "^0.5.39",
"@eth-optimism/contracts-bedrock": "0.10.0",
"@eth-optimism/core-utils": "^0.12.0",
"@eth-optimism/hardhat-deploy-config": "^0.2.5",
......
/* 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 { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../../helpers'
import { expect } from '../../setup'
const ERR_INVALID_X_DOMAIN_MESSAGE =
'ERC721Bridge: function can only be called from the other bridge'
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 = 600_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
let Factory__L1ERC721Bridge: ContractFactory
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
Factory__L1ERC721Bridge = await ethers.getContractFactory('L1ERC721Bridge')
L1ERC721Bridge = await Factory__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('constructor', async () => {
it('initializes correctly', async () => {
it('reverts if cross domain messenger is address(0)', async () => {
await expect(
Factory__L1ERC721Bridge.deploy(
constants.AddressZero,
DUMMY_L2_BRIDGE_ADDRESS
)
).to.be.revertedWith('ERC721Bridge: messenger cannot be address(0)')
})
it('reverts if other bridge is address(0)', async () => {
await expect(
Factory__L1ERC721Bridge.deploy(
Fake__L1CrossDomainMessenger.address,
constants.AddressZero
)
).to.be.revertedWith('ERC721Bridge: other bridge cannot be address(0)')
})
expect(await L1ERC721Bridge.messenger()).equals(
Fake__L1CrossDomainMessenger.address
)
expect(await L1ERC721Bridge.otherBridge()).equals(DUMMY_L2_BRIDGE_ADDRESS)
})
})
describe('ERC721 deposits', () => {
beforeEach(async () => {
await L1ERC721.connect(alice).approve(L1ERC721Bridge.address, tokenId)
})
it('bridgeERC721() reverts if remote token is address(0)', async () => {
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721(
L1ERC721.address,
constants.AddressZero,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: remote token cannot be address(0)')
})
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 ERC721BridgeInitiated event with the correct arguments.
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
)
.to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated')
.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('finalizeBridgeERC721', [
DUMMY_L2_ERC721_ADDRESS,
L1ERC721.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('bridgeERC721To() reverts if NFT receiver is address(0)', async () => {
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: nft recipient cannot be address(0)')
})
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 ERC721BridgeInitiated event with the correct arguments.
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
)
.to.emit(L1ERC721Bridge, 'ERC721BridgeInitiated')
.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('finalizeBridgeERC721', [
DUMMY_L2_ERC721_ADDRESS,
L1ERC721.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 bridgeERC721 from a contract account', async () => {
await expect(
L1ERC721Bridge.bridgeERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: account is not externally owned')
})
describe('Handling ERC721.transferFrom() failures that revert', () => {
it('bridgeERC721(): will revert if ERC721.transferFrom() reverts', async () => {
await expect(
L1ERC721Bridge.connect(bob).bridgeERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721: transfer from incorrect owner')
})
it('bridgeERC721To(): will revert if ERC721.transferFrom() reverts', async () => {
await expect(
L1ERC721Bridge.connect(bob).bridgeERC721To(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721: transfer from incorrect owner')
})
it('bridgeERC721To(): will revert if the L1 ERC721 is zero address', async () => {
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721To(
constants.AddressZero,
DUMMY_L2_ERC721_ADDRESS,
bobsAddress,
tokenId,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('function call to a non-contract account')
})
it('bridgeERC721To(): will revert if the L1 ERC721 has no code', async () => {
await expect(
L1ERC721Bridge.connect(alice).bridgeERC721To(
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).finalizeBridgeERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
constants.AddressZero,
constants.AddressZero,
tokenId,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MESSAGE)
})
it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC721)', async () => {
await expect(
L1ERC721Bridge.finalizeBridgeERC721(
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_MESSAGE)
})
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).bridgeERC721(
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 credit funds to the withdrawer to finalize withdrawal', async () => {
// finalizing the withdrawal emits an ERC721BridgeFinalized event with the correct arguments.
await expect(
L1ERC721Bridge.finalizeBridgeERC721(
L1ERC721.address,
DUMMY_L2_ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
tokenId,
NON_NULL_BYTES32,
{ from: Fake__L1CrossDomainMessenger.address }
)
)
.to.emit(L1ERC721Bridge, 'ERC721BridgeFinalized')
.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 } from '@defi-wonderland/smock'
import ICrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/libraries/bridge/ICrossDomainMessenger.sol/ICrossDomainMessenger.json'
import { toRpcHexString } from '@eth-optimism/core-utils'
import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../../helpers'
import { expect } from '../../setup'
const ERR_INVALID_X_DOMAIN_MESSAGE =
'ERC721Bridge: function can only be called from the other bridge'
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 TOKEN_ID: number = 10
const FINALIZATION_GAS = 600_000
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 Factory__L2ERC721Bridge: ContractFactory
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
Factory__L2ERC721Bridge = await ethers.getContractFactory('L2ERC721Bridge')
L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy(
Fake__L2CrossDomainMessenger.address,
DUMMY_L1BRIDGE_ADDRESS
)
// Deploy an L2 ERC721
L2ERC721 = await (
await ethers.getContractFactory('OptimismMintableERC721')
).deploy(
L2ERC721Bridge.address,
100,
DUMMY_L1ERC721_ADDRESS,
'L2Token',
'L2T',
{ gasLimit: 4_000_000 } // Necessary to avoid an out-of-gas error
)
})
describe('constructor', async () => {
it('reverts if cross domain messenger is address(0)', async () => {
await expect(
Factory__L2ERC721Bridge.deploy(
constants.AddressZero,
DUMMY_L1BRIDGE_ADDRESS
)
).to.be.revertedWith('ERC721Bridge: messenger cannot be address(0)')
})
it('reverts if other bridge is address(0)', async () => {
await expect(
Factory__L2ERC721Bridge.deploy(
Fake__L2CrossDomainMessenger.address,
constants.AddressZero
)
).to.be.revertedWith('ERC721Bridge: other bridge cannot be address(0)')
})
it('initializes correctly', async () => {
expect(await L2ERC721Bridge.messenger()).equals(
Fake__L2CrossDomainMessenger.address
)
expect(await L2ERC721Bridge.otherBridge()).equals(DUMMY_L1BRIDGE_ADDRESS)
})
})
// test the transfer flow of moving a token from L1 to L2
describe('finalizeBridgeERC721', () => {
it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => {
await expect(
L2ERC721Bridge.connect(alice).finalizeBridgeERC721(
DUMMY_L1ERC721_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
NON_ZERO_ADDRESS,
TOKEN_ID,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MESSAGE)
})
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).finalizeBridgeERC721(
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_MESSAGE)
})
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: invalid token ID'
)
// Successfully finalizes the deposit.
const expectedResult = expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721(
L2ERC721.address,
DUMMY_L1ERC721_ADDRESS,
aliceAddress,
bobsAddress,
TOKEN_ID,
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
)
// Depositing causes an ERC721BridgeFinalized event to be emitted.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721BridgeFinalized')
.withArgs(
L2ERC721.address,
DUMMY_L1ERC721_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 L2Token: Contract
beforeEach(async () => {
L2Token = await (
await ethers.getContractFactory('OptimismMintableERC721')
).deploy(
L2ERC721Bridge.address,
100,
DUMMY_L1ERC721_ADDRESS,
'L2Token',
'L2T'
)
await ethers.provider.send('hardhat_impersonateAccount', [
L2ERC721Bridge.address,
])
await ethers.provider.send('hardhat_setBalance', [
L2ERC721Bridge.address,
toRpcHexString(ethers.utils.parseEther('1')),
])
const signer = await ethers.getSigner(L2ERC721Bridge.address)
await L2Token.connect(signer).safeMint(aliceAddress, TOKEN_ID)
})
it('bridgeERC721() reverts if remote token is address(0)', async () => {
await expect(
L2ERC721Bridge.connect(alice).bridgeERC721(
L2Token.address,
constants.AddressZero,
TOKEN_ID,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: remote token cannot be address(0)')
})
it('bridgeERC721() reverts when called by non-owner of nft', async () => {
await expect(
L2ERC721Bridge.connect(bob).bridgeERC721(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL)
})
it('bridgeERC721() reverts if called by a contract', async () => {
await expect(
L2ERC721Bridge.connect(l2MessengerImpersonator).bridgeERC721(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: account is not externally owned')
})
it('bridgeERC721() burns and sends the correct withdrawal message', async () => {
// Make sure that alice begins as the NFT owner
expect(await L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal.
const expectedResult = expect(
L2ERC721Bridge.connect(alice).bridgeERC721(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
)
// A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs(
L2Token.address,
DUMMY_L1ERC721_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(L2Token, 'Transfer')
.withArgs(aliceAddress, constants.AddressZero, TOKEN_ID)
// Assert Alice's balance went down
const aliceBalance = await L2Token.balanceOf(aliceAddress)
expect(aliceBalance).to.equal(0)
// Assert that the token isn't owned by anyone
await expect(L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith(
'ERC721: invalid token ID'
)
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(
'finalizeBridgeERC721',
[
DUMMY_L1ERC721_ADDRESS,
L2Token.address,
aliceAddress,
aliceAddress,
TOKEN_ID,
NON_NULL_BYTES32,
]
)
)
// gaslimit should be correct
expect(withdrawalCallToMessenger.args[2]).to.equal(0)
})
it('bridgeERC721To() reverts if NFT receiver is address(0)', async () => {
await expect(
L2ERC721Bridge.connect(alice).bridgeERC721To(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
constants.AddressZero,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith('ERC721Bridge: nft recipient cannot be address(0)')
})
it('bridgeERC721To() reverts when called by non-owner of nft', async () => {
await expect(
L2ERC721Bridge.connect(bob).bridgeERC721To(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
bobsAddress,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
).to.be.revertedWith(ERR_INVALID_WITHDRAWAL)
})
it('bridgeERC721To() burns and sends the correct withdrawal message', async () => {
// Make sure that alice begins as the NFT owner
expect(await L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
// Initiates a successful withdrawal.
const expectedResult = expect(
L2ERC721Bridge.connect(alice).bridgeERC721To(
L2Token.address,
DUMMY_L1ERC721_ADDRESS,
bobsAddress,
TOKEN_ID,
0,
NON_NULL_BYTES32
)
)
// A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge.
await expectedResult.to
.emit(L2ERC721Bridge, 'ERC721BridgeInitiated')
.withArgs(
L2Token.address,
DUMMY_L1ERC721_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(L2Token, 'Transfer')
.withArgs(aliceAddress, constants.AddressZero, TOKEN_ID)
// Assert Alice's balance went down
const aliceBalance = await L2Token.balanceOf(aliceAddress)
expect(aliceBalance).to.equal(0)
// Assert that the token isn't owned by anyone
await expect(L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith(
'ERC721: invalid token ID'
)
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(
'finalizeBridgeERC721',
[
DUMMY_L1ERC721_ADDRESS,
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, 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 =
'0x0034223412342234223422342234223422342234'
describe('OptimismMintableERC721', () => {
let l2BridgeImpersonator: Signer
let alice: Signer
let Fake__L2ERC721Bridge: FakeContract
let OptimismMintableERC721: Contract
let l2BridgeImpersonatorAddress: string
let aliceAddress: string
let baseUri: string
const remoteChainId = 100
let Factory__OptimismMintableERC721
before(async () => {
;[l2BridgeImpersonator, alice] = await ethers.getSigners()
l2BridgeImpersonatorAddress = await l2BridgeImpersonator.getAddress()
aliceAddress = await alice.getAddress()
baseUri = ''.concat(
'ethereum:',
DUMMY_L1ERC721_ADDRESS,
'@',
remoteChainId.toString(),
'/tokenURI?uint256='
)
Factory__OptimismMintableERC721 = await ethers.getContractFactory(
'OptimismMintableERC721'
)
OptimismMintableERC721 = await Factory__OptimismMintableERC721.deploy(
l2BridgeImpersonatorAddress,
remoteChainId,
DUMMY_L1ERC721_ADDRESS,
'L2ERC721',
'ERC'
)
// 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 OptimismMintableERC721.connect(l2BridgeImpersonator).safeMint(
aliceAddress,
TOKEN_ID,
{
from: Fake__L2ERC721Bridge.address,
}
)
})
describe('constructor', () => {
it('should revert if bridge is address(0)', async () => {
await expect(
Factory__OptimismMintableERC721.deploy(
ethers.constants.AddressZero,
remoteChainId,
DUMMY_L1ERC721_ADDRESS,
'L2ERC721',
'ERC'
)
).to.be.revertedWith(
'OptimismMintableERC721: bridge cannot be address(0)'
)
})
it('should revert if remote chain id is address(0)', async () => {
await expect(
Factory__OptimismMintableERC721.deploy(
l2BridgeImpersonatorAddress,
0,
DUMMY_L1ERC721_ADDRESS,
'L2ERC721',
'ERC'
)
).to.be.revertedWith(
'OptimismMintableERC721: remote chain id cannot be zero'
)
})
it('should revert if remote token is address(0)', async () => {
await expect(
Factory__OptimismMintableERC721.deploy(
l2BridgeImpersonatorAddress,
remoteChainId,
ethers.constants.AddressZero,
'L2ERC721',
'ERC'
)
).to.be.revertedWith(
'OptimismMintableERC721: remote token cannot be address(0)'
)
})
it('should be able to create a standard ERC721 contract with the correct parameters', async () => {
expect(await OptimismMintableERC721.bridge()).to.equal(
l2BridgeImpersonatorAddress
)
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 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(
OptimismMintableERC721.connect(alice).safeMint(aliceAddress, 100)
).to.be.revertedWith(
'OptimismMintableERC721: only bridge can call this function'
)
await expect(
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 () => {
// ERC165
expect(await OptimismMintableERC721.supportsInterface(0x01ffc9a7)).to.be
.true
// OptimismMintableERC721
expect(await OptimismMintableERC721.supportsInterface(0xe49bc7f8)).to.be
.true
// ERC721
expect(await OptimismMintableERC721.supportsInterface(0x80ac58cd)).to.be
.true
// 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 OptimismMintableERC721.tokenURI(TOKEN_ID)).to.equal(tokenUri)
})
})
})
/* 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('OptimismMintableERC721Factory', () => {
let signer: Signer
let Factory__L1ERC721: MockContractFactory<ContractFactory>
let L1ERC721: MockContract<Contract>
let OptimismMintableERC721Factory: Contract
let baseURI: string
const remoteChainId = 100
let Factory__OptimismMintableERC721Factory: ContractFactory
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')
Factory__OptimismMintableERC721Factory = await ethers.getContractFactory(
'OptimismMintableERC721Factory'
)
OptimismMintableERC721Factory =
await Factory__OptimismMintableERC721Factory.deploy(
DUMMY_L2_BRIDGE_ADDRESS,
remoteChainId
)
baseURI = ''.concat(
'ethereum:',
L1ERC721.address.toLowerCase(),
'@',
remoteChainId.toString(),
'/tokenURI?uint256='
)
})
it('should revert if bridge is initialized as address(0)', async () => {
await expect(
Factory__OptimismMintableERC721Factory.deploy(
ethers.constants.AddressZero,
remoteChainId
)
).to.be.revertedWith(
'OptimismMintableERC721Factory: bridge cannot be address(0)'
)
})
it('should revert if remote chain id is initialized as zero', async () => {
await expect(
Factory__OptimismMintableERC721Factory.deploy(DUMMY_L2_BRIDGE_ADDRESS, 0)
).to.be.revertedWith(
'OptimismMintableERC721Factory: remote chain id cannot be zero'
)
})
it('should be deployed with the correct constructor argument', async () => {
expect(await OptimismMintableERC721Factory.bridge()).to.equal(
DUMMY_L2_BRIDGE_ADDRESS
)
})
it('should be able to create a standard ERC721 contract', async () => {
const tx = await OptimismMintableERC721Factory.createOptimismMintableERC721(
L1ERC721.address,
'L2ERC721',
'ERC'
)
const receipt = await tx.wait()
// 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('OptimismMintableERC721Created')
// Get the L2 ERC721 address from the emitted event and check it was created correctly
const l2ERC721Address = erc721CreatedEvent.args.localToken
const OptimismMintableERC721 = new Contract(
l2ERC721Address,
(await ethers.getContractFactory('OptimismMintableERC721')).interface,
signer
)
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 OptimismMintableERC721Factory.isOptimismMintableERC721(
OptimismMintableERC721.address
)
).to.equal(true)
})
it('should not be able to create a standard token with a 0 address for l1 token', async () => {
await expect(
OptimismMintableERC721Factory.createOptimismMintableERC721(
ethers.constants.AddressZero,
'L2ERC721',
'ERC'
)
).to.be.revertedWith(
'OptimismMintableERC721Factory: L1 token address cannot be address(0)'
)
})
})
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