经过上一节的学习,咱们完成了 ERC721 的实现。并非很复杂,对吧?不少相似的以太坊概念,当你只听人们谈论它们的时候,会以为很复杂。因此最简单的理解方式就是你本身来实现它。
不过要记住那只是最简单的实现。还有不少的特性咱们也许想加入到咱们的实现中来,好比一些额外的检查,来确保用户不会不当心把他们的僵尸转移给0 地址(这被称做 “烧币
”, 基本上就是把代币转移到一个谁也没有私钥的地址,让这个代币永远也没法恢复)。 或者在 DApp 中加入一些基本的拍卖逻辑。(你能想出一些实现的方法么?)git
可是为了让咱们的课程不至于离题太远,因此咱们只专一于一些基础实现。若是你想学习一些更深层次的实现,能够在这个教程结束后,去看看 OpenZeppelin
的 ERC721 合约。github
咱们未来学习你在编写智能合约的时候须要注意的一个主要的安全特性:防止溢出和下溢。编程
假设咱们有一个 uint8
, 只能存储8 bit数据。这意味着咱们能存储的最大数字就是二进制 11111111
(或者说十进制的 2^8 - 1 = 255).安全
来看看下面的代码。最后 number 将会是什么值?app
uint8 number = 255; number++;
在这个例子中,咱们致使了溢出 — 虽然咱们加了1, 可是 number
出乎意料地等于 0
了。 (若是你给二进制 11111111
加1
, 它将被重置为 00000000
,就像钟表从 23:59 走向 00:00)。less
下溢(underflow)
也相似,若是你从一个等于 0 的 uint8 减去 1, 它将变成 255 (由于 uint 是无符号的,其不能等于负数)。dom
虽然咱们在这里不使用 uint8,并且每次给一个 uint256 加 1 也不太可能溢出 (2^256 真的是一个很大的数了),在咱们的合约中添加一些保护机制依然是很是有必要的,以防咱们的 DApp 之后出现什么异常状况。学习
为了防止这些状况,OpenZeppelin 创建了一个叫作 SafeMath
的 库(library),默认状况下能够防止这些问题。区块链
不过在咱们使用以前…… 什么叫作库?ui
一个库
是 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 库添加进咱们的合约。
咱们已经帮你把 OpenZeppelin 的 SafeMath
库包含进 safemath.sol
了,若是你想看一下代码的话,如今能够看看,不过咱们下一节将深刻进去。
首先咱们来告诉咱们的合约要使用 SafeMath。咱们将在咱们的 ZombieFactory 里调用,这是咱们的基础合约 — 这样其余全部继承出去的子合约均可以使用这个库了。
safemath.sol
引入到 zombiefactory.sol
.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 的部分代码:
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 方法只作简单的四则运算,而后在发生溢出或下溢的时候抛出错误。
为了防止溢出和下溢,咱们能够在咱们的代码里找 +, -, *, 或 /,而后替换为 add, sub, mul, div.
好比,与其这样作:
myUint++;
咱们这样作:
myUint = myUint.add(1);
在 ZombieOwnership
中有两个地方用到了数学运算,来替换成 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 实现不会有溢出或者下溢了。
回头看看咱们在以前课程写的代码,还有其余几个地方也有可能致使溢出或下溢。
好比, 在 ZombieAttack
里面咱们有:
myZombie.winCount++; myZombie.level++; enemyZombie.lossCount++;
咱们一样应该在这些地方防止溢出。(一般状况下,老是使用 SafeMath 而不是普通数学运算是个好主意,也许在之后 Solidity 的新版本里这点会被默认实现,可是如今咱们得本身在代码里实现这些额外的安全措施)。
不过咱们遇到个小问题 — winCount 和 lossCount 是 uint16
, 而 level 是 uint32
。 因此若是咱们用这些做为参数传入 SafeMath 的 add 方法。 它实际上并不会防止溢出,由于它会把这些变量都转换成 uint256:
function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; } // 若是咱们在`uint8` 上调用 `.add`。它将会被转换成 `uint256`. // 因此它不会在 2^8 时溢出,由于 256 是一个有效的 `uint256`.
这就意味着,咱们须要再实现两个库来防止 uint16 和 uint32 溢出或下溢。咱们能够将其命名为 SafeMath16
和 SafeMath32
。
代码将和 SafeMath 彻底相同,除了全部的 uint256 实例都将被替换成 uint32 或 uint16。
咱们已经将这些代码帮你写好了,打开 safemath.sol
合约看看代码吧。
如今咱们须要在 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; } }
分配:
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年问题,因此不用担忧 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); } }
如今,让咱们也顺手把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 代码终于完成啦。
在之后的课程中,咱们将学习如何将游戏部署到以太坊,以及如何和 Web3.js 交互。
不过在你离开这节以前,咱们来谈谈如何 给你的代码添加注释
.
Solidity 里的注释和 JavaScript 相同。在咱们的课程中你已经看到了很多单行注释了:
// 这是一个单行注释,能够理解为给本身或者别人看的笔记
只要在任何地方添加一个 //
就意味着你在注释。如此简单因此你应该常常这么作。
不过咱们也知道你的想法:有时候单行注释是不够的。毕竟你生来话痨。
contract CryptoZombies { /* 这是一个多行注释。我想对全部花时间来尝试这个编程课程的人说声谢谢。 它是免费的,并将永远免费。可是咱们依然倾注了咱们的心血来让它变得更好。 要知道这依然只是区块链开发的开始而已,虽然咱们已经走了很远, 仍然有不少种方式来让咱们的社区变得更好。 若是咱们在哪一个地方出了错,欢迎在咱们的 github 提交 PR 或者 issue 来帮助咱们改进: https://github.com/loomnetwork/cryptozombie-lessons 或者,若是你有任何的想法、建议甚至仅仅想和咱们打声招呼,欢迎来咱们的电报群: https://t.me/loomnetworkcn */ }
因此咱们有了多行注释:
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 注释来解释每一个方法是作什么的。
给 ZombieOwnership
加上一些 natspec
标签:
zombieownership.sol
pragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; import "./safemath.sol"; /// TODO: 把这里变成 natspec 标准的注释把 /// @title 一个管理转移僵尸全部权的合约 /// @author Corwien /// @dev 符合 OpenZeppelin 对 ERC721 标准草案的实现 /// @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); } }