初探区块链数字加密资产标准ERC721
ERC721介绍
数字加密货币大致可以分为原生币(coin)和代币(token)两大类。前者如BTC、ETH等,拥有自己的区块链。后者如Tether、TRON、ONT等,依附于现有的区块链。市场上流通的基于以太坊的代币大都遵从ERC20协议。最近出现了一种被称为ERC721的数字加密资产,例如CryptoKitties。
ERC20可能是其中最广为人知的标准了。它诞生于2015年,到2017年9月被正式标准化。协议规定了具有可互换性(fungible)代币的一组基本接口,包括代币符号、发行量、转账、授权等。所谓可互换性(fungibility)指代币之间无差异,同等数量的两笔代币价值相等。交易所里流通的绝大部分代币都是可互换的,一单位的币无论在哪儿都价值一单位。
与之相对的则是非互换性(non-fungible)资产。比如CryptoKitties中的宠物猫就是典型的非互换性资产,因为每只猫各有千秋,而且由于不同辈分的稀缺性不同,市场价格也差异巨大。这种非标准化资产很长时间内都没有标准协议,直到2017年9月才出现ERC721提案,定义了一组常用的接口。ERC721至今仍旧处于草案阶段,但已经被不少dApp采用,甚至出现了专门的交易所。
ERC721标准
下面先给出ERC721标准的具体内容,后面会讲解。
每个符合ERC721标准的合约必须实现ERC721
和ERC165
接口。
pragma solidity ^0.4.20;
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// 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 Change or reaffirm the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// 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);
}
如果钱包程序接受安全转账,它必须实现钱包接口。
/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
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.
/// Note: 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);
}
ERC721接口解析
先来一波名词解释:
- NFT: 非互换性资产(non-fungible token)
- tokenId: NFT的id,类型为uint256
- owner: NFT的拥有者,类型为address
- balance: 用户拥有的NFT数量,类型为uint256
- approved: NFT的管理者,只有NFT的owner和approved可以对NFT进行操作,类型为address
- operator:operator拥有一个用户所有NFT的管理权限,类型为address
接下来解释下接口内的事件和方法:
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
当NFT被转移时会触发Transfer事件,在NFT被创建和销毁时也会触发此事件。在NFT被转移时,他的approved会被重置为零地址。
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
当NFT的管理者,也就是approved被改变的时候,会触发Approval事件。如果approved是零地址,说明NFT没有管理者。
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
当NFT的owner指定一个用户拥有他所有NFT的管理权限时,会触发ApprovalForAll事件。operator拥有owner所有NFT的管理权限。
function function balanceOf(address _owner) external view returns (uint256);
查询一个用户拥有的NFT数量。
function ownerOf(uint256 _tokenId) external view returns (address);
查询一个NFT的拥有者。
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
NFT的拥有者或管理者把一个NFT转移给别人,当to是一个合约的地址时,这个方法会调用onERC721Received方法。
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
NFT的拥有者或管理者把一个NFT转移给别人,这个方法有一个额外的data参数,上面那个safeTransferFrom方法会调用这个方法,然后把data置为""。
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
NFT的拥有者或管理者把一个NFT转移给别人,可以看到,这个方法比起上面两个要少了个safe,说明是不安全的转移,转移者要自己验证to是不是一个正确的地址,如果不是的话,NFT可能会永久丢失。
function approve(address _approved, uint256 _tokenId) external payable;
NFT的拥有者把NFT的管理权限授予一个用户。
function setApprovalForAll(address _operator, bool _approved) external;
NFT的拥有者授权一个用户是否拥有自己所有NFT的管理权限。合约必须允许一个用户有多个operator。
function getApproved(uint256 _tokenId) external view returns (address);
查询一个NFT的管理者
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
查询指定的operator是否拥有owner所有NFT的管理权限。
function supportsInterface(bytes4 interfaceID) external view returns (bool);
查询一个合约是否实现了一个接口。
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
这个方法处理NFT的转移,当一笔转移被发起时,这个方法可能会被调用,目的是拒绝这次转移。
ERC721的实现
Github上已经有ERC721的实现了,地址:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
不过这是solidity的实现,并且基于公链,如果要基于联盟链,可以看一下我的这篇文章基于Hyperledger Fabric实现ERC721
参考
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
https://zhuanlan.zhihu.com/p/112275276