一、什么是ERC721
ERC721是一个开放的、用来描述以太坊上建立非同质或者唯一代币的标准(协议),NFT就是non-fungible tokens的首字母缩写。
当前色大多数代币(ERC20代币)都是同质的,这意味着这种代币的每一个token都是相同的,而ERC721每一个代币都是唯一的。
二、标准定义
ERC-721标准定义约定了一个智能合约必须实现的最小接口,它包括代币管理、持有和交易功能。然而它并不包括代币元数据的相关内容,也缺少对一些实用的功能支持。
请记住,这里提到的是最小接口,这就意味着它只有基本功能。然而在实际运用中,你不能仅依赖于这些接口,你必须增加一些额外的功能来支持代币应用。
官方网址: http://erc721.org/
这里贴出标准定义:
pragma solidity ^0.4.20;
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
/// 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.
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.
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.
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
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
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`
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
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
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
/// @notice Set or reaffirm the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// @dev 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
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
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
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
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);
}
interface ERC721TokenReceiver {
/// @notice Handle the receipt of an NFT
/// @dev The ERC721 smart contract calls this function on the
/// recipient after a `transfer`. This function MAY throw to revert and reject the transfer. Return
/// of other than the magic value MUST result in the transaction being reverted.
/// @notice The contract address is always the message sender.
/// @param _operator The address which called `safeTransferFrom` function
/// @param _from The address which previously owned the token
/// @param _tokenId The NFT identifier which is being transferred
/// @param _data Additional data with no specified format
/// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
/// unless throwing
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}
三、标准详情
ERC-721标准其实可以分为三个接口,ERC721接口、ERC165接口、ERC721TokenReceiver接口。
3.1、 IERC721
IERC721接口定义了三个事件,分别是交易事件、授权事件和全部授权事件。事件是用来使轻客户端更方便的追踪以太坊状态的改变。
IERC721接口定义了9个函数。分别应用于用户余额、代币所有者、安全交易、普通交易、授权、全部授权、查询授权、查询是否全部授权。
这里详细解释一下安全交易。安全交易是基于接收到代币后必须作出一个响应(执行某些代码)的思想。如果没有响应,则认为代币交易未成功。当然,因为只有合约才会执行代码,所以安全交易只对接收者是合约有效。安全交易函数提供了一个参数data
,代表传递给接收合约的数据。无信息传递时,data
也可以为空。
它有一个同名函数,显然另一个是重载函数,重载函数和原函数的区别在于它并不提供参数data,从注释中可以看到它其实就相当于另一个函数的data
为空。
除了有安全交易函数,还有普通交易函数,这就意味着ERC721并不强制使用安全交易。
3.2、 IERC165
IERC165接口约定了合约支持的接口,比如支持ERC721。每个支持的接口都有一个对应的bytes4值与之相对应,这个是固定的,是常量(有一个计算方式)。
3.3、 IERC721TokenReceiver
它定义了合约接收代币后执行的函数,其中一个参数就是IERC721安全交易中的data
。
四、一些附加功能
前面提到过,ERC721标准只是最小接口,直接拿来实际运用还存在很多问题,比如没有增发功能(用户没有获取代币来源)等。需求不同,有时可能还需要燃烧功能。为了能在应用端正确显示用户所拥有的NFT代币,还需要可列举功能。因此这里存在许多附加标准(功能),可以根据自己需求增加并组合。
4.1、 燃烧功能 IERC721Burnable
IERC721Burnable,燃烧其实就是代币从用户账户中减去,并将代币的owner设置为空地址
4.2、可列举功能 IERC721Enumerable
IERC721Enumerable 可列举增加的三个函数分别为:代币发行总量、按索引获取用户代币和按总索引获取代币。但是具体实现时有一些细节点:
- 构造器,在构造器中增加可列举接口支持,参阅上面提到的IRC165.
- 每个地址都对有一个整数数组来存储它的token,这一点使用Vyper实现不了,因为Vyper没有动态数组。
- 因为上述第2点,所以需要修改增发、交易和燃烧方法来改变那个动态数组的内容。
4.3、 元数据 IERC721Metadata
IERC721Metadata增加了名称、符号和代币uri函数。但是具体实现时有一些细节点:
- 构造器中增加元数据接口支持
- 在燃烧时要清除对应的元数据
4.4、 其它接口 mintable 和 pausable
其它功能还有增发和暂停等。增发就是增加一个角色控制外部增发接口(或者某种条件下自动增发),暂停也是一样,只是增加一个外部暂停接口(或者某种条件自动触发暂停)。
五、常用组合
一般常用的组合为ERC721标准+ IERC721Enumerable + IERC721Metadata,再加上自定义的mintable。
六 、示例合约
所有的这一切你都不用自己写,有人写出了示例合约并做成了模块,使用时只需要导入就行了。例如:
import 'openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol';
实用技巧,你需要再导入一个 MinterRole 就可以自定义增发实现了。如果你想发行的代币序号从1开始排列的话,直接使用 ERC721Enumerable 中的代币数组长度计数就好,不需要重新做一个变量计数。如果想有其它的实现方式,需要自己实现。
七、依赖库安装
导入模块(依赖库)中的示例合约需要提前安装对应的库,比如在truffle工程中使用上面的库,需要执行:npm install @openzeppelin/contracts
。