ERC721-非同质化代币标准

简要说明:不可替代代币(NFT:non-fungible tokens)的标准接口。

原文章:https://eips.ethereum.org/EIPS/eip-721

摘要

以下标准允许在智能合约中实施 NFT 标准 API该标准提供了跟踪和传输 NFT 的基本功能。

该提案考虑了个人拥有和交易 NFT 以及委托给第三方经纪人/钱包/拍卖商(“运营商”)的用例。NFT 可以代表数字或实物资产的所有权。该提案考虑了各种各样的资产,如:

  • 有形财产:房屋、独特的艺术品
  • 虚拟收藏品:独特的小猫图片、收藏卡
  • “负价值”资产——贷款、负担和其他责任

一般来说,所有的房子都是不同的,没有任何两只小猫是相同的。NFT 是可区分的,必须单独跟踪每一项 NFT 的所有权。

动机

标准接口允许钱包/经纪人/拍卖应用程序与以太坊上的任何 NFT 配合使用。我们提供简单的 ERC-721 智能合约以及跟踪任意大量 NFT 的合约。下面讨论其他应用。

该标准受到 ERC-20 代币标准的启发,并建立在 EIP-20 创建以来两年的经验基础上。EIP-20 不足以跟踪 NFT,因为每种资产都是不同的(不可替代),而每个数量的代币都是相同的(可替代)

下面探讨了该标准与 EIP-20 之间的差异。

规格

每个符合 ERC-721 的合约都必须实现 ERC-721 和 ERC-165 接口:

pragma solidity ^0.4.20;

/// @title ERC-721 不可替代代币标准
/// @dev 参见 https://eips.ethereum.org/EIPs/eip-721
/// Note: 此接口的 ERC-165 标识符是 0x80ac58cd
interface ERC721 { // is ERC165
	/// @dev 当任何 NFT 的所有权通过任何机制发生变化时,就会触发此事件
	/// 当 NFT 创建(`from` == 0)和销毁(`to` == 0)时发出此事件
	/// 例如在合约创建期间,可以创建和分配任意数量的 NFT,而无需发出 Transfer。
	/// 在任何转账时,该 NFT 的批准地址(如果有)将重置为 无
	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 当为所有者启用或禁止操作员时,触发该事件
	/// 运营者可以管理所有者的所有 NFT。
	event ApprovalForAll(address indexed  _owner, address indexed _approved, uint256 indexed _tokenId);
	
	/// @notice 计算分配给所有者的所有 NFT
	/// @dev NFT 分给零地址被认为是无效的,并且该函数会抛出有关零地址的查询
	/// @param 要查询余额的地址
	/// @return 拥有 NFT 的数量,可能为 0
	function balanceOf(address _owner) external view returns(uint256);
	
	/// @notice 查找 NFT 的所有者
	/// @dev NFT 分配到零地址被认为是无效的,并且关于他们的查询确实会抛出
	/// @param _tokensId NFT 的标识符
	/// @return NFT 所有者的地址
	function ownerOf(uint256 _tokenId) external view returns(address);
	
	/// @notice 将 NFT 所有权从一个地址转移到另一个地址
	/// @dev 抛出异常,除非`msg.sender`是当前所有者、授权操作者或批准的地址
	/// 如果 `_from` 不是当前所有者,则抛出异常,如果`_to`是零地址,则抛出异常
	/// 传输完毕后,此函数检查“_to”是否是智能合约(code > 0)
	/// 如果是,他会在`_to`上调用`onERC721Received`
	/// 如果返回值不是`byte4(keccak256("onERC721Received(address, address, uint256, bytes)"))`,则会抛出异常
	/// @param _from NFT 的当前所有者
	/// @param _to 新的所有者
	/// @param _tokenId 要传输的 NFT
	/// @param data 没有指定格式的附加数据,在对`_to`的调用中发送
	function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
	
	/// @notice 将 NFT 的所有权从一个地址转移到另一个地址
	/// @dev 这与带有额外数据参数的另一函数的工作方式相同,知识该函数将 data 设置为""
	/// @param _from NFT 的当前所有者
	/// @param _to 新所有者
	/// @param _tokenId 要传输的 NFT
	function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
	
	/// @notice 转移 NFT 的所有权——调用者有责任确认`_to`能够接受 NFTs,否则它们可能会永久丢失
	/// @dev 抛出异常,除非`msg.sender`是当前所有者,一个授权的运营商,或该 NFT 的批准地址。如果 `_tokenId` 不是有效的 NFT,则抛出异常
	/// @param _from NFT的当前所有者
	/// @param _to 新所有者
	/// @param _tokenId 要传输的 NFT
	function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
	
