以太坊开发实战学习-合约安全(八)

8
通过上一节的学习,我们完成了 ERC721 的实现。并不是很复杂,对吧?很多类似的以太坊概念,当你只听人们谈论它们的时候,会觉得很复杂。所以最简单的理解方式就是你自己来实现它。

一、预防溢出

不过要记住那只是最简单的实现。还有很多的特性我们也许想加入到我们的实现中来,比如一些额外的检查,来确保用户不会不小心把他们的僵尸转移给0 地址(这被称作 “烧币”, 基本上就是把代币转移到一个谁也没有私钥的地址,让这个代币永远也无法恢复)。 或者在 DApp 中加入一些基本的拍卖逻辑。(你能想出一些实现的方法么?)

但是为了让我们的课程不至于离题太远,所以我们只专注于一些基础实现。如果你想学习一些更深层次的实现,可以在这个教程结束后,去看看 OpenZeppelin 的 ERC721 合约。

合约安全增强:溢出和下溢

我们将来学习你在编写智能合约的时候需要注意的一个主要的安全特性:防止溢出和下溢。

什么是溢出(overflow)?

假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255).

来看看下面的代码。最后 number 将会是什么值?

uint8 number = 255;
number++;

在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number 出乎意料地等于 0了。 (如果你给二进制 111111111, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00)。

下溢(underflow)也类似,如果你从一个等于 0 的 uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

虽然我们在这里不使用 uint8,而且每次给一个 uint256 加 1 也不太可能溢出 (2^256 真的是一个很大的数了),在我们的合约中添加一些保护机制依然是非常有必要的,以防我们的 DApp 以后出现什么异常情况。

使用 SafeMath

为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 库(library),默认情况下可以防止这些问题。

不过在我们使用之前…… 什么叫做库?

一个是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。

比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — addsubmul, 以及 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 里调用,这是我们的基础合约 — 这样其他所有继承出去的子合约都可以使用这个库了。

  • 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 关键字 — 库和 合约很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 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区别

assertrequire 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 requireassert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。

所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。

在我们的代码里使用SafeMath。

为了防止溢出和下溢,我们可以在我们的代码里找 +, -, *, 或 /,然后替换为 add, sub, mul, div.

比如,与其这样做:

myUint++;

我们这样做:

myUint = myUint.add(1);

实战演练

ZombieOwnership 中有两个地方用到了数学运算,来替换成 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 实现不会有溢出或者下溢了。

回头看看我们在之前课程写的代码,还有其他几个地方也有可能导致溢出或下溢。

比如, 在 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 溢出或下溢。我们可以将其命名为 SafeMath16SafeMath32

代码将和 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;
  }
}

实战演练

分配:

  • 1、声明我们将为 uint32 使用SafeMath32。
  • 2、声明我们将为 uint16 使用SafeMath16。
  • 3、在 ZombieFactory 里还有一处我们也应该使用 SafeMath 的方法, 我们已经在那里留了注释提醒你。

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);
  }
}

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...