以太坊上的代币

如果你对以太坊的世界有一些了解,你很可能听过人们聊到代币——尤其是 ERC20 代币
一个代币在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner)

在智能合约内部,通常有一个映射,mapping(address => uint256) balances,用于追踪每个地址还有多少余额
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数

由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互
这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互
这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了

其他代币标准

对于像货币一样的代币来说,ERC20 代币非常酷。 但是要在我们僵尸游戏中代表僵尸就并不是特别有用
首先,僵尸不像货币可以分割 —— 我可以发给你 0.237 以太,但是转移给你 0.237 的僵尸听起来就有些搞笑。
其次,并不是所有僵尸都是平等的。 你的 2 级僵尸”Steve”完全不能等同于我 732 级的僵尸”H4XF13LD MORRIS 💯💯😎💯💯”。(你差得远呢,Steve)???我的僵尸叫 yichen,我的僵尸 7 级了,谢谢

有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为 ERC721 代币
ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这些特性正好让我们的僵尸可以用来交易
请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处

我们将把所有 ERC721 逻辑存储在一个叫 ZombieOwnership 的合约中

  • 在文件顶部声明我们 pragma 的版本(格式参考之前的课程)
  • 将 zombieattack.sol import 进来
  • 声明一个继承 ZombieAttack 的新合约, 命名为 ZombieOwnership。合约的其他部分先留空
1
2
3
4
5
pragma solidity ^0.4.19;
import "./zombieattack.sol";
contract ZombieOwnership is ZombieAttack {

}

ERC721 标准, 多重继承

让我们来看一看 ERC721 标准:

1
2
3
4
5
6
7
8
9
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 库中的当前版本,但在未来正式发布之前它可能会有更改。 所以把这 一个 可能的实现当作考虑,但不要把它作为 ERC721 代币的官方标准

实现一个代币合约

在实现一个代币合约的时候,我们首先要做的是将接口复制到它自己的 Solidity 文件并导入它,import “./erc721.sol”;。 接着,让我们的合约继承它,然后我们用一个函数定义来重写每个方法
但等一下—— ZombieOwnership 已经继承自 ZombieAttack 了 —— 它如何能够也继承于 ERC721 呢?
幸运的是在 Solidity,你的合约可以继承自多个合约,参考如下:

1
2
3
contract SatoshiNakamoto is NickSzabo, HalFinney {
// 啧啧啧,宇宙的奥秘泄露了
}

正如你所见,当使用多重继承的时候,你只需要用逗号 , 来隔开几个你想要继承的合约。在上面的例子中,我们的合约继承自 NickSzabo 和 HalFinney

我们已经在上面为你创建了带着接口的 erc721.sol

  • 将 erc721.sol 导入到 zombieownership.sol
  • 声明 ZombieOwnership 继承自 ZombieAttack 和 ERC721
1
2
3
4
5
6
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {

}

balanceOf 和 ownerOf

我们已经把所有你需要在本课中实现的函数的空壳复制好了。
在本章节,我们将实现头两个方法: balanceOf 和 ownerOf。

balanceOf

1
function balanceOf(address _owner) public view returns (uint256 _balance);

这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。
在我们的例子中,我们的“代币”是僵尸。你还记得在我们 DApp 的哪里存储了一个主人拥有多少只僵尸吗?

ownerOf

1
function ownerOf(uint256 _tokenId) public view returns (address _owner);

这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address。
同样的,因为在我们的 DApp 里已经有一个 mapping (映射) 存储了这个信息,所以对我们来说这个实现非常直接清晰。我们可以只用一行 return 语句来实现这个函数

注意:要记得, uint256 等同于 uint。我们从课程的开始一直在代码中使用 uint,但从现在开始我们将在这里用 uint256,因为我们直接从规范中复制粘贴

每个函数的代码都应该只有 1 行 return 语句。看看我们在之前课程中写的代码,想想我们都把这个数据存储在哪

  • 实现 balanceOf 来返回 _owner 拥有的僵尸数量
  • 实现 ownerOf 来返回拥有 ID 为 _tokenId 僵尸的所有者的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];//新增的
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];//新增的
}
function transfer(address _to, uint256 _tokenId) public {

}
function approve(address _to, uint256 _tokenId) public {

}
function takeOwnership(uint256 _tokenId) public {
}
}

重构

在前一个章节我们定义了一个叫 ownerOf 的函数。但如果你还记得第 4 课的内容,我们同样在 zombiefeeding.sol 里以 ownerOf 命名创建了一个 modifier(修饰符)
如果你尝试编译这段代码,编译器会给你一个错误说你不能有相同名称的修饰符和函数

所以我们应该把在 ZombieOwnership 里的函数名称改成别的吗?
不,我们不能那样做!!!要记得,我们正在用 ERC721 代币标准,意味着其他合约将期望我们的合约以这些确切的名称来定义函数。这就是这些标准实用的原因——如果另一个合约知道我们的合约符合 ERC721 标准,它可以直接与我们交互,而无需了解任何关于我们内部如何实现的细节