	/// @notice 更改或重新确认 NFT 的批准地址
	/// @dev 零地址表示没有批准的地址
	/// 除非 `msg.sender` 是当前 NFT 所有者或或当前所有者的授权操作员,否则抛出异常
	/// @param _approved 新批准的 NFT 控制着
	/// @param _tokenId 要批准的 NFT
	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`不是有效的 NFT,则抛出异常
	/// @param _tokenId 用于查找批准地址的 NFT
	/// @return 返回该 NFT 的批准地址,如果没有则为零地址
	function getApproved(uint256 _tokenId) external view returns(address);
	
	/// @notice 查询一个地址是否是另一个地址的授权操作者
	/// @param _owner 拥有 NFT 的地址
	/// @param _operator 代表所有者的地址
	/// @return 如果`_operator`是`_owner`的批准操作员,则返回 true,否则返回 false
	function isApprovedForAll(address _owner, address _operator) external view returns(bool);
}

interface ERC165{
	/// @notice 查询合约是否实现了接口
	/// @param interfaceID 接口标识符,如 ERC-165 中指定的
	/// @dev 接口标识在 ERC-165 中指定,这个功能使用的 gas 少于 30,000
	/// @return 如果合约实现了`interfaceID`,则返回 `true` 并且 `interfaceID` 不为 0xffffffff,否则为 `false`
	function supportsInterface(bytes4 interfaceID) external view returns(bool);
}

如果钱包/经纪人/拍卖应用程序接受安全转账,则必须实现钱包接口。

/// @dev 注意:此接口的 ERC-165 标识符为 0x150b7a02
interface ERC721TokenReceiver{
	/// @notice 处理 NFT 的接收
	/// @dev ERC721 智能合约在“转账”后在接受者上调用此函数。该函数可能会抛出异常以恢复被拒绝传输,返回魔术变量意外的值必须导致交易回滚
	/// Note: 合约地址始终是消息发送者
	/// @param _operator 调用 `safeTransferFrom` 函数的地址
	/// @param _from 之前拥有该代币的地址
	/// @param _tokenId 正在传输的 NFT 标识符
	/// @param _data 没有指定格式的附加数据
	/// @return `bytes4(keccak256('onERC721Received(address, address, uint256, bytes)'))` 除非抛出
	function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

对于 ERC-721 智能合约,元数据拓展是可选的(参考下面的“注意事项”)。这允许访问智能合约的名称以及 NFT 所代表的资产的详细信息。

/// @title ERC-721 不可替代代币标准,可选元数据拓展
/// @dev 参考:https://eips.ethereum.org/EIPS/eip-721
/// Note:该接口的 ERC-165 标识符为 0x5b5e139f
interface ERC721 Metadata { // is ERC721
	/// @notice 本合约中 NFT 集合的描述性名称
	function name() external view returns (string _name);
	
	/// @notice 本合约中 NFT 的缩写名称
	function symbol() external view returns (string _symbol);
	
	/// @notice 给定资产的独特统一资源标识符(URI)
	/// @dev 如果`_tokenId` 不是有效的 NFT,则抛出异常。URI 在 RFC3986 中定义.
	/// URI 可以指向符合“ERC721 元数据 JSON 架构”的 JSON 文件。
	function tokenURI(uint256 _tokenId) external view returns (string);
}

这是上面引用的“ERC721 元数据 JSON 架构”

{
    "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 which 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."
	    }
	}
}

对于 ERC-721 合约,枚举拓展是可选的(参考下面的”注意事项”)。这允许合约发布其完整的 NFT 列表并使其可以被发现

/// @title ERC-721 不可替代代币标准,可选枚举拓展
/// @dev 参考:https://eips.ethereum.org/EIPS/eip-721
/// 注意:该接口的 ERC-165 标识符是 0x780e9d63
interface ERC721Enumerable  { //is ERC721
	/// @notice 计数该合约跟踪的 NFT
	/// @return 该合约跟踪的有效 NFT 计数,其中每个 NFT 都有一个指定且可查询的所有者,不等于零地址。
	function totalSupply() external view returns(uint256);
	
	/// @notice 枚举有效的 NFT
	/// @dev 如果`_index` >= `totalSupply()` 则抛出异常
	/// @param _index 小于`totalSupply()` 的计数器
	/// @return 返回第`_index`个 NFT 的代币标识符(未指定排序顺序)
	function tokenByIndex(uint256 _index) external view returns (uint256);
	
	/// @notice 美剧分配给所有者的 NFT
	/// @dev 如果 `_index` >= `balanceOf(_owner)` 或者如果`_owner`是零地址,则抛出,表示无效的 NFT
	/// @param _owner 我们对它们拥有的 NFT 感兴趣的地址
	/// @param _index 小于 `balanceOf(_owner)` 的计数器
	/// @return 分配给“_owner”的第“_index”个 NFT 的代币标识符(未指定排序顺序)
	function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns(uint256);
}

注意事项

Solidity 0.4.20 版本的接口语法表达能力不足以记录 ERC-721 标准。每个符号 ERC-721 的合约还必须遵守以下规定:

  • Solidity issue #3412:上述接口包含每个函数的显性可变性保证。可变性保证从弱到强依次为:`payable,implicit nonpayable(隐式不可支付)、viewpure。你的实现必须满足此接口中的可变性保证,并且你可能会满足更强的保证。例如,此接口中的 payable 函数可以在你的合约中实现为 nonpayable(未指定状态可变性)。我们预计更高版本的 Solidity 版本将允许你从该接口继承更严格的合约,但 0.4.20 版本的解决方案是你可以在从合约继承之前编辑此接口以添加更严格的可变性。
  • Solidity issue #3419:实现 ERC721MetadataERC721Enumerable 的合约也应实现 ERC721。ERC-721 实现了ERC-165 接口的要求。
  • Solidity issue #2330:如果一个函数在本规范中显示为external,那么如果它使用 public ,那么合约是合规的。作为 0.4.20 的解决方法,可以编辑此接口以在继承合约之前切换到公共接口
  • Solidity issue #3494、#3544:使用this.*.selector 被 Solidity 标记为警告,Solidity 的未来版本不会将其标记为错误

如果新版本的 Solidity 允许用代码表示警告,则可以更新此 EIP 并删除警告,这将等同于原始规范。

基本原理

以太坊智能合约有许多议题的用途都依赖于跟踪可区分资产。现在或计划中的 NFT 的例子包括 Decentraland 中的 LAND、CryptoPunks 中的同名朋克,以及使用 DMarket 或 EnjinCoin 等系统的游戏内物品。未来的用途包括跟踪现实世界的资产,如房地产(正如 Ubiquity 或 Propyl 等公司所设想的那样)。在每种情况下,至关重要的是,这些项目不能作为分类帐本中的数字“集中在一起”,而是每项资产必须单独且原子地跟踪其所有权。无论这些资产的性质如何,如果我们拥有一个允许跨职能资产管理和销售平台的标准化接口,那么生态系统将会更加强大。

“NFT”选词

“NFT” 几乎让所有受访者感到满意,并且广泛适用于广泛的可区分数字资产,我们认识到“契约”对于本标准的某些应用(特别是物理属性)具有很强的描述性。

考虑的替代方案:可区分资产、所有权、代币、资产、股票、票据

NFT 标识符(NFT Identifiers)

每个 NFT 都由 ERC-721 合约中唯一uint256ID 进行标识。该识别号码在合约有效期内不得更改。该Paircontract address, uint256 tokenId)将成为以太坊链上特定资产的全球唯一且完全合格的标识符。虽然某些 ERC-721 智能合约可能会发现从 ID 0 开始并为每一个新 NFT 简单地加一很方便,但调用者不应假设 ID 号具有任何特殊模式。并且必须将 ID 视为“黑匣子”。另外注意,NFT 可能会变得无效(被销毁)。请参阅枚举函数以了解支持的枚举接口。

