摘要
本标准允许在智能合约中实现非同质化代币(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的合约还必须遵守以下规定:
Solidity问题 #3412:上述接口包括每个函数的显式可变性保证。可变性保证按从弱到强的顺序是:payable、隐式nonpayable、view和pure。您的实现必须满足此接口中的可变性保证,并且可以满足更强的保证。例如,此接口中的payable函数可能在您的合约中实现为nonpayable(未指定状态可变性)。我们期望后续的Solidity版本将允许您的更严格合约从此接口继承,但对于0.4.20版本的解决方法是,您可以在从此接口继承之前编辑此接口以添加更严格的可变性
Solidity问题 #3419:实现ERC721Metadata或ERC721Enumerable的合约也应实现ERC721。ERC-721实现接口ERC-165的要求
Solidity问题 #2330:如果函数在此规范中显示为external,则合约使用public可见性将是兼容的。作为0.4.20版本的解决方法,您可以在继承到您的合约之前编辑此接口以切换到public
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.sender或countOf(_to)非零或先前非零(因为此类情况是安全的) - 向交易双方收费 — 当使用非零
_approved调用approve时(如果先前是零地址)要求付款,当使用零地址调用approve时(如果先前是非零地址)退款,调用任何转移函数时要求付款,要求转移参数_to等于msg.sender,要求转移参数_to是NFT的批准地址 - 只读NFT注册表 — 始终从
safeTransferFrom、transferFrom、approve和setApprovalForAll抛出异常
失败的交易将抛出异常,这是在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
元数据选择(元数据扩展)
我们在元数据扩展中要求name和symbol函数。我们审查的每个代币EIP和草案(ERC-20、ERC-223、ERC-677、ERC-777、ERC-827)都包含这些函数
我们提醒实现作者,如果您反对使用此机制,空字符串是对name和symbol的有效响应。我们还提醒大家,任何智能合约都可以使用与您合约相同的名称和符号。客户端如何确定哪些ERC-721智能合约是知名(规范)的,不在本标准范围内
提供了将NFT与URI关联的机制。我们期望许多实现将利用此功能为每个NFT提供元数据。图像大小建议来自Instagram,他们可能非常了解图像可用性。URI可能是可变的(即随时间变化)。我们考虑了代表房屋所有权的NFT,在这种情况下,有关房屋的元数据(图像、居住者等)自然会发生变化
元数据作为字符串值返回。目前这只能从web3调用中使用,不能从其他合约中使用。这是可以接受的,因为我们没有考虑区块链上应用程序查询此类信息的用例
考虑过的替代方案:将每个资产的所有元数据放在区块链上(太昂贵),使用URL模板查询元数据部分(URL模板不适用于所有URL方案,特别是P2P URL),multiaddr网络地址(不够成熟)
向后兼容性
我们采用了ERC-20规范中的balanceOf、totalSupply、name和symbol语义。如果其目标是更兼容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