以太坊代币系列之非同质化代币ERC721

1,265 阅读10分钟

0x00 写在前面

在2013年7月,一款被称为MasterCoin的项目通过数字货币论坛Bitcointalk募集了超过5000个比特币,至此开启了区块链界的新的一轮资金众筹的方式。后来跃居数字货币市值榜第二的以太坊网络在主网上线后,推出了一个功能,发行代币。而这个功能将区块链界的资金募集推上了高峰。

为了更快速化的发行一款令牌,以太坊社区制定了一个令牌标准规范ERC20,直接将代币的发行时间降低到10分钟以内。

而最近火热的猫猫狗狗游戏,本质上也是一种基于以太坊网络的令牌,每一只猫或者狗都是一个以太坊上的令牌,而且这些令牌每一个都独一无二,无法互换。而这种令牌被社区极其看好,遂社区制定了一项关于非同质化令牌标准,这项标准就是我们今天要说的ERC721.

相比于ERC20,ERC721的想象空间就更大了,由于其独特的属性,每个令牌独一无二,不仅对于虚拟资产可以通证化,也可以对于现实生活中的车子房子进行通证化,当资产可以通证化,我们便能实现资产的上链。

0x01 ERC721标准制定动机

为了让任何人都能在以太坊上发行ERC721这种非同质化的代币,并且能够让钱包/拍卖应用/交易所等应用能够简单而且顺利的快速接入这类非同质化代币,以太坊社区制定了ERC721代币标准。

ERC721代币被称为非同质化代币,应为为 non-fungible tokens,简称NFT。

0x02 ERC721标准规范

在ERC721规范中,为了让代币实现非同质化,每个NFT都拥有一个唯一的ID标识,并且标识在智能合约内是不可变的,而这个NFT将可以实现标记现实中的某个资产,包括房子,车子,收藏品等。

在ERC20代币中,每个代币的转移都是同样的,并且代币可以进行拆分,最多支持小数点后18位,而在ERC721中每个代币无法拆分,一个代币就是一份资产,所以的转移交换都是单个代币的交换,因为映射到现实中,我们的房子车子都不能拆分成很多份。

我们在转移ERC20代币时,只需要标记我们转移的数量,不需要指定我到底转移的是我100个代币里面的哪一个,因为他们是同质代币,转哪一个的价值都是一样的。而在ERC721中,我们每次转移代币都需要指定所要转移的代币的ID,因为每一个代币都代表了不同的数字资产,或者映射在现实中的某个实体资产。

每一个实现ERC721标准代币的合约,都需要满足一下方法:

A. 方法 Method

1、 balanceOf() 获取余额

函数原型
  1. function balanceOf(address _owner) external view returns (uint256);

方法 获取  _owner 地址的余额,  external 修饰符代表该方法仅能在合约外部访问。

2、 ownerOf() 获取token的所有者

函数原型
  1. function ownerOf(uint256 _tokenId) external view returns (address);

方法 获取指定  _tokenId 的代币的所有者,仅合约外部能够访问。

因为ERC721代币,有自己的唯一标识,所以我们可以通过唯一标识去识别该代币的主人是谁,而ERC20是做不到这一点的,这也说明ERC721可以用来确权。

3、 transferFrom() 转移代币

函数原型
  1. function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

方法 从  _from 地址中转出  _tokenId 代币到  _to 地址中。

在ERC721中转移代币,必须标记你所要转移的代币的唯一标识ID。

4、 safeTransferFrom() 安全转移代币

函数原型
  1. function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

方法 从  _from 地址中转出  _tokenId 代币到  _to 地址中。

5、 approve() 授权代币操作权

函数原型
  1. function approve(address _approved, uint256 _tokenId) external payable;

方法 授权批准  _approved 地址拥有  _tokenId 代币的运营权。

授权人必须是 _tokenId 代币的所有者或者已经被批准拥有该代币运营权的地址。

6、 setApprovalForAll() 设置代币操作权

函数原型
  1. function setApprovalForAll(address _operator, bool _approved) external;

方法 授权批准加入或者移除  _operator 成为自己所有代币的运营者。合约必须支持代币存在多个运营者。

7、 getApproved() 获取被批准者

函数原型
  1. function getApproved(uint256 _tokenId) external view returns (address);

方法 获取  _tokenId 代币的被授权运营者

8、 isApprovedForAll() 是否被授权

函数原型
  1. function isApprovedForAll(address _owner, address _operator) external view returns (bool);

方法 判断  _operator 是否被  _owner 授权为所有代币的运营者

B. 事件 Event

1、 Transfer() 转移事件

事件原型
  1. event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

事件 当进行代币的转移时,触发该事件。

2、 Approval() 授权事件