uint256 的选择允许各种各样的应用程序,因为 UUID 和 sha3 哈希可以直接转换为uint256

转移机制

ERC-721 标准化了安全传输函数 safeTransferFrom(带不带字节参数重载)和不安全函数 transferFrom。转账可以通过以下方式发起:

  • NFT 的所有者
  • NFT 的批准地址
  • NFT 当前所有者的授权运营商

此外,授权商可以为 NFT 设置批准的地址。这为钱包、经纪商和拍卖应用程序提供了一套强大的工具来快速使用大量 NFT。

传输和接受函数的文档仅指定事件必须抛出的条件。你的实现可能会引发其他情况。这使得实现能够获得有趣的结果:

  • 如果合约已暂停,则禁止传输——现有技术,CryptoKitties 部署的合约,第 611 行

  • 在黑名单中阻止某些地址接收 NFT——先前的 CryptoKitties 部署的合约,第 565、566 行

  • 禁止不安全的传输——除非_to等于 msg.sendercountOf(_to)非零或之前非零(因为这种情况是安全的),否则 transferFrom 会抛出异常

  • 对交易的双方收取费用——调用 approve 时要求支付一个非零的 _approved(如果之前是零地址),如果调用 approve_approved 是零地址并且之前是非零地址,则退款支付,调用任何转移函数时要求支付,要求转移参数 _to 等于 msg.sender,要求转移参数 _to 是 NFT 的批准地址

  • 只读 NFT 注册表——总是从safeTransferFromtransferFromapprovesetApprovalForAll中抛出异常

