4.6 ERC-721非同质通证标准及其严重缺陷

ERC-721非同质通证标准

ERC-721是一个自由的开放标准,描述了如何在以太坊区块链上构建不可互换或独特的通证。尽管现在大多数通证采用了ERC20通证标准,也就是说大多数通证都是可置换的(每个代币都与其他代币完全相同),对于主要需要货币属性的通证来说,ERC20标准是很好的选择。但是对于具有唯一性的物品,ERC20接口就力不从心了,例如同是一幅名画,真实作品和复制品价值是不一样的。所以就有了专门针对收藏品等的ERC721标准:每个ERC-721通证都是唯一的。

这就是说:ERC20通证是可以相互置换的,而ERC721通证则是不能置换的,亦即ERC721的每个通证是唯一的,且不可分割,它的最小单位就是1。

以下是该标准的核心规范:

pragma solidity ^0.4.20;

/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    /// @dev 当任何NFT的所有权更改时(不管哪种方式),就会触发此事件。
    /// 在创建(此时`from` == 0)和销毁NFT时(此时`to` == 0),也会会触发此事件。
    /// 创建合约时例外:此时可以创建和派发任意数量的NFT而无需触发此事件。
    /// 在进行任何转移时,该 NFT(如果有)的被授权地址将重置为无。
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    /// @dev 当更改或确认NFT的被授权地址时触发。
    ///  零地址表示没有被授权的地址。
    ///  触发 `Transfer` 事件时,该NFT的被授权地址(如果有)同样被重置为“无”(零地址)。
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    /// @dev 所有者启用或禁用操作员时触发。
    /// 操作员可管理所有者所持有的NFTs。
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    /// @notice 统计所有者持有的NFTs数量
    /// @dev NFT 不能分配给零地址,查询零地址同样会异常
    /// @param _owner : 查询余额的地址
    /// @return `_owner`所有的NFT数量,可以是0
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    /// @notice 返回所有者
    /// @dev NFT分配给零地址是无效的,查询零地址抛出异常
    /// @param _tokenId NFT的编号(id),也是其识别码。
    /// @return 返回所有者地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. 
    When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    /// @notice 将NFT的所有权从一个地址转移到另一个地址
    /// @dev 如果`msg.sender` 不是当前的所有者(或被授权者)则抛出异常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    /// 当转移完成时,函数检查  `_to` 是否为合约(代码数量 > 0),如果是,调用 `_to`的 `onERC721Received` 并且检查返回值是否是 `0x150b7a02` 
    /// (即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`)如果不是抛出异常。    /// @param _from :当前的所有者
    /// @param _from NFT当前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要转移的token id.
    /// @param data : 附加额外的参数(没有指定格式),传递给接收者。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @notice 将NFT的所有权从一个地址转移到另一个地址,功能同上,不带data参数。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @notice 转移所有权 -- 调用者负责确认`_to`是否能接收NFT,否则可能永久丢失。
    /// @dev 如果`msg.sender` 不是当前的所有者(或被授权操作员,或被授权地址)则抛出异常
    /// 如果 `_from` 不是当前的所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    /// @notice 更改或确认NFT的被授权地址
    /// @dev 零地址表示没有被授权的地址。
    ///  如果`msg.sender` 不是当前的所有者或被授权的操作员则抛出异常。
    /// @param _approved 新授权的控制者
    /// @param _tokenId : 授予的token id
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    /// @notice 启用或禁用授权第三方(操作员)管理 `msg.sender` 所有资产
    /// @dev 触发 ApprovalForAll 事件,合约必须允许每个所有者可以有多个操作员。
    /// @param _operator 要添加到授权操作员列表中的地址
    /// @param _approved True 表示操作员被授权, false 表示撤销授权
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    /// @notice 获取单个NFT的授权地址
    /// @dev 如果 `_tokenId` 无效,抛出异常。
    /// @param _tokenId :  token id
    /// @return 返回被授权地址, 零地址表示没有被授权地址。
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    /// @notice 查询一个地址是否是另一个地址的被授权操作员
    /// @param _owner 所有者
    /// @param _operator 代表所有者的被授权操作员
    /// @return 返回True则`_operator`是所有者的被授权操作员, 否则返回false。
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

最后一部分表明:每个符合ERC721的智能合约必须同时符合ERC721和ERC165。ERC165是智能合约定义自己支持哪些接口的一种方式。

除此之外,ERC-721 还提供了以下扩展接口。

钱包接口(wallet interface)

钱包等应用要接受NFT的安全转账,则必须实现如下接口。

interface ERC721TokenReceiver {
    /// @notice 处理接收NFT
    /// @dev ERC721智能合约在`transfer`完成后,在收币地址上调用这个函数。
    /// 函数可以复原或者拒绝转账。返回非`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);
}

元数据接口(metadata extension)

ERC-721引入了Metadata的概念,以供每个NFT描述自身的属性,供智能合约查询。对于NFT合约,这是可选的接口:

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice 合约中NFT集合的名字
    function name() external view returns (string _name);

    /// @notice 其缩写名称
    function symbol() external view returns (string _symbol);

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

里面提到的“ERC721 Metadata JSON Schema”则如下:

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