事件原型
  1. event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

事件 进行单token授权时,触发该事件。

3、 ApprovalForAll() 授权所有代币事件

事件原型
  1. event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

事件  setApprovalForAll方法调用时触发

0x03 ERC721标准接口

在ERC721代币合约实现时,必须实现ERC721接口以及ERC165接口,以下为接口:

  1. pragma solidity ^0.4.20;

  2. interface ERC721 {

  3.    // 转移事件,记录转出,转入,以及被转移代币的ID

  4.    event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

  5.    // 授权,记录授权者,被授权者,被授权代币ID

  6.    event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

  7.    // 授权所有代币,记录授权者,被授权者

  8.    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

  9.    // 获取代币余额

  10.    // @params address _owner 代币所有者

  11.    // @return uint256 返回数量

  12.    function balanceOf(address _owner) external view returns (uint256);

  13.    // 获取nft代币所有者

  14.    // @params uint256 _tokenId 代币ID

  15.    // @return address 拥有者地址

  16.    function ownerOf(uint256 _tokenId) external view returns (address);

  17.    // 安全转移代币

  18.    // @params address _from 发送人地址

  19.    // @params address _to 收款人地址

  20.    // @params uint256 _tokenId 代币ID

  21.    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

  22.    // 转移代币

  23.    // @params address _from 发送人地址

  24.    // @params address _to 收款人地址

  25.    // @params uint256 _tokenId 代币ID

  26.    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

  27.    // 授权

  28.    // @params address _approved 被授权人

  29.    // @params uint256 _tokenId 代币ID

  30.    function approve(address _approved, uint256 _tokenId) external payable;

  31.    // 授权加入或者移除所有运营权

  32.    // @params address _approved 被授权人

  33.    // @params bool _approved 加入或删除

  34.    function setApprovalForAll(address _operator, bool _approved) external;

  35.    // 获取被授权人

  36.    // @params uint256 _tokenId 代币ID

  37.    // @return address 被授权地址

  38.    function getApproved(uint256 _tokenId) external view returns (address);

  39.    // 是否代币运营者

  40.    // @params address _owner 所有者

  41.    // @params address _operator 被授权者

  42.    // @return bool 是/否

  43.    function isApprovedForAll(address _owner, address _operator) external view returns (bool);

  44. }

  45. interface ERC165 {

  46.    // 查看是否支持该接口

  47.    // @params bytes4 interfaceID 接口ID

  48.    // 计算方式如:bytes4(keccak256('supportsInterface(bytes4)'))

  49.    function supportsInterface(bytes4 interfaceID) external view returns (bool);

  50. }

0x04 代码示例

