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() 获取余额
函数原型
function balanceOf(address _owner) external view returns (uint256);
方法 获取 _owner 地址的余额, external 修饰符代表该方法仅能在合约外部访问。
2、 ownerOf() 获取token的所有者
函数原型
function ownerOf(uint256 _tokenId) external view returns (address);
方法 获取指定 _tokenId 的代币的所有者,仅合约外部能够访问。
因为ERC721代币,有自己的唯一标识,所以我们可以通过唯一标识去识别该代币的主人是谁,而ERC20是做不到这一点的,这也说明ERC721可以用来确权。
3、 transferFrom() 转移代币
函数原型
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
方法 从 _from 地址中转出 _tokenId 代币到 _to 地址中。
在ERC721中转移代币,必须标记你所要转移的代币的唯一标识ID。
4、 safeTransferFrom() 安全转移代币
函数原型
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
方法 从 _from 地址中转出 _tokenId 代币到 _to 地址中。
5、 approve() 授权代币操作权
函数原型
function approve(address _approved, uint256 _tokenId) external payable;
方法 授权批准 _approved 地址拥有 _tokenId 代币的运营权。
授权人必须是 _tokenId 代币的所有者或者已经被批准拥有该代币运营权的地址。
6、 setApprovalForAll() 设置代币操作权
函数原型
function setApprovalForAll(address _operator, bool _approved) external;
方法 授权批准加入或者移除 _operator 成为自己所有代币的运营者。合约必须支持代币存在多个运营者。
7、 getApproved() 获取被批准者
函数原型
function getApproved(uint256 _tokenId) external view returns (address);
方法 获取 _tokenId 代币的被授权运营者
8、 isApprovedForAll() 是否被授权
函数原型
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
方法 判断 _operator 是否被 _owner 授权为所有代币的运营者
B. 事件 Event
1、 Transfer() 转移事件
事件原型
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
事件 当进行代币的转移时,触发该事件。
2、 Approval() 授权事件
事件原型
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
事件 进行单token授权时,触发该事件。
3、 ApprovalForAll() 授权所有代币事件
事件原型
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
事件 setApprovalForAll方法调用时触发
0x03 ERC721标准接口
在ERC721代币合约实现时,必须实现ERC721接口以及ERC165接口,以下为接口:
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提供的代码示例,仅提供主要方法代码,完整版代码请通过附录获取:
-
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/
如果喜欢,别说话,扫我~
