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

mug發表於2021-09-09
透過上一節的學習,我們完成了 ERC721 的實現。並不是很複雜,對吧?很多類似的以太坊概念,當你只聽人們談論它們的時候,會覺得很複雜。所以最簡單的理解方式就是你自己來實現它。

一、預防溢位

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

但是為了讓我們的課程不至於離題太遠,所以我們只專注於一些基礎實現。如果你想學習一些更深層次的實現,可以在這個教程結束後,去看看 OpenZeppelin 的 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 以後出現什麼異常情況。

使用 SafeMath

為了防止這些情況,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 = 8uint256 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 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 相似,若結果為否它就會丟擲錯誤。 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 溢位或下溢。我們可以將其命名為 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 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 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 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 

三、註釋

屍遊戲的 Solidity 程式碼終於完成啦。

在以後的課程中,我們將學習如何將遊戲部署到以太坊,以及如何和 Web3.js 互動。

不過在你離開這節之前,我們來談談如何 給你的程式碼新增註釋.

註釋語法

Solidity 裡的註釋和 JavaScript 相同。在我們的課程中你已經看到了不少單行註釋了:

// 這是一個單行註釋,可以理解為給自己或者別人看的筆記

只要在任何地方新增一個 // 就意味著你在註釋。如此簡單所以你應該經常這麼做。

不過我們也知道你的想法:有時候單行註釋是不夠的。畢竟你生來話癆。

contract CryptoZombies { 
  /* 這是一個多行註釋。我想對所有花時間來嘗試這個程式設計課程的人說聲謝謝。
  它是免費的,並將永遠免費。但是我們依然傾注了我們的心血來讓它變得更好。

   要知道這依然只是區塊鏈開發的開始而已,雖然我們已經走了很遠,
   仍然有很多種方式來讓我們的社群變得更好。
   如果我們在哪個地方出了錯,歡迎在我們的 github 提交 PR 或者 issue 來幫助我們改進:
    

    或者,如果你有任何的想法、建議甚至僅僅想和我們打聲招呼,歡迎來我們的電報群:
     
  */
}

所以我們有了多行註釋:

contract CryptoZombies { 
  /* 這是一個多行註釋。我想對所有花時間來嘗試這個程式設計課程的人說聲謝謝。
  它是免費的,並將永遠免費。但是我們依然傾注了我們的心血來讓它變得更好。

   要知道這依然只是區塊鏈開發的開始而已,雖然我們已經走了很遠,
   仍然有很多種方式來讓我們的社群變得更好。
   如果我們在哪個地方出了錯,歡迎在我們的 github 提交 PR 或者 issue 來幫助我們改進:
    

    或者,如果你有任何的想法、建議甚至僅僅想和我們打聲招呼,歡迎來我們的電報群:
     
  */
}

特別是,最好為你合約中每個方法新增註釋來解釋它的預期行為。這樣其他開發者(或者你自己,在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/17contract 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);
  }
}

原文連結:https://segmentfault.com/a/1190000015310168

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/506/viewspace-2806712/,如需轉載,請註明出處,否則將追究法律責任。

相關文章