以下为 OpenZeppelin提供的代码示例,仅提供主要方法代码,完整版代码请通过附录获取:

                                                                                                                                    
  1. pragma solidity ^0.4 .18 ;

  2. /**

  3. * @title ERC721 Non-Fungible Token Standard basic implementation

  4. * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md

  5. */

  6. contract ERC721BasicToken {

  7.   // 等于 `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`

  8.  bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;

  9.   // 定义mapping 记录token的所有者

  10.  mapping (uint256 => address ) internal tokenOwner ;

  11.   // 记录指定token被授权人的地址

  12.  mapping (uint256 => address ) internal tokenApprovals ;

  13.   // 记录指定地址拥有多少个token

  14.  mapping (address => uint256 ) internal ownedTokensCount ;

  15.   // 记录代币运营者

  16.  mapping (address => mapping (address => bool )) internal operatorApprovals ;

  17.   // 修饰符,是否是 `_tokenId` 的所有者

  18.  modifier onlyOwnerOf (uint256 _tokenId ) {

  19.    require (ownerOf (_tokenId ) == msg .sender );

  20.    _ ;

  21.   }

  22.   // 修饰符 是否可以对该代币进行转账

  23.  modifier canTransfer (uint256 _tokenId ) {

  24.    require (isApprovedOrOwner (msg .sender , _tokenId ));

  25.    _ ;

  26.   }

  27.   // 获取代币余额

  28.   function balanceOf (address _owner ) public view returns (uint256 ) {

  29.    require (_owner != address (0 ));

  30.     return ownedTokensCount [_owner ];

  31.   }

  32.   // 代币持有人获取

  33.   function ownerOf (uint256 _tokenId ) public view returns (address ) {

  34.    address owner = tokenOwner [_tokenId ];

  35.    require (owner != address (0 ));

  36.     return owner ;

  37.   }

  38.   // 某个nft代币是否真实存在

  39.   function exists (uint256 _tokenId ) public view returns (bool ) {

  40.    address owner = tokenOwner [_tokenId ];

  41.     return owner != address (0 );

  42.   }

  43.   // 授权代币运营权

  44.   function approve (address _to , uint256 _tokenId ) public {

  45.    address owner = ownerOf (_tokenId );

  46.    require (_to != owner );

  47.    require (msg .sender == owner || isApprovedForAll (owner , msg .sender ));

  48.     if (getApproved (_tokenId ) != address (0 ) || _to != address (0 )) {

  49.      tokenApprovals [_tokenId ] = _to ;

  50.       Approval( owner, _to, _tokenId);

  51.     }

  52.   }

  53.   // 获取运营权归属者

  54.   function getApproved (uint256 _tokenId ) public view returns (address ) {

  55.     return tokenApprovals [_tokenId ];

  56.   }

  57.   // 从运营列表中移除或者添加进列表

  58.   function setApprovalForAll (address _to , bool _approved ) public {

  59.    require (_to != msg .sender );

  60.    operatorApprovals [msg .sender ][_to ] = _approved ;

  61.     ApprovalForAll( msg. sender, _to, _approved);

  62.   }

  63.   // 判断是否是运营者

  64.   function isApprovedForAll (address _owner , address _operator ) public view returns (bool ) {

  65.     return operatorApprovals [_owner ][_operator ];

  66.   }

  67.   // 代币转移

  68.   function transferFrom (address _from , address _to , uint256 _tokenId ) public canTransfer (_tokenId ) {

  69.    require (_from != address (0 ));

  70.    require (_to != address (0 ));

  71.    clearApproval (_from , _tokenId );

  72.    removeTokenFrom (_from , _tokenId );

  73.    addTokenTo (_to , _tokenId );

  74.     Transfer( _from, _to, _tokenId);

  75.   }

  76.   // 安全转移

  77.   function safeTransferFrom (

  78.    address _from ,

  79.    address _to ,

  80.    uint256 _tokenId

  81.   )

  82.     public

  83.    canTransfer (_tokenId )

  84.   {

  85.    safeTransferFrom (_from , _to , _tokenId , "");

  86.   }

  87.   // 是否拥有代币的运营权

  88.   function isApprovedOrOwner (address _spender , uint256 _tokenId ) internal view returns (bool ) {

  89.    address owner = ownerOf (_tokenId );

  90.     return _spender == owner || getApproved (_tokenId ) == _spender || isApprovedForAll (owner , _spender );

  91.   }

  92.   // 清除运营权

  93.   function clearApproval (address _owner , uint256 _tokenId ) internal {

  94.    require (ownerOf (_tokenId ) == _owner );

  95.     if (tokenApprovals [_tokenId ] != address (0 )) {

  96.      tokenApprovals [_tokenId ] = address (0 );

  97.       Approval( _owner, address( 0), _tokenId);

  98.     }

  99.   }

  100.   // 添加一个代币

  101.   function addTokenTo (address _to , uint256 _tokenId ) internal {

  102.    require (tokenOwner [_tokenId ] == address (0 ));

  103.    tokenOwner [_tokenId ] = _to ;

  104.    ownedTokensCount [_to ] = ownedTokensCount [_to ].add (1 );

  105.   }

  106.   // 移除一个代币

  107.   function removeTokenFrom (address _from , uint256 _tokenId ) internal {

  108.    require (ownerOf (_tokenId ) == _from );

  109.    ownedTokensCount [_from ] = ownedTokensCount [_from ].sub (1 );

  110.    tokenOwner [_tokenId ] = address (0 );

  111.   }

  112.   // 是否是安全转移

  113.   // 能够接收ERC721代币的合约必须实现`onERC721Received`方法

  114.   // 通过判断是否存在该方法查看是否安全转移

  115.   function checkAndCallSafeTransfer (

  116.    address _from ,

  117.    address _to ,

  118.    uint256 _tokenId ,

  119.    bytes _data

  120.   )

  121.    internal

  122.    returns (bool )

  123.   {

  124.     if (!_to .isContract ()) {

  125.       return true;

  126.     }

  127.    bytes4 retval = ERC721Receiver (_to ).onERC721Received (_from , _tokenId , _data );

  128.     return (retval == ERC721_RECEIVED );

  129.   }

  130. }

0x05 附录

A. 修饰符

在 Solidity编程中,有一个概念叫做修饰符,英文是  modify ,当为一个方法添加了某个修饰符时,也就意味着这个方法必须满足这个修饰符所要求的事情,比如  external 关键词是要求该方法仅允许外部合约访问。

下面列出一些上面提到的修饰符:

external 仅允许外部合约调用该方法

payable 仅标记了该关键词的方法能够接收转账操作

B. 引用

ERC721草案

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md

OpenZeppelin对于ERC721完整实现

https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/

如果喜欢,别说话,扫我~