让我们来聊聊 代币.
如果你对以太坊的世界有一些了解,你很可能听过人们聊到代币——尤其是 ERC20 代币.
一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
.
在智能合约内部,通常有一个映射, mapping(address => uint256) balances
,用于追踪每个地址还有多少余额。
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。
这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了。
其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。
交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。
对于像货币一样的代币来说,ERC20 代币非常酷。 但是要在我们僵尸游戏中代表僵尸就并不是特别有用。
首先,僵尸不像货币可以分割 —— 我可以发给你 0.237 以太,但是转移给你 0.237 的僵尸听起来就有些搞笑。
其次,并不是所有僵尸都是平等的。 你的2级僵尸"Steve"完全不能等同于我732级的僵尸"H4XF13LD MORRIS ?????"。(你差得远呢,Steve)。
有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721 代币.
ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这些特性正好让我们的僵尸可以用来交易。
请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。
我们将来学习你在编写智能合约的时候需要注意的一个主要的安全特性:防止溢出和下溢。
什么是 溢出 (overflow)?
假设我们有一个 uint8
, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111
(或者说十进制的 2^8 - 1 = 255).
来看看下面的代码。最后 number
将会是什么值?
uint8 number = 255;
number++;
在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number
出乎意料地等于 0
了。 (如果你给二进制 11111111
加1, 它将被重置为 00000000
,就像钟表从 23:59
走向 00:00
)。
下溢(underflow
)也类似,如果你从一个等于 0
的 uint8
减去 1
, 它将变成 255
(因为 uint
是无符号的,其不能等于负数)。
虽然我们在这里不使用 uint8
,而且每次给一个 uint256
加 1
也不太可能溢出 (2^256 真的是一个很大的数了),在我们的合约中添加一些保护机制依然是非常有必要的,以防我们的 DApp 以后出现什么异常情况。
为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 库(library),默认情况下可以防止这些问题。
不过在我们使用之前…… 什么叫做库?
一个库 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。
比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256
这样的语法。 SafeMath 库有四个方法 — add
, sub
, mul
, 以及 div
。现在我们可以这样来让 uint256
调用这些方法:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
来看看 SafeMath 的部分代码:
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
首先我们有了 library
关键字 — 库和 合约
很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 using
关键字,它可以自动把库的所有方法添加给一个数据类型:
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul
和 add
其实都需要两个参数。 在我们声明了 using SafeMath for uint
后,我们用来调用这些方法的 uint
就自动被作为第一个参数传递进去了(在此例中就是 test
)
我们来看看 add
的源代码看 SafeMath 做了什么:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
基本上 add
只是像 +
一样对两个 uint
相加, 但是它用一个 assert
语句来确保结果大于 a
。这样就防止了溢出。
assert
和 require
相似,若结果为否它就会抛出错误。 assert
和 require
区别在于,require
若失败则会返还给用户剩下的 gas, assert
则不会。所以大部分情况下,你写代码的时候会比较喜欢 require
,assert
只在代码可能出现严重错误的时候使用,比如 uint
溢出。
所以简而言之, SafeMath 的 add
, sub
, mul
, 和 div
方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。
Solidity 里的注释和 JavaScript 相同。单行注释了:
// 这是一个单行注释,可以理解为给自己或者别人看的笔记
只要在任何地方添加一个 //
就意味着你在注释。如此简单所以你应该经常这么做。
不过我们也知道你的想法:有时候单行注释是不够的。毕竟你生来话痨。
所以我们有了多行注释:
contract CryptoZombies {
/* 这是一个多行注释。我想对所有花时间来尝试这个编程课程的人说声谢谢。
它是免费的,并将永远免费。但是我们依然倾注了我们的心血来让它变得更好。
要知道这依然只是区块链开发的开始而已,虽然我们已经走了很远,
仍然有很多种方式来让我们的社区变得更好。
如果我们在哪个地方出了错,欢迎在我们的 github 提交 PR 或者 issue 来帮助我们改进:
https://github.com/loomnetwork/cryptozombie-lessons
或者,如果你有任何的想法、建议甚至仅仅想和我们打声招呼,欢迎来我们的电报群:
https://t.me/loomnetworkcn
*/
}
特别是,最好为你合约中每个方法添加注释来解释它的预期行为。这样其他开发者(或者你自己,在6个月以后再回到这个项目中)可以很快地理解你的代码而不需要逐行阅读所有代码。
Solidity 社区所使用的一个标准是使用一种被称作 natspec 的格式,看起来像这样:
/// @title 一个简单的基础运算合约
/// @author H4XF13LD MORRIS ?????
/// @notice 现在,这个合约只添加一个乘法
contract Math {
/// @notice 两个数相乘
/// @param x 第一个 uint
/// @param y 第二个 uint
/// @return z (x * y) 的结果
/// @dev 现在这个方法不检查溢出
function multiply(uint x, uint y) returns (uint z) {
// 这只是个普通的注释,不会被 natspec 解释
z = x * y;
}
}
@title
(标题) 和 @author
(作者)很直接了.
@notice
(须知)向 用户 解释这个方法或者合约是做什么的。 @dev
(开发者) 是向开发者解释更多的细节。
@param
(参数)和 @return
(返回) 用来描述这个方法需要传入什么参数以及返回什么值。
注意你并不需要每次都用上所有的标签,它们都是可选的。不过最少,写下一个 @dev
注释来解释每个方法是做什么的