摘要

本标准允许在智能合约中实现非同质化代币(NFT)的标准API。该标准提供了追踪和转移NFT的基本功能

考虑了NFT由个人拥有和交易的情况,以及委托给第三方经纪人/钱包/拍卖商(“操作者”)的情况。NFT可以代表数字或物理资产的所有权。我们考虑了多样化的资产类型,并且相信您将设想出更多应用场景:

  • 物理资产 — 房屋、独特艺术品
  • 虚拟收藏品 — 独特的小猫图片、收藏卡牌
  • “负价值"资产 — 贷款、负担和其他责任

通常,所有房屋都是不同的,没有两只完全相同的小猫。NFT是可区分的,您必须单独跟踪每个NFT的所有权

动机

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

本标准受ERC-20代币标准启发,并建立在自EIP-20创建以来两年的经验基础上。EIP-20不足以追踪NFT,因为每个资产都是独特的(非同质化),而一定数量的代币每个都是相同的(同质化)

下面将检查本标准与EIP-20之间的差异


规范

本文件中使用的关键字"MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY"和"OPTIONAL"应按RFC 2119中的描述进行解释

每个ERC-721兼容合约必须实现ERC721和ERC165接口(受下面"注意事项"的约束):

pragma solidity ^0.4.20;

/// @title ERC-721 非同质化代币标准
/// @dev 参见 https://eips.ethereum.org/EIPS/eip-721
///  注意:此接口的ERC-165标识符是 0x80ac58cd
interface ERC721 /* 继承自 ERC165 */ {
    /// @dev 当任何NFT的所有权通过任何机制发生变化时触发此事件
    ///  创建NFT时(`from` == 0)和销毁NFT时(`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 _operator, bool _approved);

