ERC721

admin
admin 2021年08月07日
  • 在其它设备中阅读本文章

ERC721 是非同质化(Non-Fungible Token,简称 NFT 或 NFTs)代币标准,NFT 可以代表对数字或物理资产的所有权,NFT 是可区分的。

规范

每个符合 ERC721 的合同都必须实现 ERC721 和 ERC165 接口

pragma solidity ^0.8.0;

import "./IERC165.sol";

// @title ERC721非同质化代币标准
// Note: ERC165接口id为0x80ac58cd
interface IERC721 is IERC165 {
    /// @dev 当任何NFT的所有权更改时(不管哪种方式),就会触发此事件。
    ///  包括在创建时(`from` == 0)和销毁时(`to` == 0), 合约创建时除外。
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev 当更改或确认NFT的授权地址时触发。
    ///  零地址表示没有授权的地址。
    ///  发生 `Transfer` 事件时,同样表示该NFT的授权地址(如果有)被重置为“无”(零地址)。
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev 所有者启用或禁用操作员时触发。(操作员可管理所有者所持有的NFTs)
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice 统计所持有的NFTs数量
    /// @dev NFT 不能分配给零地址,查询零地址同样会异常
    /// @param _owner : 待查地址
    /// @return 返回数量,也许是0
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice 返回所有者
    /// @dev NFT 不能分配给零地址,查询零地址抛出异常
    /// @param _tokenId NFT 的id
    /// @return 返回所有者地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice 将NFT的所有权从一个地址转移到另一个地址
    /// @dev 如果`msg.sender` 不是当前的所有者(或授权者)抛出异常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    ///  当转移完成时,函数检查  `_to` 是否是合约,如果是,调用 `_to`的 `onERC721Received` 并且检查返回值是否是 `0x150b7a02` (即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`)  如果不是抛出异常。
    /// @param _from :当前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要转移的token id.
    /// @param data : 附加额外的参数(没有指定格式),传递给接收者。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice 将NFT的所有权从一个地址转移到另一个地址,功能同上,不带data参数。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 转移所有权 -- 调用者负责确认`_to`是否有能力接收NFTs,否则可能永久丢失。
    /// @dev 如果`msg.sender` 不是当前的所有者(或授权者、操作员)抛出异常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 更改或确认NFT的授权地址
    /// @dev 零地址表示没有授权的地址。
    ///  如果`msg.sender` 不是当前的所有者或操作员
    /// @param _approved 新授权的控制者
    /// @param _tokenId : token id
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice 启用或禁用第三方(操作员)管理 `msg.sender` 所有资产
    /// @dev 触发 ApprovalForAll 事件,合约必须允许每个所有者可以有多个操作员。
    /// @param _operator 要添加到授权操作员列表中的地址
    /// @param _approved True 表示授权, false 表示撤销
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice 获取单个NFT的授权地址
    /// @dev 如果 `_tokenId` 无效,抛出异常。
    /// @param _tokenId :  token id
    /// @return 返回授权地址, 零地址表示没有。
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice 查询一个地址是否是另一个地址的授权操作员
    /// @param _owner 所有者
    /// @param _operator 代表所有者的授权操作员
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
pragma solidity ^0.8.0;

interface IERC165 {
     /// @notice 是否合约实现了接口
    /// @param interfaceID  ERC-165定义的接口id
    /// @dev 函数要少于  30,000 gas.
    /// @return 合约实现了 `interfaceID`(不为  0xffffffff)返回`true` , 否则false.
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

如果合约(应用)要接受 NFT 的 安全转账,则必须实现以下接口。

pragma solidity ^0.8.0;

/// @dev 按 ERC-165 标准,接口id为 0x150b7a02
interface IERC721Receiver {
    /// @notice 处理接收NFT
    /// @dev ERC721智能合约在`transfer`完成后,在接收这地址上调用这个函数。
    /// 函数可以通过revert 拒绝接收。返回非`0x150b7a02` 也同样是拒绝接收。
    /// 注意: 调用这个函数的 msg.sender是ERC721的合约地址
    /// @param _operator :调用 `safeTransferFrom` 函数的地址。
    /// @param _from :之前的NFT拥有者
    /// @param _tokenId : NFT token id
    /// @param _data : 附加信息
    /// @return 正确处理时返回 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

以下元信息扩展是可选的,但是可以提供一些资产代表的信息以便查询。

pragma solidity ^0.8.0;

import "./IERC721.sol";

/// @title ERC-721非同质化代币标准, 可选元信息扩展
/// Note: 按ERC165标准,接口id为0x5b5e139f.
interface IERC721Metadata is IERC721 {
    /// @notice NFTs 集合的名字
    function name() external view returns (string _name);

