ERC721
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 时都抛出异常。