所以,那意味着我们将必须重构我们第 4 课中的代码,将 modifier 的名称换成别的

我们回到了 zombiefeeding.sol 。我们将把 modifier 的名称从 ownerOf 改成 onlyOwnerOf。

  • 把修饰符定义中的名称改成 onlyOwnerOf
  • 往下滑到使用此修饰符的函数 feedAndMultiply 。我们也需要改这里的名称

    注意:我们在 zombiehelper.sol 和 zombieattack.sol 里也使用了这个修饰符

就改个修饰符,就不复制代码了

ERC721: 转移标准

现在我们将通过学习把所有权从一个人转移给另一个人来继续我们的 ERC721 规范的实现。
注意 ERC721 规范有两种不同的方法来转移代币:

1
2
3
4
function transfer(address _to, uint256 _tokenId) public;

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;

第一种方法是代币的拥有者调用 transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId
第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他
你注意到了吗,transfer 和 takeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)

我们把这个逻辑抽象成它自己的私有函数 _transfer,然后由这两个函数来调用它。 这样我们就不用写重复的代码了
让我们来定义 _transfer 的逻辑

  • 定义一个名为 _transfer 的函数。它会需要 3 个参数:address _from、address _to 和 uint256 _tokenId。它应该是一个 私有 函数
  • 我们有 2 个映射会在所有权改变的时候改变: ownerZombieCount (记录一个所有者有多少只僵尸)和 zombieToOwner (记录什么人拥有什么)
  • 我们的函数需要做的第一件事是为 接收 僵尸的人(address _to)增 加 ownerZombieCount。使用 ++ 来增加
  • 接下来,我们将需要为 发送 僵尸的人(address _from)减少 ownerZombieCount。使用 – 来扣减
  • 最后,我们将改变这个 _tokenId 的 zombieToOwner 映射,这样它现在就会指向 _to
  • 我们还需要再做一件事情。ERC721 规范包含了一个 Transfer 事件。这个函数的最后一行应该用正确的参数触发 Transfer ——查看 erc721.sol 看它期望传入的参数并在这里实现
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
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}//新增↑
function transfer(address _to, uint256 _tokenId) public {

}
function approve(address _to, uint256 _tokenId) public {

}
function takeOwnership(uint256 _tokenId) public {
}
}
  • 我们想确保只有代币或僵尸的所有者可以转移它。还记得我们如何限制只有所有者才能访问某个功能吗?没错,我们已经有一个修饰符能够完成这个任务了。所以将修饰符 onlyOwnerOf 添加到这个函数中。
  • 现在该函数的正文只需要一行代码。它只需要调用 _transfer。
  • 记得把 msg.sender 作为参数传递进 address _from。
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
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
// 1. 在这里添加修饰符
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId){
_transfer(msg.sender,_to,_tokenId);
// 2. 在这里定义方法
}
function approve(address _to, uint256 _tokenId) public {
}
function takeOwnership(uint256 _tokenId) public {
}
}

ERC721: 批准

现在,让我们来实现 approve
记住,使用 approve 或者 takeOwnership 的时候,转移有 2 个步骤:
你,作为所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve
新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他获得了批准,然后把代币转移给他
因为这发生在 2 个函数的调用中,所以在函数调用之间,我们需要一个数据结构来存储什么人被批准获取什么

  • 首先,让我们来定义一个映射 zombieApprovals。它应该将一个 uint 映射到一个 address
  • 这样一来,当有人用一个 _tokenId 调用 takeOwnership 时,我们可以用这个映射来快速查找谁被批准获取那个代币
  • 在函数 approve 上, 我们想要确保只有代币所有者可以批准某人来获取代币。所以我们需要添加修饰符 onlyOwnerOf 到 approve
  • 函数的正文部分,将 _tokenId 的 zombieApprovals 设置为和 _to 相等
  • 最后,在 ERC721 规范里有一个 Approval 事件。所以我们应该在这个函数的最后触发这个事件(参考 erc721.sol 来确认传入的参数,并确保 _owner 是 msg.sender)
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
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
// 2. 在这里添加方法修饰符
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
// 3. 在这里定义方法
}
function takeOwnership(uint256 _tokenId) public {
}
}

ERC721: takeOwnership

最后一个函数 takeOwnership, 应该只是简单地检查以确保 msg.sender 已经被批准来提取这个代币或者僵尸。若确认,就调用 _transfer

  • 首先,我们要用一个 require 句式来检查 _tokenId 的 zombieApprovals 和 msg.sender 相等
  • 这样如果 msg.sender 未被授权来提取这个代币,将抛出一个错误
  • 为了调用 _transfer,我们需要知道代币所有者的地址(它需要一个 _from 来作为参数)。幸运的是我们可以在我们的 ownerOf 函数中来找到这个参数
  • 所以,定义一个名为 owner 的 address 变量,并使其等于 ownerOf(_tokenId)
  • 最后,调用 _transfer, 并传入所有必须的参数。(在这里你可以用 msg.sender 作为 _to, 因为代币正是要发送给调用这个函数的人)