通過上一節(jié)的學(xué)習(xí)舶得,我們完成了 ERC721 的實現(xiàn)。并不是很復(fù)雜爽蝴,對吧沐批?很多類似的以太坊概念,當(dāng)你只聽人們談?wù)撍鼈兊臅r候霜瘪,會覺得很復(fù)雜。所以最簡單的理解方式就是你自己來實現(xiàn)它惧磺。
一颖对、預(yù)防溢出
不過要記住那只是最簡單的實現(xiàn)。還有很多的特性我們也許想加入到我們的實現(xiàn)中來磨隘,比如一些額外的檢查缤底,來確保用戶不會不小心把他們的僵尸轉(zhuǎn)移給0 地址(這被稱作 “燒幣
”, 基本上就是把代幣轉(zhuǎn)移到一個誰也沒有私鑰的地址顾患,讓這個代幣永遠也無法恢復(fù))。 或者在 DApp 中加入一些基本的拍賣邏輯个唧。(你能想出一些實現(xiàn)的方法么江解?)
但是為了讓我們的課程不至于離題太遠,所以我們只專注于一些基礎(chǔ)實現(xiàn)徙歼。如果你想學(xué)習(xí)一些更深層次的實現(xiàn)犁河,可以在這個教程結(jié)束后,去看看 OpenZeppelin
的 ERC721 合約魄梯。
合約安全增強:溢出和下溢
我們將來學(xué)習(xí)你在編寫智能合約的時候需要注意的一個主要的安全特性:防止溢出和下溢桨螺。
什么是溢出(overflow)?
假設(shè)我們有一個 uint8
, 只能存儲8 bit數(shù)據(jù)。這意味著我們能存儲的最大數(shù)字就是二進制 11111111
(或者說十進制的 2^8 - 1 = 255).
來看看下面的代碼酿秸。最后 number 將會是什么值灭翔?
uint8 number = 255;
number++;
在這個例子中,我們導(dǎo)致了溢出 — 雖然我們加了1辣苏, 但是 number
出乎意料地等于 0
了肝箱。 (如果你給二進制 11111111
加1
, 它將被重置為 00000000
,就像鐘表從 23:59 走向 00:00)稀蟋。
下溢(underflow)
也類似煌张,如果你從一個等于 0 的 uint8 減去 1, 它將變成 255 (因為 uint 是無符號的,其不能等于負數(shù))糊治。
雖然我們在這里不使用 uint8唱矛,而且每次給一個 uint256 加 1 也不太可能溢出 (2^256 真的是一個很大的數(shù)了),在我們的合約中添加一些保護機制依然是非常有必要的井辜,以防我們的 DApp 以后出現(xiàn)什么異常情況绎谦。
使用 SafeMath
為了防止這些情況,OpenZeppelin 建立了一個叫做 SafeMath
的 庫(library)粥脚,默認情況下可以防止這些問題窃肠。
不過在我們使用之前…… 什么叫做庫?
一個庫
是 Solidity 中一種特殊的合約。其中一個有用的功能是給原始數(shù)據(jù)類型增加一些方法刷允。
比如冤留,使用 SafeMath 庫的時候,我們將使用 using SafeMath for uint256
這樣的語法树灶。 SafeMath 庫有四個方法 — add
纤怒, sub
, mul
天通, 以及 div
〔淳剑現(xiàn)在我們可以這樣來讓 uint256 調(diào)用這些方法:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
我們將在下一章來學(xué)習(xí)這些方法,不過現(xiàn)在我們先將 SafeMath 庫添加進我們的合約。
實戰(zhàn)演練
我們已經(jīng)幫你把 OpenZeppelin 的 SafeMath
庫包含進 safemath.sol
了烘豹,如果你想看一下代碼的話瓜贾,現(xiàn)在可以看看,不過我們下一節(jié)將深入進去携悯。
首先我們來告訴我們的合約要使用 SafeMath祭芦。我們將在我們的 ZombieFactory 里調(diào)用,這是我們的基礎(chǔ)合約 — 這樣其他所有繼承出去的子合約都可以使用這個庫了憔鬼。
- 1龟劲、將
safemath.sol
引入到zombiefactory.sol
. - 2、添加定義:
using SafeMath for uint256;
.
zombiefactory.sol
pragma solidity ^0.4.19;
import "./ownable.sol";
// 1\. 在這里引入
import "./safemath.sol";
contract ZombieFactory is Ownable {
// 2\. 在這里定義 using safemath
using SafeMath for uint 256;
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
二逊彭、SafeMath
來看看 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
關(guān)鍵字 — 庫和 合約
很相似咸灿,但是又有一些不同。 就我們的目的而言侮叮,庫允許我們使用 using
關(guān)鍵字避矢,它可以自動把庫的所有方法添加給一個數(shù)據(jù)類型:
using SafeMath for uint;
// 這下我們可以為任何 uint 調(diào)用這些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul 和 add 其實都需要兩個參數(shù)。 在我們聲明了 using SafeMath for uint
后囊榜,我們用來調(diào)用這些方法的 uint 就自動被作為第一個參數(shù)傳遞進去了(在此例中就是 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
語句來確保結(jié)果大于 a
。這樣就防止了溢出卸勺。
assert和require區(qū)別
assert
和 require
相似砂沛,若結(jié)果為否它就會拋出錯誤。 assert 和 require 區(qū)別在于曙求,require
若失敗則會返還給用戶剩下的 gas碍庵, assert 則不會。所以大部分情況下悟狱,你寫代碼的時候會比較喜歡 require
静浴,assert
只在代碼可能出現(xiàn)嚴重錯誤的時候使用,比如 uint 溢出挤渐。
所以簡而言之苹享, SafeMath 的 add, sub浴麻, mul得问, 和 div 方法只做簡單的四則運算,然后在發(fā)生溢出或下溢的時候拋出錯誤软免。
在我們的代碼里使用SafeMath宫纬。
為了防止溢出和下溢,我們可以在我們的代碼里找 +膏萧, -漓骚, *宣蔚, 或 /,然后替換為 add, sub, mul, div.
比如认境,與其這樣做:
myUint++;
我們這樣做:
myUint = myUint.add(1);
實戰(zhàn)演練
在 ZombieOwnership
中有兩個地方用到了數(shù)學(xué)運算,來替換成 SafeMath 方法把挟鸠。
- 1叉信、將 ++ 替換成 SafeMath 方法。
- 2艘希、將 -- 替換成 SafeMath 方法硼身。
ZombieOwnership
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
using SafeMath for uint256;
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 {
// 1\. 替換成 SafeMath 的 `add`
// ownerZombieCount[_to].add(1); // 這種寫法錯誤,沒有賦值
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
// 2\. 替換成 SafeMath 的 `sub`
// ownerZombieCount[_from].sub(1); // 這種寫法錯誤
ownerZombieCount[_from] = ownerZombieCount[_from].sub(1);
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
function takeOwnership(uint256 _tokenId) public {
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
}
其他類型
太好了,這下我們的 ERC721 實現(xiàn)不會有溢出或者下溢了覆享。
回頭看看我們在之前課程寫的代碼佳遂,還有其他幾個地方也有可能導(dǎo)致溢出或下溢。
比如撒顿, 在 ZombieAttack
里面我們有:
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
我們同樣應(yīng)該在這些地方防止溢出丑罪。(通常情況下,總是使用 SafeMath 而不是普通數(shù)學(xué)運算是個好主意凤壁,也許在以后 Solidity 的新版本里這點會被默認實現(xiàn)吩屹,但是現(xiàn)在我們得自己在代碼里實現(xiàn)這些額外的安全措施)。
不過我們遇到個小問題 — winCount 和 lossCount 是 uint16
拧抖, 而 level 是 uint32
煤搜。 所以如果我們用這些作為參數(shù)傳入 SafeMath 的 add 方法。 它實際上并不會防止溢出唧席,因為它會把這些變量都轉(zhuǎn)換成 uint256:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
// 如果我們在`uint8` 上調(diào)用 `.add`擦盾。它將會被轉(zhuǎn)換成 `uint256`.
// 所以它不會在 2^8 時溢出,因為 256 是一個有效的 `uint256`.
這就意味著淌哟,我們需要再實現(xiàn)兩個庫來防止 uint16 和 uint32 溢出或下溢迹卢。我們可以將其命名為 SafeMath16
和 SafeMath32
。
代碼將和 SafeMath 完全相同绞绒,除了所有的 uint256 實例都將被替換成 uint32 或 uint16婶希。
我們已經(jīng)將這些代碼幫你寫好了,打開 safemath.sol
合約看看代碼吧蓬衡。
現(xiàn)在我們需要在 ZombieFactory
里使用它們喻杈。
safemath.sol
pragma solidity ^0.4.18;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
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;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
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;
}
/**
* @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title SafeMath32
* @dev SafeMath library implemented for uint32
*/
library SafeMath32 {
function mul(uint32 a, uint32 b) internal pure returns (uint32) {
if (a == 0) {
return 0;
}
uint32 c = a * b;
assert(c / a == b);
return c;
}
function div(uint32 a, uint32 b) internal pure returns (uint32) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint32 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint32 a, uint32 b) internal pure returns (uint32) {
assert(b <= a);
return a - b;
}
function add(uint32 a, uint32 b) internal pure returns (uint32) {
uint32 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title SafeMath16
* @dev SafeMath library implemented for uint16
*/
library SafeMath16 {
function mul(uint16 a, uint16 b) internal pure returns (uint16) {
if (a == 0) {
return 0;
}
uint16 c = a * b;
assert(c / a == b);
return c;
}
function div(uint16 a, uint16 b) internal pure returns (uint16) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint16 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint16 a, uint16 b) internal pure returns (uint16) {
assert(b <= a);
return a - b;
}
function add(uint16 a, uint16 b) internal pure returns (uint16) {
uint16 c = a + b;
assert(c >= a);
return c;
}
}
實戰(zhàn)演練
分配:
- 1、聲明我們將為 uint32 使用SafeMath32狰晚。
- 2筒饰、聲明我們將為 uint16 使用SafeMath16。
- 3壁晒、在 ZombieFactory 里還有一處我們也應(yīng)該使用 SafeMath 的方法瓷们, 我們已經(jīng)在那里留了注釋提醒你。
zombiefactory.sol
pragma solidity ^0.4.19;
import "./ownable.sol";
import "./safemath.sol";
contract ZombieFactory is Ownable {
using SafeMath for uint256;
// 1\. 為 uint32 聲明 使用 SafeMath32
using SafeMath32 for uint32;
// 2\. 為 uint16 聲明 使用 SafeMath16
using SafeMath16 for uint16;
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) internal {
// 注意: 我們選擇不處理2038年問題,所以不用擔(dān)心 readyTime 的溢出
// 反正在2038年我們的APP早完蛋了
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
// 3\. 在這里使用 SafeMath 的 `add` 方法:
// ownerZombieCount[msg.sender]++;
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
現(xiàn)在谬晕,讓我們也順手把zombieattack.sol
文件里邊的方法也修改為safeMath 形式碘裕。
zombieattack.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
// 這兒有一個
randNonce = randNonce.add(1);
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
// 這里有三個
myZombie.winCount = myZombie.winCount.add(1);
myZombie.level = myZombie.level.add(1);
enemyZombie.lossCount = enemyZombie.lossCount.add(1);
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
// 這兒還有倆哦
myZombie.lossCount = myZombie.lossCount.add(1);
enemyZombie.winCount = enemyZombie.winCount.add(1);
_triggerCooldown(myZombie);
}
}
}
三、注釋
尸游戲的 Solidity 代碼終于完成啦攒钳。
在以后的課程中帮孔,我們將學(xué)習(xí)如何將游戲部署到以太坊,以及如何和 Web3.js 交互不撑。
不過在你離開這節(jié)之前文兢,我們來談?wù)勅绾?給你的代碼添加注釋
.
注釋語法
Solidity 里的注釋和 JavaScript 相同。在我們的課程中你已經(jīng)看到了不少單行注釋了:
// 這是一個單行注釋焕檬,可以理解為給自己或者別人看的筆記
只要在任何地方添加一個 //
就意味著你在注釋姆坚。如此簡單所以你應(yīng)該經(jīng)常這么做。
不過我們也知道你的想法:有時候單行注釋是不夠的实愚。畢竟你生來話癆兼呵。
contract CryptoZombies {
/* 這是一個多行注釋。我想對所有花時間來嘗試這個編程課程的人說聲謝謝腊敲。
它是免費的萍程,并將永遠免費。但是我們依然傾注了我們的心血來讓它變得更好兔仰。
要知道這依然只是區(qū)塊鏈開發(fā)的開始而已茫负,雖然我們已經(jīng)走了很遠,
仍然有很多種方式來讓我們的社區(qū)變得更好乎赴。
如果我們在哪個地方出了錯忍法,歡迎在我們的 github 提交 PR 或者 issue 來幫助我們改進:
https://github.com/loomnetwork/cryptozombie-lessons
或者,如果你有任何的想法榕吼、建議甚至僅僅想和我們打聲招呼饿序,歡迎來我們的電報群:
https://t.me/loomnetworkcn
*/
}
所以我們有了多行注釋:
contract CryptoZombies {
/* 這是一個多行注釋。我想對所有花時間來嘗試這個編程課程的人說聲謝謝羹蚣。
它是免費的原探,并將永遠免費。但是我們依然傾注了我們的心血來讓它變得更好顽素。
要知道這依然只是區(qū)塊鏈開發(fā)的開始而已咽弦,雖然我們已經(jīng)走了很遠,
仍然有很多種方式來讓我們的社區(qū)變得更好胁出。
如果我們在哪個地方出了錯型型,歡迎在我們的 github 提交 PR 或者 issue 來幫助我們改進:
https://github.com/loomnetwork/cryptozombie-lessons
或者,如果你有任何的想法全蝶、建議甚至僅僅想和我們打聲招呼闹蒜,歡迎來我們的電報群:
https://t.me/loomnetworkcn
*/
}
特別是寺枉,最好為你合約中每個方法添加注釋來解釋它的預(yù)期行為。這樣其他開發(fā)者(或者你自己绷落,在6個月以后再回到這個項目中)可以很快地理解你的代碼而不需要逐行閱讀所有代碼姥闪。
Solidity 社區(qū)所使用的一個標準是使用一種被稱作 natspec
的格式,看起來像這樣:
/// @title 一個簡單的基礎(chǔ)運算合約
/// @author H4XF13LD MORRIS
/// @notice 現(xiàn)在砌烁,這個合約只添加一個乘法
contract Math {
/// @notice 兩個數(shù)相乘
/// @param x 第一個 uint
/// @param y 第二個 uint
/// @return z (x * y) 的結(jié)果
/// @dev 現(xiàn)在這個方法不檢查溢出
function multiply(uint x, uint y) returns (uint z) {
// 這只是個普通的注釋甘畅,不會被 natspec 解釋
z = x * y;
}
}
@title
(標題) 和 @author
(作者)很直接了.
@notice
(須知)向 用戶 解釋這個方法或者合約是做什么的。@dev
(開發(fā)者) 是向開發(fā)者解釋更多的細節(jié)往弓。
@param
(參數(shù))和 @return
(返回) 用來描述這個方法需要傳入什么參數(shù)以及返回什么值。
注意你并不需要每次都用上所有的標簽蓄氧,它們都是可選的函似。不過最少,寫下一個 @dev 注釋來解釋每個方法是做什么的喉童。
實戰(zhàn)演練
給 ZombieOwnership
加上一些 natspec
標簽:
zombieownership.sol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";
/// TODO: 把這里變成 natspec 標準的注釋把
/// @title 一個管理轉(zhuǎn)移僵尸所有權(quán)的合約
/// @author Corwien
/// @dev 符合 OpenZeppelin 對 ERC721 標準草案的實現(xiàn)
/// @date 2018/06/17
contract ZombieOwnership is ZombieAttack, ERC721 {
using SafeMath for uint256;
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[_to].add(1);
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
function takeOwnership(uint256 _tokenId) public {
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
}