目录
一、前言
二、代币
1、普通代币
2、以太坊中的代币
3、代币为什么重要
4、ERC-20
5、ERC-721
6、实战1
1.要求
2.代码
7、实战2- balanceOf 和 ownerOf
1.要求
2.代码
三、重构
1、讲解
2、实战1
1.要求
2.代码
四、ERC-721: 转移标准
1、讲解
2、实战1
1.要求
2.代码
3、实战2-transfer
1.要求
2.代码
看了一些区块链的教程,论文,在网上刚刚找到了一个项目实战,CryptoZombies。
从这节课开始,我们就开启的新的篇章,这节课是原项目实战的第五课,接下来,让我们一起来学习新的知识吧。
如果你想了解更多有关于机器学习、深度学习、区块链、计算机视觉等相关技术的内容,想与更多大佬一起沟通,那就扫描下方二维码加入我们吧!
如果大家对区块链或者以太坊有比较多的了解,你一定听说过代币,如果你研究过相关的论文,你一定见到过代币。那什么是代币,代币都能用来干嘛呢?
如果不考虑区块链,单纯的来考虑代币,我想,大家能够有个比较直观的印象,如果你能看到我的这篇博客,那你一定能够听说过或者见到过某种类型的代币,不管是在电视上,还是书籍里,还是生活中。
如果你看过跟跟赌有关的电影,有些电影里面玩家双方用一些道具代替纸币,这些道具就是筹码,如果你听说过抓娃娃机,或者玩过抓娃娃机,你能知道,很多时候,我们需要先买一些圆圆的像硬币一样的东西。如果你坐过地铁,有时候,会给你一个地铁卡,有的是像银行卡一样的,有的是像钱币一样圆圆的。
如上这些,都在一定程度上算是代币,所以我们也能基本理解代币,就是说能够代替钱币让我们进行一些操作的东西,我们可以通过这些代换取物品,享受服务,但是只能用于固定用途,比如,抓娃娃机的圆片不能用于地铁。所以他的适用范围是受限制的。
所以,代币的定义如下:
代币,是一种形状及尺寸类似货币,但限制使用范围、不具通货效力的物品。代币通常需要以金钱换取,用在商店、游乐场、大众运输工具等地方,做为凭证以使用服务、换取物品等。代币的材质以金属或塑胶为主。
上面说的代币是普通的代币,那以太坊中的代币是什么呢?
以太坊中的代币是加密货币的一种,英文是token,也属于上面提到的代币的范畴,这个代币只能在以太坊平台中使用,用来在以太坊世界中进行各种交易。
以太坊中的代币主要有如下几种:
1.应用代币:保证区块链以太坊的运作,用于激励矿工,创建区块。例如BTC,ETC。应用代币一般采用POW(工作量证明)机制。
2.权益代币:类似公司股权,这是以太坊中新应用的一类代币,权益代币会让其持有者进行收益的分红。区块链黄金交易平台 Digix 通过发行 DGD 的权益代币进行应用众筹。权益代币一般使用POS(权益证明)机制。
3.债权代币:用来解决区块链应用流动性不足的问题。当某个应用突然拥入大量新用户而没有代币来使用应用。为避免其应用代币的价格剧烈波动的原因,又禁止老用户大量抛售自己手中的应用代币。这个时候,这个应用就需要临时借一笔钱,来购买应用代币,以满足新用户的使用需求。这就是债权代币。它类似于为应用提供一个短期的贷款。而对于债权代币的持有者则类似一种储蓄行为,因为一般都可以获得一定的利息回报。
但是我们都知道,以太坊中,都是各种各样的合约,所以从形式上来看:
代币在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数。
举个例子:
transfer(address _to, uint256 _value)
balanceOf(address _owner).
在智能合约内部,通常用下面的映射来追踪每个地址有多少余额:
mapping(address => uint256) balances
我们以上面提到的ERC-20为例,由于所有 ERC-20 代币共享具有相同名称的同一组函数,所以它们都可以以相同的方式进行交互。也就是说我们构建的应用程序能够与一个 ERC-20 代币进行交互,也代表他能与任何 ERC-20 代币进行交互。 这样我们就可以比较轻松地添加更多的代币,我们只需插入新代币的地址,我们的应用就可以使用了。
举个关于交易所的例子,当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。
ERC-20 是一个 以太坊区块链上的智能和约的一种协议标准。其中ERC是Ethereum Request for Comments的缩写。
ERC-20是可替代型代币(同质代币)的标准 API,相当于最基本的代币,包括转账和余额跟踪的功能。
除了ERC-20,还有很多代币标准,比如:ERC-223,ERC-721,ERC-777,ERC-809等等。后续我会专门写一篇文章来讲述代币,也会详细讲解所有的代币标准。
ERC-20 是以太坊中最常用的代币标准,对于货币来说,交易过程是可分的,比如我们可以转移0.5以太。
ERC-721和ERC-20很大的一个区别就在于,ERC-721是不可互换,不能分割的,我们只能以整个单位进行交易,并且每个单位都有唯一的ID。
我们的僵尸是独立不可分割的,比如,我们不能生成0.5个僵尸,所以在我们这里,我们要使用的是ERC-721代币。
在使用之前,我们要先看一下ERC-721代币长什么样:
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
注:ERC721目前是一个草稿,还没有正式商定的实现。在本教程中,我们使用的是 OpenZeppelin 库中的当前版本,但在未来正式发布之前它可能会有更改。 所以把这 一个 可能的实现当作考虑,但不要把它作为 ERC-721 代币的官方标准。
当我们要再添加一个继承,我们之前已经继承过了,那我们怎么实现多个继承呢?我们看如下的示例:
contract SatoshiNakamoto is NickSzabo, HalFinney {
}
1.在文件顶部声明我们
pragma
的版本。
2.
将zombieattack.sol
和erc721.sol
import
进来。3.声明一个继承
ZombieAttack
和ERC721
的新合约, 命名为ZombieOwnership
。合约的其他部分先留空。
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
// Import file here
import "./erc721.sol";
// Declare ERC721 inheritance here
contract ZombieOwnership is ZombieAttack, ERC721 {
}
在实战之前,我们先看一下这两个函数的定义方式:
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
对于上面的函数,我们只需要一个传入 address
参数,然后返回这个 address
拥有多少代币。对于下面的函数,我们需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address
。
1.实现
balanceOf
来返回_owner
拥有的僵尸数量。2.实现
ownerOf
来返回拥有 ID 为_tokenId
僵尸的所有者的地址。
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) external view returns (uint256) {
// 1. Return the number of zombies `_owner` has here
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
// 2. Return the owner of `_tokenId` here
return zombieToOwner[_tokenId];
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}
上面的代码是有些问题的,如果我们编译,会提示我们:
你不能有相同名称的修饰符和函数。
这是因为:
我们定义了一个叫
ownerOf
的函数。之前我们在zombiefeeding.sol
里以ownerOf
命名创建了一个modifier修饰符。
但是我们不能将这个函数改为别的名称,为什么呢?
因为在ERC-721中,是这样定义的,我们要使用这个标准,就要按照这种方式进行函数定义,所以,我们只能修改我们的修饰符。
回到了 zombiefeeding.sol
, 我们将把 modifier
的名称从 ownerOf
改成 onlyOwnerOf
。
1.把修饰符定义中的名称改成
onlyOwnerOf
。
2.
往下滑到使用此修饰符的函数feedAndMultiply
。我们也需要改这里的名称。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
// 1. Change modifier name to `onlyOwnerOf`
modifier onlyOwnerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
// 2. Change modifier name here as well
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
我们可能需要把我们的东西给别人,比如进行交易,这个时候,我们就要转移我们的所有权。在ERC-721中,有两种不同的方法来实现转移代币:
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
上面的代码是有些问题的,如果我们编译,会提示我们:
1.第一种方法是代币的拥有者调用
transfer
方法,传入他想转移到的address
和他想转移的代币的_tokenId
。2.第二种方法是代币拥有者首先调用
approve
,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个mapping (uint256 => address)
里。然后,当有人调用takeOwnership
时,合约会检查msg.sender
是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
transfer
和 takeOwnership
都将包含相同的转移逻辑,只是以相反的顺序。:
第一种方法:代币的发送者调用函数
第二种方法:代币的接收者调用函数
我们把这个逻辑抽象成它自己的私有函数 _transfer
,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。
1.定义一个名为
_transfer
的函数。它会需要3个参数:address _from
、address _to
和uint256 _tokenId
。它应该是一个私有函数。
2.
有2个映射会在所有权改变的时候改变:
(1)ownerZombieCount:
记录一个所有者有多少只僵
(2)zombieToOwner
:记录什么人拥有什么改变方式如下:
(1)
为接收僵尸的人(address _to
)增加ownerZombieCount
。使用++
来增加。
(2)
为发送僵尸的人(address _from
)减少ownerZombieCount
。使用--
来扣减。(3)改变
_tokenId
的zombieToOwner
映射指向_to
。3.ERC-721规范包含了一个
Transfer
事件。这个函数的最后一行应该用正确的参数触发Transfer
——查看erc721.sol
看它期望传入的参数并在这里实现。
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
// Define _transfer() here
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}
1.我们想确保只有代币或僵尸的所有者可以转移它。定义一个名为的映射
zombieApprovals
。它应将映射uint
到address
。这样,当不是所有者的某人使用_tokenId
呼叫transferFrom
时,我们可以使用此映射快速查找他是否被许可使用该代币。
2.
为transferFrom添加一个require声明。该声明应该确保只有拥有者owner 或 核验后的代币/僵尸的地址才能转移它。其方法如下:
(1)zombieToOwner
for_tokenId
is equal tomsg.sender
或
(2)zombieApprovals
for_tokenId
is equal tomsg.sender
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
// 1. Define mapping here
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
// 2. Add the require statement here
require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
// 3. Call _transfer
_transfer(_from, _to, _tokenId);
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}