观点:由于NFT最早面向艺术品,ERC-721可选Metadata接口“ERC721 Metadata JSON Schema”只指定了name、description、image三个属性(且不可修改)。但对于游戏而言,这些属性远远不够,因此我们下一节要介绍的另一个NFT标准EIP-1155将name、description、image属性转移到了URI的json里面,而不规定URI接口返回的JSON Schema。

问题在于 tokenURI 可指向一个JSON 文件,现有绝大多数NFT产品就通过将其图片或视频的URL记录到这个JSON 文件中,而完成其产品——这也就是说,这些所谓的“非同质化通证”的图片或视频大多保存在私有服务器上。这显然是有问题的,因为大家用智能合约玩的居然是互联网产品!!

为了解决这个巨大的缺陷,我和道易程开发者一起,于2019年进行了摸索,并最终决定采用SVG格式图片链上存储的方式解决部分需求,并于2020年初推出对应的接口标准ERC-2569

枚举接口(enumeration extension)

枚举接口包含了按索引获取到对应的代币,可以提供NFTs的完整列表,以便NFT可被发现。

interface ERC721Enumerable {
    /// @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);}

一个通证标准并不容易,对吧?

ERC-721通证标准的第一个应用是Cryptokitties。中文名叫谜恋猫,又被币民称为加密猫。,

Cryptokitties的每一只猫对应于一个ERC-721通证。每一只猫都是不一样的,不同的毛色,不同的眼睛嘴巴鼻子,等等。而不同的遗传特征组合决定了这只猫的价格高低。你用一只普通的猫,来换我的名贵的猫,我会给你吗?当然不可能!所以ERC-721的通证,就是典型的“不可替换”的通证。每一个通证都是一个独立的个体,有着自己独特的价值。这样每一个都具有独特性和稀缺性,所以ERC-721最大的应用属性被认为是收藏。ERC-721在收藏品市场的应用,就是每个ERC-721对应罕见的独一无二的收藏品。

ERC-721非同质通证标准允许在智能合同中部署非同质通证(NFT)的标准API。该标准提供了跟踪和转让非同质通证的基本功能。我们很容易就联想到非同质通证由个人拥有和处理以及向第三方经纪人/钱包/拍卖商(运营商)委托的例子,非同质通证可以代表数字或实物资产的所有权(或债务)。我们也很容易就联想到ERC-721非同质通证标准适应的几种资产:

  • 有形资产——房产、独特的艺术品等等;

  • 虚拟收藏品——独特的小猫图片、收藏卡片、卡牌游戏中的卡牌、Decentraland中的土地和房产等等;

  • “负值”资产——贷款、债务和其它财务责任。

一般来说,所有的小猫都是独特的,没有两只小猫是一样的。非同质通证(NFT)恰好是可区分的,你能够独立跟踪每一只猫的所有权。

所以,ERC-721是允许在以太坊的任何非同质通证上应用钱包/经纪人/拍卖app的一种标准接口。它提供简单的ERC-721智能合约以及跟踪任意数量非同质通证的合约。可以说ERC-721定义了智能合约必须执行的最小接口,以便管理,拥有和交易独特的通证。它没有强制标记元数据的标准或限制添加补充功能。此标准受ERC-20通证标准的启发,并建立在EIP-20创建以来两年的经验基础之上。EIP-20的局限是不足以追踪非同质通证,因为每一个资产都是不同的(不可置换的),但一堆ERC20代币里的每一个都是相同的(可置换的)。

ERC721非同质通证标准定稿于18年3月13日,它从测试版中移出并转化为社区正式版v1规范,很快就得到来自整个加密生态系统的大量项目的支持和认可。最大的导火索是以该通证标准创新出基因工程智能合约的游戏“谜恋猫”,在ERC721非同质通证标准定稿之前,“谜恋猫”于2017年11月28日即登录以太坊区块链,并成功地引发市场热捧。

ERC721非同质通证标准的代表性应用为区块链“游戏”CryptoCelebrities、CryptoKitties(谜恋猫)、EtherTulips、CryptoPunks、Ethercraft、Decentraland、Etheremon(以太怪物)和Etherbots(以太机器人)等等。这些“游戏”可以说很低幼,但受追捧程度都很高。因为ERC721非同质通证标准为游戏开发者提供了三个新的构建模块:

首先,智能合约允许开发人员创建可公开验证的规则,用户在不受国界限制的情况下可以在全球各地互相连接,并且交易金是内置于协议中的。

其次,非同质通证(NFT)能够开发出可证明的集稀缺性、可编程性和抗审查三种特性于一体的数字商品。

其三,它使得虚拟角色、道具、勋章、物品可以跨服(服务器)、跨界(国界)、跨戏(游戏)。

这三个新的构建模块将使开发者能够扩展现有的游戏内核,或者创建新的游戏内核。会不会鸟枪变大炮,我们拭目以待!

ERC721非同质通证标准,又被翻译为不可置换代币标准。

相关技术文献

关联技术标准:

  1. ERC-20 Token Standard.

  2. ERC-165 Standard Interface Detection.

  3. ERC-173 Owned Standard.

  4. ERC-223 Token Standard.

  5. ERC-677 transferAndCall Token Standard.

  6. ERC-827 Token Standard.

使用了该技术标准的项目:

主要参考文献

最后更新于