摘要:合約安全增強溢出和下溢我們將來學習你在編寫智能合約的時候需要注意的一個主要的安全特性防止溢出和下溢。實戰演練給加上一些標簽把這里變成標準的注釋把一個管理轉移僵尸所有權的合約符合對標準草案的實現
通過上一節的學習,我們完成了 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 = 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區別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 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); } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24128.html
摘要:以太坊開發高級語言學習。地址以太坊區塊鏈由賬戶組成,你可以把它想象成銀行賬戶。使用很安全,因為它具有以太坊區塊鏈的安全保障除非竊取與以太坊地址相關聯的私鑰,否則是沒有辦法修改其他人的數據的。 以太坊開發高級語言學習。 一、映射(Mapping)和地址(Address) 我們通過給數據庫中的僵尸指定主人, 來支持多玩家模式。 如此一來,我們需要引入2個新的數據類型:mapping(映射)...
摘要:是企業與區塊鏈相遇的地方。的框架旨在成為開發區塊鏈解決方案的支柱。以太坊,主要是針對工程師使用進行區塊鏈以太坊開發的詳解。 如果你想將區塊鏈合并到一個Java項目中,現在我們來看看就是這個細分領域中三個最大的OSS玩家。 好的伙計們,我們都聽說過比特幣,以太坊或其他加密貨幣,其中有一些時髦的名字圍繞著我們常見的新聞,但我們作為Java開發人員知道如何輕松地與這些區塊鏈技術進行交互嗎?以...
摘要:原文發表于以太坊智能合約開發第二篇理解以太坊相關概念很多人都說比特幣是區塊鏈,以太坊是區塊鏈。它是以太坊智能合約的運行環境。是由以太坊節點提供。以太坊社區把基于智能合約的應用稱為去中心化的應用。 原文發表于:以太坊智能合約開發第二篇:理解以太坊相關概念 很多人都說比特幣是區塊鏈1.0,以太坊是區塊鏈2.0。在以太坊平臺上,可以開發各種各樣的去中心化應用,這些應用構成了以太坊的整個生態...
摘要:本文面向以太坊智能合約應用程序開發人員,并討論如何在密碼保護后,安全地運行你的以太坊節點,以便通過進行安全輸出。以太坊,主要是針對工程師使用進行區塊鏈以太坊開發的詳解。 本文面向以太坊智能合約應用程序開發人員,并討論如何在密碼保護后,安全地運行你的以太坊節點,以便通過Internet進行安全輸出。 Go Ethereum(geth)是以太坊節點最受歡迎的軟件。其他流行的以太坊實現是Pa...
摘要:本文面向以太坊智能合約應用程序開發人員,并討論如何在密碼保護后,安全地運行你的以太坊節點,以便通過進行安全輸出。以太坊,主要是針對工程師使用進行區塊鏈以太坊開發的詳解。 本文面向以太坊智能合約應用程序開發人員,并討論如何在密碼保護后,安全地運行你的以太坊節點,以便通過Internet進行安全輸出。 Go Ethereum(geth)是以太坊節點最受歡迎的軟件。其他流行的以太坊實現是Pa...
閱讀 888·2021-11-23 09:51
閱讀 1095·2021-11-15 17:57
閱讀 1671·2021-09-22 15:24
閱讀 816·2021-09-07 09:59
閱讀 2227·2019-08-29 15:10
閱讀 1853·2019-08-29 12:47
閱讀 756·2019-08-29 12:30
閱讀 3374·2019-08-26 13:51