以太坊開發實戰學習-合約安全(八)
透過上一節的學習,我們完成了 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 則不會。所以大部分情況下,你寫程式碼的時候會比較喜歡 require
,assert
只在程式碼可能出現嚴重錯誤的時候使用,比如 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); } }
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/506/viewspace-2806712/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 以太坊智慧合約開發:讓合約接受轉賬
- 以太坊智慧合約開發第四篇:實現Hello World智慧合約
- 以太坊開發實戰學習-ERC721標準(七)
- 以太坊開發實戰學習-Web3.js(十)WebJS
- 以太坊開發實戰學習-Web3.js(九)WebJS
- 以太坊開發實戰學習-高階Solidity理論 (五)Solid
- 如何打造安全的以太坊智慧合約
- eth以太坊智慧合約交易平臺開發
- 以太坊智慧合約開發第六篇:truffle開發框架框架
- substrate學習筆記9:開發智慧合約筆記
- 以太坊智慧合約開發第二篇:理解以太坊相關概念
- Conflux與以太坊合約開發工具區別UX
- 【區塊鏈】實戰·以太坊智慧合約程式設計引導區塊鏈程式設計
- 以太坊智慧合約開發第七篇:智慧合約與網頁互動網頁
- 【精通以太坊】——第九章 智慧合約安全
- substrate學習筆記10:開發erc20合約筆記
- 智慧合約開發(3)—— 以太坊虛擬機器(EVM)基礎虛擬機
- olidity語言開發以太坊智慧合約中的繼承繼承
- 以太坊Solidity程式語言開發框架————4、編譯合約Solid框架編譯
- 以太坊Solidity程式語言開發框架————7、合約互動Solid框架
- 以太坊Solidity程式語言開發框架————8、測試合約Solid框架
- 以太坊智慧合約開發第五篇:字串拼接—Solidity字串Solid
- 爬蟲實戰開發學習(一)爬蟲
- 以太坊蜜罐智慧合約分析
- substrate學習筆記8:ink合約開發之初體驗筆記
- Polygon馬蹄鏈在以太坊上的智慧合約開發應用Go
- dubbo學習筆記---dubbo開發實戰筆記
- 量化合約/合約量化/秒合約系統開發/永續合約/合約跟單
- 以太坊智慧合約升級策略
- 以太坊智慧合約-猜數字
- 合約量化|秒合約|合約跟單系統開發案例
- 秒合約|合約跟單|永續合約系統開發模式模式
- 以太坊智慧合約開發環境搭建以及第一個Dapp開發環境APP
- 以太坊智慧合約開發第三篇:安裝節點工具Ganache
- 開發NEO智慧合約的實用技巧
- SET智慧合約量化系統開發|秒合約量化開發搭建
- 以太坊智慧合約gas如何估計?
- 以太坊智慧合約call注入攻擊