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 {
......
This diff is collapsed.
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;
/**
* @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;
/* 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,
......
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