失败的交易会抛出异常,这是 ERC-223、ERC-677、ERC-827 和 OpenZeppelin 的 SafeERC20.sol 中识别出的最佳做法。ERC-20定义了一个授权功能,当调用后稍后将其修改为不不同金额是,会造成问题,就像在 OpenZeppelin 的问题 #438 中。在 ERC-721 中,没有授权,因为每个 NFT 都是唯一的,数字是 0 或者 1,因此我们得到了 ERC-20 最初设计的好处,而没有后来发现的问题

NFT 的创建(“铸造”)和销毁(“燃烧”)不包括在规范中。您的合约可以通过其他方式实现这些功能。请参考有关创建和销毁 NFT 时您的责任event文档

我们质疑 onERC721Received 上的 operator 参数是否有必要,在我们能想象的所有情况中,如果操作员很重要,那么操作员可以将代币转移到自己名下,然后发送它,然后他们将成为 from 地址,这似乎时牵强的,因为我们认为操作员时代币的临时所有者(将其转移到自己时多余的)。当操作员发送代币时,它是操作员根据自己的意愿行事,而不是代币持有人的代理人行事。这就是为什么操作员和先前的代币所有者对于代币接收者都很重要的原因。

考虑的替代方法:只允许两步 ERC-20 风格交易,要求转账功能从不引发异常,要求所有功能返回指示操作成功的布尔值。

ERC-165 接口

我们选择了标准接口检查(ERC-165)来公开 ERC-721 智能合约支持的接口。

未来的 EIP 可能会创建一个全球合约接口的注册表。我们强烈支持这样的 EIP,他将允许您的 ERC-721 实现通过委托到另一个合约来实现 ERC721EnumerableERC721Metadata或其他接口。

Gas 和复杂性(关于枚举拓展)

