以太坊開發實戰學習-合約安全(八)

經過上一節的學習,咱們完成了 ERC721 的實現。並非很複雜,對吧?不少相似的以太坊概念,當你只聽人們談論它們的時候,會以爲很複雜。因此最簡單的理解方式就是你本身來實現它。

1、預防溢出

不過要記住那只是最簡單的實現。還有不少的特性咱們也許想加入到咱們的實現中來,好比一些額外的檢查,來確保用戶不會不當心把他們的殭屍轉移給0 地址(這被稱做 「燒幣」, 基本上就是把代幣轉移到一個誰也沒有私鑰的地址,讓這個代幣永遠也沒法恢復)。 或者在 DApp 中加入一些基本的拍賣邏輯。(你能想出一些實現的方法麼?)git

可是爲了讓咱們的課程不至於離題太遠,因此咱們只專一於一些基礎實現。若是你想學習一些更深層次的實現,能夠在這個教程結束後,去看看 OpenZeppelin 的 ERC721 合約。github

合約安全加強:溢出和下溢

咱們未來學習你在編寫智能合約的時候須要注意的一個主要的安全特性:防止溢出和下溢。編程

什麼是溢出(overflow)?

假設咱們有一個 uint8, 只能存儲8 bit數據。這意味着咱們能存儲的最大數字就是二進制 11111111 (或者說十進制的 2^8 - 1 = 255).安全

來看看下面的代碼。最後 number 將會是什麼值?app

uint8 number = 255;
number++;

在這個例子中,咱們致使了溢出 — 雖然咱們加了1, 可是 number 出乎意料地等於 0了。 (若是你給二進制 111111111, 它將被重置爲 00000000,就像鐘錶從 23:59 走向 00:00)。less

下溢(underflow)也相似,若是你從一個等於 0 的 uint8 減去 1, 它將變成 255 (由於 uint 是無符號的,其不能等於負數)。dom

雖然咱們在這裏不使用 uint8,並且每次給一個 uint256 加 1 也不太可能溢出 (2^256 真的是一個很大的數了),在咱們的合約中添加一些保護機制依然是很是有必要的,以防咱們的 DApp 之後出現什麼異常狀況。學習

使用 SafeMath

爲了防止這些狀況,OpenZeppelin 創建了一個叫作 SafeMath 的 庫(library),默認狀況下能夠防止這些問題。區塊鏈

不過在咱們使用以前…… 什麼叫作庫?ui

一個是 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 裏調用,這是咱們的基礎合約 — 這樣其餘全部繼承出去的子合約均可以使用這個庫了。

  • 一、將 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);
  }

}

2、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 方法把。

  • 一、將 ++ 替換成 SafeMath 方法。
  • 二、將 -- 替換成 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;
  }
}

實戰演練

分配:

  • 一、聲明咱們將爲 uint32 使用SafeMath32。
  • 二、聲明咱們將爲 uint16 使用SafeMath16。
  • 三、在 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);
    }
  }
}

3、註釋

屍遊戲的 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);
  }
}
相關文章
相關標籤/搜索