    /// @notice NFTs 缩写代号
    function symbol() external view returns (string _symbol);

    /// @notice 一个给定资产的唯一的统一资源标识符(URI)
    /// @dev 如果 `_tokenId` 无效,抛出异常. URIs在 RFC 3986 定义,
    /// URI 也许指向一个 符合 "ERC721 元数据 JSON Schema" 的 JSON 文件
    function tokenURI(uint256 _tokenId) external view returns (string);
}

以下是 "ERC721 元数据 JSON Schema" 描述:

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "指示NFT代表什么"
        },
        "description": {
            "type": "string",
            "description": "描述NFT 代表的资产"
        },
        "image": {
            "type": "string",
            "description": "指向NFT表示资产的资源的URI(MIME 类型为 image/*) , 可以考虑宽度在320到1080像素之间,宽高比在1.91:1到4:5之间的图像。
        }
    }
}

以下枚举扩展信息是可选的,但是可以提供 NFT 的完整列表,以便 NFT 可被发现。

pragma solidity ^0.8.0;

import "./IERC721.sol";

/// @title ERC721非同质化代币标准枚举扩展信息
///  Note: 按ERC165标准,接口id为 0x780e9d63.
interface IERC721Enumerable is IERC721 {
     /// @notice  NFTs 计数
    /// @return  返回合约有效跟踪(所有者不为零地址)的 NFT数量
    function totalSupply() external view returns (uint256);

    /// @notice 枚举索引NFT
    /// @dev 如果 `_index` >= `totalSupply()` 则抛出异常
    /// @param _index 小于 `totalSupply()`的索引号
    /// @return 对应的token id(标准不指定排序方式)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice 枚举索引某个所有者的 NFTs
    /// @dev  如果 `_index` >= `balanceOf(_owner)` 或 `_owner` 是零地址,抛出异常
    /// @param _owner 查询的所有者地址
    /// @param _index 小于 `balanceOf(_owner)` 的索引号
    /// @return 对应的token id (标准不指定排序方式)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

NFT 身份 ID
每个 NFT 都由 ERC721 智能合约内部的唯一 ID 标识。该识别码在整个协议期内均不得更改。(合约地址, tokenId)对将成为以太坊链上特定资产的全球唯一且完全合格的标识符。尽管某些 ERC721 智能合约可能会方便地以 ID 为 0 起始并为每个新的 NFT 加 1,但调用者不得假设 ID 号具有任何特定的模式,并且必须将 ID 视为“黑匣子” 。另请注意,NFT 可能会变得无效(被销毁)。

由于 UUIDs 和 sha3 哈希可以直接转换为 uint256 ,因此使用 uint256 可实现更广泛的应用。

代币转移(转账)

ERC721 标准有两个转移函数 safeTransferFrom (重载了带 data 和不带 data 参数二种函数形式)及 transferFrom,转移可由一下角色发起:

  • NFT 的所有者
  • NFT 的被授权(approved)地址
  • NFT 当前所有者授权的(authorized)操作员
    此外, 授权的操作员也可以为 NFT 设置授权(approved)地址,这可以为钱包、经纪人和拍卖应用提供一套强有力的工具,方便快速使用大量的 NFT。

转移方法仅仅列出了特定条件下需要抛出异常的情况,我们自己的实现也可以在其他情况下抛出异常, 这可以实现一些有趣效果:

  • 如果合约暂定,可以禁用转账 — 如 CryptoKitties 合约(611 行)
  • 将接收 NFT 的某些地址列入黑名单 — 如 CryptoKitties 合约(565, 566 行)
  • 禁用非安全的转账 — 除非_to 为 msg.sender 或 countOf(_to)为非零或之前是非零(这些情况是安全的),否则 transferFrom 抛出异常。
  • 向交易双方收取费用 — 从零地址向非零地址授权(approve)可以要求支付费用,还原到(approve)零地址时退款;调用任何 transfer 时要求支付费用;要求 transfer 参数_to 等于 msg.sender ;要求 transfer 参数_to 是 NFT 授权(approved)的地址
  • 只读的 NFT 注册表 — 使用函数 unsafeTransfer, transferFrom, approve 以及 setApprovalForAll 时都抛出异常。