以太坊令牌系列之非同质化令牌ERC721

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
pragma solidity ^0.4.20;

interface ERC721 {

// 转移事件,记录转出,转入,以及被转移代币的ID
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

// 授权,记录授权者,被授权者,被授权代币ID
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

// 授权所有代币,记录授权者,被授权者
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

// 获取代币余额
// @params address _owner 代币所有者
// @return uint256 返回数量
function balanceOf(address _owner) external view returns (uint256);

// 获取nft代币所有者
// @params uint256 _tokenId 代币ID
// @return address 拥有者地址
function ownerOf(uint256 _tokenId) external view returns (address);

// 安全转移代币
// @params address _from 发送人地址
// @params address _to 收款人地址
// @params uint256 _tokenId 代币ID
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

// 转移代币
// @params address _from 发送人地址
// @params address _to 收款人地址
// @params uint256 _tokenId 代币ID
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

// 授权
// @params address _approved 被授权人
// @params uint256 _tokenId 代币ID
function approve(address _approved, uint256 _tokenId) external payable;

// 授权加入或者移除所有运营权
// @params address _approved 被授权人
// @params bool _approved 加入或删除
function setApprovalForAll(address _operator, bool _approved) external;

// 获取被授权人
// @params uint256 _tokenId 代币ID
// @return address 被授权地址
function getApproved(uint256 _tokenId) external view returns (address);

// 是否代币运营者
// @params address _owner 所有者
// @params address _operator 被授权者
// @return bool 是/否
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
// 查看是否支持该接口
// @params bytes4 interfaceID 接口ID
// 计算方式如:bytes4(keccak256('supportsInterface(bytes4)'))
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

0x04 代码示例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
pragma solidity ^0.4.18;

/**
* @title ERC721 Non-Fungible Token Standard basic implementation
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721BasicToken {

// 等于 `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;

// 定义mapping 记录token的所有者
mapping (uint256 => address) internal tokenOwner;

// 记录指定token被授权人的地址
mapping (uint256 => address) internal tokenApprovals;

// 记录指定地址拥有多少个token
mapping (address => uint256) internal ownedTokensCount;

// 记录代币运营者
mapping (address => mapping (address => bool)) internal operatorApprovals;

// 修饰符,是否是 `_tokenId` 的所有者
modifier onlyOwnerOf(uint256 _tokenId) {
require(ownerOf(_tokenId) == msg.sender);
_;
}

// 修饰符 是否可以对该代币进行转账
modifier canTransfer(uint256 _tokenId) {
require(isApprovedOrOwner(msg.sender, _tokenId));
_;
}

// 获取代币余额
function balanceOf(address _owner) public view returns (uint256) {
require(_owner != address(0));
return ownedTokensCount[_owner];
}

// 代币持有人获取
function ownerOf(uint256 _tokenId) public view returns (address) {
address owner = tokenOwner[_tokenId];
require(owner != address(0));
return owner;
}

// 某个nft代币是否真实存在
function exists(uint256 _tokenId) public view returns (bool) {
address owner = tokenOwner[_tokenId];
return owner != address(0);
}

// 授权代币运营权
function approve(address _to, uint256 _tokenId) public {
address owner = ownerOf(_tokenId);
require(_to != owner);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender));

if (getApproved(_tokenId) != address(0) || _to != address(0)) {
tokenApprovals[_tokenId] = _to;
Approval(owner, _to, _tokenId);
}
}

// 获取运营权归属者
function getApproved(uint256 _tokenId) public view returns (address) {
return tokenApprovals[_tokenId];
}

// 从运营列表中移除或者添加进列表
function setApprovalForAll(address _to, bool _approved) public {
require(_to != msg.sender);
operatorApprovals[msg.sender][_to] = _approved;
ApprovalForAll(msg.sender, _to, _approved);
}

// 判断是否是运营者
function isApprovedForAll(address _owner, address _operator) public view returns (bool) {
return operatorApprovals[_owner][_operator];
}

// 代币转移
function transferFrom(address _from, address _to, uint256 _tokenId) public canTransfer(_tokenId) {
require(_from != address(0));
require(_to != address(0));

clearApproval(_from, _tokenId);
removeTokenFrom(_from, _tokenId);
addTokenTo(_to, _tokenId);

Transfer(_from, _to, _tokenId);
}

// 安全转移
function safeTransferFrom(
address _from,
address _to,
uint256 _tokenId
)
public
canTransfer(_tokenId)
{
safeTransferFrom(_from, _to, _tokenId, "");
}

// 是否拥有代币的运营权
function isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) {
address owner = ownerOf(_tokenId);
return _spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender);
}

// 清除运营权
function clearApproval(address _owner, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _owner);
if (tokenApprovals[_tokenId] != address(0)) {
tokenApprovals[_tokenId] = address(0);
Approval(_owner, address(0), _tokenId);
}
}

// 添加一个代币
function addTokenTo(address _to, uint256 _tokenId) internal {
require(tokenOwner[_tokenId] == address(0));
tokenOwner[_tokenId] = _to;
ownedTokensCount[_to] = ownedTokensCount[_to].add(1);
}

// 移除一个代币
function removeTokenFrom(address _from, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _from);
ownedTokensCount[_from] = ownedTokensCount[_from].sub(1);
tokenOwner[_tokenId] = address(0);
}

// 是否是安全转移
// 能够接收ERC721代币的合约必须实现`onERC721Received`方法
// 通过判断是否存在该方法查看是否安全转移
function checkAndCallSafeTransfer(
address _from,
address _to,
uint256 _tokenId,
bytes _data
)
internal
returns (bool)
{
if (!_to.isContract()) {
return true;
}
bytes4 retval = ERC721Receiver(_to).onERC721Received(_from, _tokenId, _data);
return (retval == ERC721_RECEIVED);
}
}

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/

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

qrcode_for_gh_0f12fe5ef5fd_258.jpg