该规范考虑了管理少量和任意大量 NFT 的实现。如果您的应用程序能够增长,请避免在您的代码中使用 for/while 循环(参见 CryptoKitties 赏金问题 #4)。这些循环表明您的合约可能无法拓展,并且 Gas 成本将随着时间的推移而无限上升。

我们已经部署了一个名为 XXXXERC721 的合约到测试网络,他实例化并跟踪了340282366920938463463374607431768211456个不同的契约(2^128)

。这足以将每个 IPV6 地址分配给以太坊账户的所有者,或者根据尺寸只有几微米的纳米机器人的所有权,总计占地球大小的一般。您可以从区块链中查询它。而且每个功能所需的燃气比查询 ENS 所需的燃气费要少。

这个例子清楚地说明了:ERC-721 标准时可拓展的。

考虑的替代方法:如果需要 for 循环,则删除资产枚举功能,或者从枚举功能返回一个 Solidity 数组类型。

隐私

在动机部分中确定的钱包/经纪人/拍卖商对于确定所有者拥有的 NFT 具有强烈的需求。

考虑到 NFT 不能被列举的用例可能会很有趣,比如私人的资产所有权注册表,或者部分私人的注册表。然而,隐私无法实现,因为攻击者可以简单地调用 ownerOf 以获取每个可能的 tokenId。

元数据选择(元数据拓展)

我们在元数据拓展中要求了名称和符号函数,我们审查的每个代币 EIP 和草案(ERC-20、ERC-223、ERC-677、ERC-777、ERC-827)都包含了这些函数。

我们提醒实现作者,如果你对此机制的使用标识抗议,空字符串是名称和符号的有效响应。我们还要提醒大家,任何合约都可以使用与您的合约相同的名称和符号。客户端如何确定哪些 ERC-721 智能合约是规范的,超出了此标准的范围。

提供了一种将 NFT 与 URI 关联的机制。我们预计许多实现将利用此功能为每个 NFT 提供元数据。图像大小的建议取自 Instagram ,他们可能对图像可用性了解甚深。URI 可能是可变的(即它会随着时间变化)。我们考虑了一个代表房屋所有权的 NFT,这种情况下关于房屋的元数据(图像、居住者等)自然会发生变化。

元数据以字符串值返回。目前,这只能从 web3 中调用,而不能从其他合约中调用。这是可以接受的,因为我们还没有考虑在区块链上应用程序会查询此类信息的用例。

考虑的替代方案:将每个资产的所有数据存储在区块链上(成本过高),使用 URL 模板来查询元数据部分(URL 模板不能与所有 URL 方案一起使用,特别是 P2P URL),多地址网络地址(成熟度还不够)

社区共识

在最初的ERC-721问题上进行了大量讨论,此外,我们在Gitter上举行了第一次现场会议,得到了良好的代表性和广泛宣传(在Reddit上,在Gitter的#ERC频道上以及最初的ERC-721问题中)。感谢以下参与者:

  • @ImAllInNow Rob from DEC Gaming / 在密歇根以太坊聚会上介绍于2月7日
  • @Arachnid Nick Johnson
  • @jadhavajay AyanWorks的Ajay Jadhav
  • @superphly Cody Marx Bailey - XRAM Capital / 在1月20日的黑客马拉松中分享/联合国金融未来黑客马拉松。
  • @fulldecent William Entriken 在ETHDenver 2018年举行了第二次活动,讨论可以区分的资产标准(笔记将被发布)。

在这个过程中,我们一直非常包容,并邀请任何有疑问或贡献的人参与我们的讨论。然而,此标准仅编写以支持在此处列出的已确定的用例。

向后兼容性

我们采用了来自 ERC-20 规范的balanceOftotalSupplynamesymbol语义。实现也可以包括一个返回uint8(0)的函数decimals,如果其目标是更兼容 ERC-20 并支持此标准的话。然而,我们认为要求所有 ERC-721 实现都支持decimals函数是牵强附会的。

截至2018年2月,以下是一些示例NFT实现:

  • CryptoKitties - 与早期版本的此标准兼容。
  • CryptoPunks - 部分兼容 ERC-20,但不容易通用化,因为它在合约中直接包含拍卖功能,并使用将资产明确称为“punks”的函数名称。
  • Auctionhouse Asset Interfacec 拍卖资产接口 - 作者需要一个拍卖房屋 ÐApp 的通用接口(目前搁置)。他的“资产”合约非常简单,但缺少 ERC-20 兼容性,approve()功能和元数据。在 EIP-173 的讨论中引用了这一努力。

注意:像 Curio Cards 和 Rare Pepe 这样的“限量版、可收藏的代币”并不是可以区分的资产。实际上,它们是由许多个可互换的代币组成的集合,每个代币都由其自己的智能合约跟踪,并拥有自己的总供应量(在极端情况下可能为`1)。

onERC721Received函数专门解决了可能在某些情况下即使没有实现函数也会不经意地返回1(true)的旧部署合约问题(请参阅 Solidity 的 DelegateCallReturnValue 漏洞)。通过返回并检查一个魔术值,我们能够区分实际的肯定响应和这些无效的真值。

测试用例

0xcert ERC-721 Token包括使用Truffle编写的测试用例。

实现示例

  1. 0xcert ERC721 – 一个参考实现

    • 采用 MIT 许可,您可以自由地在您的项目中使用它
    • 包括测试用例
    • 有积极的漏洞赏金计划,如果您发现错误,您将会得到报酬
  2. Su Squares – 一个广告平台,您可以在其中租用空间并放置图片

    • 参与 Su Squares 漏洞赏金计划,以寻找与此标准或其实现相关的问题
    • 实现了完整的标准和所有可选接口
  3. ERC721ExampleDeed – 一个示例实现

    • 使用 OpenZeppelin 项目格式实现
  4. XXXXERC721,由 William Entriken 提供 – 一个可扩展的示例实现

    • 部署在 Testnet 上,实例化并跟踪了 340282366920938463463374607431768211456 个不同的权利凭证(2^128)
    • 能够支持所有通过元数据扩展进行的查询。这证明了扩展性不是一个问题。

References (参考资料):

标准 (Standards):

问题 (Issues):

讨论 (Discussions):

NFT 实现和其他项目 (NFT Implementations and Other Projects):