diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index ffdc5cfc..1005bc92 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -12,7 +12,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2020-10-22 + toolchain: nightly-2022-01-21 override: true components: rustfmt, clippy - name: install-tarpaulin diff --git a/eth-contracts/contracts/NervosMirrorNftToken.sol b/eth-contracts/contracts/NervosMirrorNftToken.sol new file mode 100644 index 00000000..0f6ab0b1 --- /dev/null +++ b/eth-contracts/contracts/NervosMirrorNftToken.sol @@ -0,0 +1,31 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title NervosMirrorNftToken - A token contract used for bridged nft token from Nervos Chain +contract NervosMirrorNftToken is ERC1155, Ownable{ + address public factory; + string public classId; + + event CreatedNftToken(address tokenAddr, string classId, address factory); + + constructor(string memory uri_, string memory classId_) ERC1155(uri_) { + factory = msg.sender; + classId = classId_; + emit CreatedNftToken(address(this), classId, factory); + } + + function mint(address to, uint256 id, uint256 amount, bytes memory data) public onlyOwner { + _mint(to, id, amount, data); + } + + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public onlyOwner { + _mintBatch(to, ids, amounts, data); + } + + function burn(address account, uint256 id, uint256 value) public onlyOwner { + _burn(account, id, value); + } +} diff --git a/eth-contracts/contracts/NftClassManager.sol b/eth-contracts/contracts/NftClassManager.sol new file mode 100644 index 00000000..4c5cbdc2 --- /dev/null +++ b/eth-contracts/contracts/NftClassManager.sol @@ -0,0 +1,124 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./interfaces/IMirrorToken.sol"; +import "./interfaces/IMirrorNftToken.sol"; +import "./NervosMirrorNftToken.sol"; + +/// @title Asset Manager - The entry to manage all bridged tokens from Nervos chain +/// The owner of this contract should be transferred to the gnosis multi-sig wallet of Force Bridge committee when the +/// initializing is done. +/// All tokens recorded in this contract should be authorized to mint, and burn through this contract to make bridge action. +contract NftClassManager is Ownable, ReentrancyGuard { + string private uri_ = ""; + + struct ChainMetadata { + uint8 chain; // 0: CKB + uint8 chainId; + uint8 nftType; // 1: mnft 2: nrc721 3: cota + uint256 cellTxHash; + uint32 cellIndex; + bytes sourceOwner; + string classId; + address token; + } + + // classId is the identifier of the nft class cell on Nervos Chain + mapping(address => ChainMetadata) public tokenToChainMap; + mapping(string => ChainMetadata) public classIdToChainMap; + address[] public allTokens; + + event MintNft( + string indexed classId, + address indexed token, + address indexed to, + uint256 nftId, + bytes lockId + ); + + event BurnNft( + string indexed classId, + address indexed token, + address indexed from, + uint256 nftId, + uint256 fee, + bytes recipient, + bytes extraData + ); + + struct MintNftRecord { + string classId; // nft class id locked on Nervos chain + address to; // address of the receiver + uint256 nftId; // nft id on Nervos chain + bytes lockId; // used to identify the lock transaction on Nervos Chain + } + + function createClass2(ChainMetadata memory chain) internal returns(address tokenAddr){ + tokenToChainMap[chain.token] = chain; + classIdToChainMap[chain.classId] = chain; + bytes32 salt = keccak256(abi.encodePacked(chain.classId)); + NervosMirrorNftToken nervosMirrorNftToken = new NervosMirrorNftToken{salt: salt}(uri_, chain.classId); + tokenAddr = address(nervosMirrorNftToken); + chain.token = tokenAddr; + allTokens.push(tokenAddr); + tokenToChainMap[tokenAddr] = chain; + classIdToChainMap[chain.classId] = chain; + } + + /// @dev Mint an nft. + /// @param records The mint records. + /// This function should only be invoked by the gnosis multi-sig wallet of Force Bridge committee. + function mintNft(MintNftRecord[] memory records) public onlyOwner { + for (uint256 i = 0; i < records.length; i++) { + ChainMetadata memory chain = classIdToChainMap[records[i].classId]; + if (chain.token == address(0)) { + chain.token = createClass2(chain); + } else { + chain.token = classIdToChainMap[chain.classId].token; + } + require(chain.token != address(0), "nft class not found"); + IMirrorNftToken(chain.token).mint(records[i].to, records[i].nftId, 1, ""); + emit MintNft( + records[i].classId, + chain.token, + records[i].to, + records[i].nftId, + records[i].lockId + ); + } + } + + /// @dev Burn an nft and get the locked amount back on Nervos chain. + /// @param token The address of the token to burn. + /// @param nftId The nft id. + /// @param recipient The recipient on Nervos Chain. + /// @param extraData Extra data to be sent to the recipient. + function burnNft( + address token, + uint256 nftId, + bytes calldata recipient, + bytes calldata extraData + ) public payable nonReentrant { + ChainMetadata memory chain = tokenToChainMap[token]; + require(chain.chainId != 0, "chain not supported"); + require(chain.token != address(0), "token not supported"); + // pay the fee to bridge committee + if (msg.value > 0) { + (bool success, ) = payable(owner()).call{value: msg.value}(""); + require(success, "fee payment to bridge committee failed"); + } + // burn the nft + IMirrorNftToken(token).burn(_msgSender(), nftId, 1); + emit BurnNft( + chain.classId, + token, + _msgSender(), + nftId, + msg.value, + recipient, + extraData + ); + } +} diff --git a/eth-contracts/contracts/NftForceBridge.sol b/eth-contracts/contracts/NftForceBridge.sol new file mode 100644 index 00000000..3184dcf0 --- /dev/null +++ b/eth-contracts/contracts/NftForceBridge.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +pragma abicoder v2; + +import {IERC721} from "./interfaces/IERC721.sol"; +import {IERC1155} from "./interfaces/IERC1155.sol"; +import {Address} from "./libraries/Address.sol"; +import {MultisigUtils} from "./libraries/MultisigUtils.sol"; +import {SafeMath} from "./libraries/SafeMath.sol"; + +contract ForceBridge { + using Address for address; + // refer to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md + uint256 public constant SIGNATURE_SIZE = 65; + uint256 public constant VALIDATORS_SIZE_LIMIT = 50; + string public constant NAME_712 = "NFT Force Bridge"; + // if the number of verified signatures has reached `multisigThreshold_`, validators approve the tx + uint256 public multisigThreshold_; + address[] validators_; + + // UNLOCK_TYPEHASH = keccak256("unlock(UnlockRecord[] calldata records)"); + bytes32 public constant UNLOCK_TYPEHASH = + 0xf1c18f82536658c0cb1a208d4a52b9915dc9e75640ed0daf3a6be45d02ca5c9f; + // CHANGE_VALIDATORS_TYPEHASH = keccak256("changeValidators(address[] validators, uint256 multisigThreshold)"); + bytes32 public constant CHANGE_VALIDATORS_TYPEHASH = + 0xd2cedd075bf1780178b261ac9c9000261e7fd88d66f6309124bddf24f5d953f8; + + bytes32 private _CACHED_DOMAIN_SEPARATOR; + uint256 private _CACHED_CHAIN_ID; + bytes32 private _HASHED_NAME; + bytes32 private _HASHED_VERSION; + bytes32 private _TYPE_HASH; + + uint256 public latestUnlockNonce_; + uint256 public latestChangeValidatorsNonce_; + + event LockedNFT( + address indexed token, + address indexed sender, + uint256 lockedNftId, + bytes recipientLockscript, + bytes sudtExtraData + ); + + event UnlockedNFT( + address indexed token, + address indexed recipient, + address indexed sender, + uint256 receivedNftId, + bytes ckbTxHash + ); + + struct UnlockNFTRecord { + address token; + address recipient; + uint256 nftId; + bytes ckbTxHash; + } + + constructor(address[] memory validators, uint256 multisigThreshold) { + // set DOMAIN_SEPARATOR + // refer: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/24a0bc23cfe3fbc76f8f2510b78af1e948ae6651/contracts/utils/cryptography/draft-EIP712.sol + bytes32 hashedName = keccak256(bytes(NAME_712)); + bytes32 hashedVersion = keccak256(bytes("1")); + bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_CHAIN_ID = _getChainId(); + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); + _TYPE_HASH = typeHash; + + // set validators + require( + validators.length > 0, + "validators are none" + ); + require( + multisigThreshold > 0, + "invalid multisigThreshold" + ); + require( + validators.length <= VALIDATORS_SIZE_LIMIT, + "number of validators exceeds the limit" + ); + validators_ = validators; + require( + multisigThreshold <= validators.length, + "invalid multisigThreshold" + ); + multisigThreshold_ = multisigThreshold; + } + + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparator() internal view virtual returns (bytes32) { + if (_getChainId() == _CACHED_CHAIN_ID) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + } + } + + function _buildDomainSeparator(bytes32 typeHash, bytes32 name, bytes32 version) private view returns (bytes32) { + return keccak256( + abi.encode( + typeHash, + name, + version, + _getChainId(), + address(this) + ) + ); + } + + function _getChainId() private view returns (uint256 chainId) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + // solhint-disable-next-line no-inline-assembly + assembly { + chainId := chainid() + } + } + + function changeValidators( + address[] memory validators, + uint256 multisigThreshold, + uint256 nonce, + bytes memory signatures + ) public { + require(nonce == latestChangeValidatorsNonce_, "changeValidators nonce invalid"); + latestChangeValidatorsNonce_ = SafeMath.add(nonce, 1); + + require( + validators.length > 0, + "validators are none" + ); + require( + multisigThreshold > 0, + "invalid multisigThreshold" + ); + require( + validators.length <= VALIDATORS_SIZE_LIMIT, + "number of validators exceeds the limit" + ); + require( + multisigThreshold <= validators.length, + "invalid multisigThreshold" + ); + + for (uint256 i = 0; i < validators.length; i++) { + for (uint256 j = i + 1; j < validators.length; j ++) { + require( + validators[i] != validators[j], + "repeated validators" + ); + } + } + + bytes32 msgHash = + keccak256( + abi.encodePacked( + "\x19\x01", // solium-disable-line + _domainSeparator(), + keccak256( + abi.encode( + CHANGE_VALIDATORS_TYPEHASH, + validators, + multisigThreshold, + nonce + ) + ) + ) + ); + + validatorsApprove(msgHash, signatures, multisigThreshold_); + + validators_ = validators; + multisigThreshold_ = multisigThreshold; + } + + /** + * @notice if addr is not one of validators_, return validators_.length + * @return index of addr in validators_ + */ + function _getIndexOfValidators(address user) + internal + view + returns (uint256) + { + for (uint256 i = 0; i < validators_.length; i++) { + if (validators_[i] == user) { + return i; + } + } + return validators_.length; + } + + /** + * @notice @dev signatures are a multiple of 65 bytes and are densely packed. + * @param signatures The signatures bytes array + */ + function validatorsApprove( + bytes32 msgHash, + bytes memory signatures, + uint256 threshold + ) public view { + require(signatures.length % SIGNATURE_SIZE == 0, "invalid signatures"); + + // 1. check length of signature + uint256 length = signatures.length / SIGNATURE_SIZE; + require( + length >= threshold, + "length of signatures must greater than threshold" + ); + + // 3. check number of verified signatures >= threshold + uint256 verifiedNum = 0; + uint256 i = 0; + + uint8 v; + bytes32 r; + bytes32 s; + address recoveredAddress; + // set indexVisited[ index of recoveredAddress in validators_ ] = true + bool[] memory validatorIndexVisited = new bool[](validators_.length); + uint256 validatorIndex; + while (i < length) { + (v, r, s) = MultisigUtils.parseSignature(signatures, i); + i++; + + recoveredAddress = ecrecover(msgHash, v, r, s); + require(recoveredAddress != address(0), "invalid signature"); + + // get index of recoveredAddress in validators_ + validatorIndex = _getIndexOfValidators(recoveredAddress); + + // recoveredAddress is not validator or has been visited + if ( + validatorIndex >= validators_.length || + validatorIndexVisited[validatorIndex] + ) { + continue; + } + + // recoveredAddress verified + validatorIndexVisited[validatorIndex] = true; + verifiedNum++; + if (verifiedNum >= threshold) { + return; + } + } + require(verifiedNum >= threshold, "signatures not verified"); + } + + function unlockNFT(UnlockNFTRecord[] calldata records, uint256 nonce, bytes calldata signatures) + public + { + // check nonce hasn't been used + require(latestUnlockNonce_ == nonce, "unlock nonce invalid"); + latestUnlockNonce_ = SafeMath.add(nonce, 1); + + // 1. calc msgHash + bytes32 msgHash = + keccak256( + abi.encodePacked( + "\x19\x01", // solium-disable-line + _domainSeparator(), + keccak256(abi.encode(UNLOCK_TYPEHASH, records, nonce)) + ) + ); + + validatorsApprove(msgHash, signatures, multisigThreshold_); + + for (uint256 i = 0; i < records.length; i++) { + UnlockNFTRecord calldata r = records[i]; + if (r.token == address(0)) { + continue; + } else if (_isERC721(r.token)){ + IERC721(r.token).safeTransferFrom(address(this), r.recipient, r.nftId); + } else if (_isERC1155(r.token)){ + IERC1155(r.token).safeTransferFrom(address(this), r.recipient, r.nftId, 1, "0x00"); + } else { + continue; + } + emit UnlockedNFT( + r.token, + r.recipient, + msg.sender, + r.nftId, + r.ckbTxHash + ); + } + } + + // before lockNFT, user should approve -> TokenLocker Contract + function lockNFT( + address token, + uint256 nftId, + bytes memory recipientLockscript, + bytes memory sudtExtraData + ) public { + if (_isERC721(token)) { + IERC721(token).safeTransferFrom(msg.sender, address(this), nftId); + } else if (_isERC1155(token)) { + IERC1155(token).safeTransferFrom(msg.sender, address(this), nftId, 1, "0x00"); + } else { + return; + } + emit LockedNFT( + token, + msg.sender, + nftId, + recipientLockscript, + sudtExtraData + ); + } + + function _isERC721(address token) private view returns (bool){ + bytes4 _InterfaceId_ERC721 = 0x80ac58cd; + /* + * 0x80ac58cd === + * bytes4(keccak256('balanceOf(address)')) ^ + * bytes4(keccak256('ownerOf(uint256)')) ^ + * bytes4(keccak256('approve(address,uint256)')) ^ + * bytes4(keccak256('getApproved(uint256)')) ^ + * bytes4(keccak256('setApprovalForAll(address,bool)')) ^ + * bytes4(keccak256('isApprovedForAll(address,address)')) ^ + * bytes4(keccak256('transferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) + */ + return IERC721(token).supportsInterface(_InterfaceId_ERC721); + } + function _isERC1155(address token) private view returns (bool){ + bytes4 _InterfaceId_ERC1155 = 0xd9b67a26; + return IERC1155(token).supportsInterface(_InterfaceId_ERC1155); + } +} diff --git a/eth-contracts/contracts/interfaces/IERC1155.sol b/eth-contracts/contracts/interfaces/IERC1155.sol new file mode 100644 index 00000000..249add78 --- /dev/null +++ b/eth-contracts/contracts/interfaces/IERC1155.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.0 (token/ERC1155/IERC1155.sol) + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 is IERC165 { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/eth-contracts/contracts/interfaces/IERC165.sol b/eth-contracts/contracts/interfaces/IERC165.sol new file mode 100644 index 00000000..40cf6d29 --- /dev/null +++ b/eth-contracts/contracts/interfaces/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.0 (utils/introspection/IERC165.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/eth-contracts/contracts/interfaces/IERC721.sol b/eth-contracts/contracts/interfaces/IERC721.sol new file mode 100644 index 00000000..1b49fdde --- /dev/null +++ b/eth-contracts/contracts/interfaces/IERC721.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.0 (token/ERC721/IERC721.sol) + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; +} diff --git a/eth-contracts/contracts/interfaces/IMirrorNftToken.sol b/eth-contracts/contracts/interfaces/IMirrorNftToken.sol new file mode 100644 index 00000000..7b4da48f --- /dev/null +++ b/eth-contracts/contracts/interfaces/IMirrorNftToken.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IMirrorNftToken { + function mint(address to, uint256 id, uint256 amount, bytes memory data) external; + + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external; + + function burn(address account, uint256 id, uint256 value) external; +}