    /// @notice 计算分配给所有者的所有NFT数量
    /// @dev 分配给零地址的NFT被视为无效,此函数对零地址的查询会抛出异常
    /// @param _owner 要查询余额的地址
    /// @return `_owner`拥有的NFT数量,可能为零
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice 查找NFT的所有者
    /// @dev 分配给零地址的NFT被视为无效,对其查询会抛出异常
    /// @param _tokenId NFT的标识符
    /// @return NFT所有者的地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice 将NFT的所有权从一个地址转移到另一个地址
    /// @dev 除非`msg.sender`是当前所有者、授权操作者或此NFT的批准地址,否则抛出异常
    ///  如果`_from`不是当前所有者,则抛出异常。如果`_to`是零地址,则抛出异常
    ///  如果`_tokenId`不是有效的NFT,则抛出异常
    ///  转移完成后,此函数检查`_to`是否为智能合约(代码大小 > 0)
    ///  如果是,则调用`_to`上的`onERC721Received`,如果返回值不是
    ///  `bytes4(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 此函数与带有额外数据参数的其他函数功能相同,
    ///  只是此函数将数据设置为""
    /// @param _from NFT的当前所有者
    /// @param _to 新所有者
    /// @param _tokenId 要转移的NFT
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 转移NFT的所有权 — 调用者有责任
    ///  确认`_to`能够接收NFT,否则
    ///  NFT可能永久丢失
    /// @dev 除非`msg.sender`是当前所有者、授权操作者或此NFT的批准地址,否则抛出异常
    ///  如果`_from`不是当前所有者,则抛出异常。如果`_to`是零地址,则抛出异常
    ///  如果`_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中指定。此函数
    ///  使用少于30,000 gas
    /// @return 如果合约实现`interfaceID`且
    ///  `interfaceID`不是0xffffffff,则为`true`,否则为`false`
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

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

/// @dev 注意:此接口的ERC-165标识符是 0x150b7a02。
interface ERC721TokenReceiver {
    /// @notice 处理NFT的接收
    /// @dev ERC721智能合约在`transfer`之后在接收者上调用此函数。
    ///  此函数可能抛出异常来回滚并拒绝转移。
    ///  返回非魔法值的其他值必须导致交易被回滚。
    ///  注意:合约地址始终是消息发送者。
    /// @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
///  注意:此接口的ERC-165标识符是 0x5b5e139f。
interface ERC721Metadata /* 继承自 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在RFC 3986中定义。URI可能指向符合"ERC721
    ///  元数据JSON模式"的JSON文件
    function tokenURI(uint256 _tokenId) external view returns (string);
}

这是上面引用的"ERC721元数据JSON模式"

{
    "title": "资产元数据",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "标识此NFT代表的资产"
        },
        "description": {
            "type": "string",
            "description": "描述此NFT代表的资产"
        },
        "image": {
            "type": "string",
            "description": "指向表示此NFT代表资产的mime类型为image/*资源的URI。建议使图像宽度在320到1080像素之间,宽高比在1.91:1到4:5之间"
        }
    }
}

枚举扩展对ERC-721智能合约是可选的(参见下面的"注意事项")。这允许您的合约发布其完整的NFT列表并使它们可被发现

/// @title ERC-721 非同质化代币标准,可选枚举扩展
/// @dev 参见 https://eips.ethereum.org/EIPS/eip-721
///  注意:此接口的ERC-165标识符是 0x780e9d63。
interface ERC721Enumerable /* 继承自 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);
}

注意事项

0.4.20 Solidity接口语法不足以记录ERC-721标准。符合ERC-721的合约还必须遵守以下规定:

  1. Solidity问题 #3412:上述接口包括每个函数的显式可变性保证。可变性保证按从弱到强的顺序是:payable、隐式nonpayable、view和pure。您的实现必须满足此接口中的可变性保证,并且可以满足更强的保证。例如,此接口中的payable函数可能在您的合约中实现为nonpayable(未指定状态可变性)。我们期望后续的Solidity版本将允许您的更严格合约从此接口继承,但对于0.4.20版本的解决方法是,您可以在从此接口继承之前编辑此接口以添加更严格的可变性

  2. Solidity问题 #3419:实现ERC721Metadata或ERC721Enumerable的合约也应实现ERC721。ERC-721实现接口ERC-165的要求

  3. Solidity问题 #2330:如果函数在此规范中显示为external,则合约使用public可见性将是兼容的。作为0.4.20版本的解决方法,您可以在继承到您的合约之前编辑此接口以切换到public

  4. Solidity问题 #3494、#3544:使用this.*.selector被Solidity标记为警告,未来版本的Solidity不会将其标记为错误。

如果新版本的Solidity允许在代码中表达注意事项,则此EIP可能会更新并移除注意事项,这将等同于原始规范

设计原理

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

“NFT"词汇选择

“NFT"几乎让所有被调查者满意,并且广泛适用于各种可区分的数字资产。我们认识到"契约"对于此标准的某些应用(特别是物理财产)非常具有描述性

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

NFT标识符

每个NFT在ERC-721智能合约内部由唯一的uint256 ID标识。此标识数字在合约的生命周期内不得更改。然后,对(合约地址,uint256 tokenId)将成为以太坊链上特定资产的全局唯一且完全限定的标识符。虽然一些ERC-721智能合约可能发现从ID 0开始并简单地为每个新NFT递增1很方便,但调用者不得假设ID数字具有任何特定模式,并且必须将ID视为"黑盒”。另请注意,NFT可能变得无效(被销毁)。请参阅枚举函数以获取支持的枚举接口

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

转移机制

ERC-721标准化了安全转移函数safeTransferFrom(带和不带字节参数的重载版本)和不安全函数transferFrom。转移可以由以下方发起:

  • NFT的所有者
  • NFT的批准地址
  • 当前NFT所有者的授权操作者

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

转移和接受函数的文档仅指定交易必须抛出异常的条件。您的实现也可能在其他情况下抛出异常。这允许实现实现有趣的结果:

  • 如果合约暂停,则禁止转移 — 先前实践,CryptoKitties部署的合约,第611行
  • 阻止某些地址接收NFT — 先前实践,CryptoKitties部署的合约,第565、566行
  • 禁止不安全转移 — transferFrom抛出异常,除非_to等于msg.sendercountOf(_to)非零或先前非零(因为此类情况是安全的)
  • 向交易双方收费 — 当使用非零_approved调用approve时(如果先前是零地址)要求付款,当使用零地址调用approve时(如果先前是非零地址)退款,调用任何转移函数时要求付款,要求转移参数_to等于msg.sender,要求转移参数_to是NFT的批准地址
  • 只读NFT注册表 — 始终从safeTransferFromtransferFromapprovesetApprovalForAll抛出异常

失败的交易将抛出异常,这是在ERC-223、ERC-677、ERC-827和OpenZeppelin的SafeERC20.sol实现中确定的最佳实践。ERC-20定义了allowance功能,当调用后稍后修改为不同数量时会导致问题,如OpenZeppelin问题 #438中所述。在ERC-721中,没有allowance,因为每个NFT都是唯一的,数量为无或一个。因此,我们获得了ERC-20原始设计的优势,而没有后来发现的问题

NFT的创建(“铸造”)和销毁(“燃烧”)不包括在规范中。您的合约可以通过其他方式实现这些。请参阅事件文档以了解创建或销毁NFT时的责任

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

考虑过的替代方案:仅允许两步ERC-20样式交易,要求转移函数从不抛出异常,要求所有函数返回指示操作成功的布尔值

ERC-165接口

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

未来的EIP可能会创建合约接口的全局注册表。我们强烈支持此类EIP,它将允许您的ERC-721实现通过委托给单独的合约来实现ERC721Enumerable、ERC721Metadata或其他接口

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

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

我们已在测试网上部署了一个合约XXXXERC721,该合约实例化并跟踪340282366920938463463374607431768211456个不同的契约(2^128)。这足以将每个IPV6地址分配给以太坊账户所有者,或者跟踪大小为几微米的纳米机器人的所有权,总计达地球大小的一半。您可以从区块链查询它。每个函数使用的gas都比查询ENS少

此说明明确表明:ERC-721标准具有可扩展性

考虑过的替代方案:如果资产枚举函数需要for循环,则移除它;从枚举函数返回Solidity数组类型

隐私

动机部分中确定的钱包/经纪人/拍卖商强烈需要识别所有者拥有哪些NFT

考虑NFT不可枚举的用例可能很有趣,例如财产所有权的私人注册表或部分私有注册表。但是,无法实现隐私,因为攻击者可以简单地(!)为每个可能的tokenId调用ownerOf

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

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

我们提醒实现作者,如果您反对使用此机制,空字符串是对namesymbol的有效响应。我们还提醒大家,任何智能合约都可以使用与您合约相同的名称和符号。客户端如何确定哪些ERC-721智能合约是知名(规范)的,不在本标准范围内

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

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

考虑过的替代方案:将每个资产的所有元数据放在区块链上(太昂贵),使用URL模板查询元数据部分(URL模板不适用于所有URL方案,特别是P2P URL),multiaddr网络地址(不够成熟)

向后兼容性

我们采用了ERC-20规范中的balanceOftotalSupplynamesymbol语义。如果其目标是更兼容ERC-20同时支持此标准,则实现也可能包括返回uint8(0)decimals函数。但是,我们发现要求所有ERC-721实现支持decimals函数是人为的

截至2018年2月的NFT实现示例:

  • CryptoKitties — 兼容此标准的早期版本。
  • CryptoPunks — 部分兼容ERC-20,但不易推广,因为它直接在合约中包含拍卖功能,并使用明确将资产称为"punks"的函数名称。
  • Auctionhouse Asset Interface — 作者需要Auctionhouse ÐApp(当前被搁置)的通用接口。他的"Asset"合约非常简单,但缺少ERC-20兼容性、approve()功能和元数据。此工作在EIP-173的讨论中被引用

注意:Curio Cards和Rare Pepe等"限量版收藏代币"不是可区分资产。它们实际上是一组单独的可替代代币,每个代币由其自己的智能合约跟踪,具有自己的总供应量(在极端情况下可能为1)

onERC721Received函数专门解决了旧部署合约的问题,这些合约可能在某些情况下无意中返回1(true),即使它们没有实现函数(参见Solidity DelegateCallReturnValue错误)。通过返回和检查魔法值,我们能够区分实际的肯定响应与这些无意义的true