摘要:第一個例子,在你把智能協議傳上以太坊之后,它就變得不可更改這種永固性意味著你的代碼永遠不能被調整或更新。允許將合約所有權轉讓給他人。為何要來驅動以太坊就像一個巨大緩慢但非常安全的電腦。
通過前邊的 Solidity 基礎語法學習,我們已經有了Solidity編程經驗,在這節就要學學 Ethereum 開發的技術細節,編寫真正的 DApp 時必知的:智能協議的所有權,Gas的花費,代碼優化,和代碼安全。一、智能協議的永固性
到現在為止,我們講的 Solidity 和其他語言沒有質的區別,它長得也很像 JavaScript.
但是,在有幾點以太坊上的 DApp 跟普通的應用程序有著天壤之別。
第一個例子,在你把智能協議傳上以太坊之后,它就變得不可更改, 這種永固性意味著你的代碼永遠不能被調整或更新。
你編譯的程序會一直,永久的,不可更改的,存在以太網上。這就是Solidity代碼的安全性如此重要的一個原因。如果你的智能協議有任何漏洞,即使你發現了也無法補救。你只能讓你的用戶們放棄這個智能協議,然后轉移到一個新的修復后的合約上。
但這恰好也是智能合約的一大優勢。 代碼說明一切。 如果你去讀智能合約的代碼,并驗證它,你會發現, 一旦函數被定義下來,每一次的運行,程序都會嚴格遵照函數中原有的代碼邏輯一絲不茍地執行,完全不用擔心函數被人篡改而得到意外的結果。
外部依賴關系在上邊的文章中,我們將加密小貓(CryptoKitties)合約的地址硬編碼到DApp中去了。有沒有想過,如果加密小貓出了點問題,比方說,集體消失了會怎么樣? 雖然這種事情幾乎不可能發生,但是,如果小貓沒了,我們的 DApp 也會隨之失效 -- 因為我們在 DApp 的代碼中用“硬編碼”的方式指定了加密小貓的地址,如果這個根據地址找不到小貓,我們的僵尸也就吃不到小貓了,而按照前面的描述,我們卻沒法修改合約去應付這個變化!
因此,我們不能硬編碼,而要采用“函數”,以便于 DApp 的關鍵部分可以以參數形式修改。
比方說,我們不再一開始就把獵物地址給寫入代碼,而是寫個函數 setKittyContractAddress, 運行時再設定獵物的地址,這樣我們就可以隨時去鎖定新的獵物,也不用擔心加密小貓集體消失了。
實戰演練請修改前邊的代碼,使得可以通過程序更改CryptoKitties合約地址。
1、刪除采用硬編碼 方式的 ckAddress 代碼行。
2、之前創建 kittyContract 變量的那行代碼,修改為對 kittyContract 變量的聲明 -- 暫時不給它指定具體的實例。
3、創建名為 setKittyContractAddress 的函數, 它帶一個參數 _address(address類型), 可見性設為external。
4、在函數內部,添加一行代碼,將 kittyContract 變量設置為返回值:KittyInterface(_address)。
注意:你可能會注意到這個功能有個安全漏洞,別擔心 - 咱們到下一章里解決它;)
zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { // 1. 移除這一行: // address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d; // 2. 只聲明變量: // KittyInterface kittyContract = KittyInterface(ckAddress); KittyInterface kittyContract; // 3. 增加 setKittyContractAddress 方法 function setKittyContractAddress(address _address) external { kittyContract = KittyInterface(_address); } function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } }二、Ownable Contracts
上面代碼中,您有沒有發現任何安全漏洞呢?
呀!setKittyContractAddress 可見性居然申明為“外部的”(external),豈不是任何人都可以調用它! 也就是說,任何調用該函數的人都可以更改 CryptoKitties 合約的地址,使得其他人都沒法再運行我們的程序了。
我們確實是希望這個地址能夠在合約中修改,但我可沒說讓每個人去改它呀。
要對付這樣的情況,通常的做法是指定合約的“所有權” - 就是說,給它指定一個主人(沒錯,就是您),只有主人對它享有特權。Ownable
下面是一個 Ownable 合約的例子: 來自 OpenZeppelin Solidity 庫的 Ownable 合約。 OpenZeppelin 是主打安保和社區審查的智能合約庫,您可以在自己的 DApps中引用。等把這一課學完,您不要催我們發布下一課,最好利用這個時間把 OpenZeppelin 的網站看看,保管您會學到很多東西!
把樓下這個合約讀讀通,是不是還有些沒見過代碼?別擔心,我們隨后會解釋。
/** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ function Ownable() public { owner = msg.sender; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner); _; } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0)); OwnershipTransferred(owner, newOwner); owner = newOwner; } }
下面有沒有您沒學過的東東?
構造函數:function Ownable()是一個 constructor (構造函數),構造函數不是必須的,它與合約同名,構造函數一生中唯一的一次執行,就是在合約最初被創建的時候。
函數修飾符:modifier onlyOwner()。 修飾符跟函數很類似,不過是用來修飾其他已有函數用的, 在其他語句執行前,為它檢查下先驗條件。 在這個例子中,我們就可以寫個修飾符 onlyOwner 檢查下調用者,確保只有合約的主人才能運行本函數。我們下一章中會詳細講述修飾符,以及那個奇怪的_;。
indexed 關鍵字:別擔心,我們還用不到它。
所以 Ownable 合約基本都會這么干:
1、合約創建,構造函數先行,將其 owner 設置為msg.sender(其部署者)
2、為它加上一個修飾符 onlyOwner,它會限制陌生人的訪問,將訪問某些函數的權限鎖定在 owner 上。
3、允許將合約所有權轉讓給他人。
onlyOwner 簡直人見人愛,大多數人開發自己的 Solidity DApps,都是從復制/粘貼 Ownable 開始的,從它再繼承出的子類,并在之上進行功能開發。
既然我們想把 setKittyContractAddress 限制為 onlyOwner ,我們也要做同樣的事情。
實戰演練首先,將 Ownable 合約的代碼復制一份到新文件 ownable.sol 中。 接下來,創建一個 ZombieFactory,繼承 Ownable。
1.在程序中導入 ownable.sol 的內容。 如果您不記得怎么做了,參考下 zombiefeeding.sol。
2.修改 ZombieFactory 合約, 讓它繼承自 Ownable。 如果您不記得怎么做了,看看 zombiefeeding.sol。
ownable.sol 文件:
/** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ function Ownable() public { owner = msg.sender; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner); _; } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0)); OwnershipTransferred(owner, newOwner); owner = newOwner; } }
zombiefactory.sol
pragma solidity ^0.4.19; // 1. 在這里導入 import "./ownable.sol"; // 2. 在這里繼承: contract ZombieFactory is Ownable{ event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } 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; 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); } }三、onlyOwner函數修飾符
現在我們有了個基本版的合約 ZombieFactory 了,它繼承自 Ownable 接口,我們也可以給 ZombieFeeding 加上 onlyOwner 函數修飾符。
這就是合約繼承的工作原理。記得:
ZombieFeeding 是個 ZombieFactory ZombieFactory 是個 Ownable函數修飾符modifier
函數修飾符看起來跟函數沒什么不同,不過關鍵字modifier 告訴編譯器,這是個modifier(修飾符),而不是個function(函數)。它不能像函數那樣被直接調用,只能被添加到函數定義的末尾,用以改變函數的行為。
再仔細讀讀 onlyOwner:
/** * @dev 調用者不是‘主人’,就會拋出異常 */ modifier onlyOwner() { require(msg.sender == owner); _; }
onlyOwner 函數修飾符是這么用的:
contract MyContract is Ownable { event LaughManiacally(string laughter); //注意! `onlyOwner`上場 : function likeABoss() external onlyOwner { LaughManiacally("Muahahahaha"); } }
注意 likeABoss 函數上的 onlyOwner 修飾符。 當你調用 likeABoss 時,首先執行 onlyOwner 中的代碼, 執行到 onlyOwner 中的_; 語句時,程序再返回并執行 likeABoss 中的代碼。
可見,盡管函數修飾符也可以應用到各種場合,但最常見的還是放在函數執行之前添加快速的 require 檢查。
因為給函數添加了修飾符 onlyOwner,使得唯有合約的主人(也就是部署者)才能調用它。
注意:主人對合約享有的特權當然是正當的,不過也可能被惡意使用。比如,萬一,主人添加了個后門,允許他偷走別人的僵尸呢?實戰演練所以非常重要的是,部署在以太坊上的 DApp,并不能保證它真正做到去中心,你需要閱讀并理解它的源代碼,才能防止其中沒有被部署者惡意植入后門;作為開發人員,如何做到既要給自己留下修復 bug 的余地,又要盡量地放權給使用者,以便讓他們放心你,從而愿意把數據放在你的 DApp 中,這確實需要個微妙的平衡。
現在我們可以限制第三方對 setKittyContractAddress 的訪問,除了我們自己,誰都無法去修改它。
1、將 onlyOwner 函數修飾符添加到 setKittyContractAddress
中。
zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; // 修改這個函數,添加權限onlyOwner function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } }四、Gas
現在我們懂了如何在禁止第三方修改我們的合約的同時,留個后門給咱們自己去修改。
讓我們來看另一種使得 Solidity 編程語言與眾不同的特征:
Gas-驅動以太坊DApps的能源在 Solidity 中,你的用戶想要每次執行你的 DApp 都需要支付一定的 gas,gas 可以用以太幣購買,因此,用戶每次跑 DApp 都得花費以太幣。
一個 DApp 收取多少 gas 取決于功能邏輯的復雜程度。每個操作背后,都在計算完成這個操作所需要的計算資源,(比如,存儲數據就比做個加法運算貴得多), 一次操作所需要花費的 gas 等于這個操作背后的所有運算花銷的總和。
由于運行你的程序需要花費用戶的真金白銀,在以太坊中代碼的編程語言,比其他任何編程語言都更強調優化。同樣的功能,使用笨拙的代碼開發的程序,比起經過精巧優化的代碼來,運行花費更高,這顯然會給成千上萬的用戶帶來大量不必要的開銷。
為何要gas來驅動?以太坊就像一個巨大、緩慢、但非常安全的電腦。當你運行一個程序的時候,網絡上的每一個節點都在進行相同的運算,以驗證它的輸出 —— 這就是所謂的”去中心化“ 由于數以千計的節點同時在驗證著每個功能的運行,這可以確保它的數據不會被被監控,或者被刻意修改。
可能會有用戶用無限循環堵塞網絡,抑或用密集運算來占用大量的網絡資源,為了防止這種事情的發生,以太坊的創建者為以太坊上的資源制定了價格,想要在以太坊上運算或者存儲,你需要先付費。
注意:如果你使用側鏈,倒是不一定需要付費,比如咱們在 Loom Network 上構建的 CryptoZombies 就免費。你不會想要在以太坊主網上玩兒“魔獸世界”吧? - 所需要的 gas 可能會買到你破產。但是你可以找個算法理念不同的側鏈來玩它。我們將在以后的課程中咱們會討論到,什么樣的 DApp 應該部署在太坊主鏈上,什么又最好放在側鏈。省gas的招數 省 gas 的招數:結構封裝(Struct packing)
在第1課中,我們提到除了基本版的 uint 外,還有其他變種 uint:uint8,uint16,uint32等。
通常情況下我們不會考慮使用 uint 變種,因為無論如何定義 uint的大小,Solidity 為它保留256位的存儲空間。例如,使用 uint8 而不是uint(uint256)不會為你節省任何 gas。
除非,把 uint 綁定到 struct 里面。
如果一個 struct 中有多個 uint,則盡可能使用較小的 uint, Solidity 會將這些 uint 打包在一起,從而占用較少的存儲空間。例如:
struct NormalStruct { uint a; uint b; uint c; } struct MiniMe { uint32 a; uint32 b; uint c; } // 因為使用了結構打包,`mini` 比 `normal` 占用的空間更少 NormalStruct normal = NormalStruct(10, 20, 30); MiniMe mini = MiniMe(10, 20, 30);
所以,當 uint 定義在一個 struct 中的時候,盡量使用最小的整數子類型以節約空間。 并且把同樣類型的變量放一起(即在 struct 中將把變量按照類型依次放置),這樣 Solidity 可以將存儲空間最小化。例如,有兩個 struct:
uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因為前者把uint32放一起了。
實戰演練咱們給僵尸添2個新功能:le??vel 和 readyTime - 后者是用來實現一個“冷卻定時器”,以限制僵尸獵食的頻率。
讓我們回到 zombiefactory.sol。
1、為 Zombie 結構體 添加兩個屬性:level(uint32)和readyTime(uint32)。因為希望同類型數據打成一個包,所以把它們放在結構體的末尾。
32位足以保存僵尸的級別和時間戳了,這樣比起使用普通的uint(256位),可以更緊密地封裝數據,從而為我們省點 gas。
zombiefactory.sol
pragma solidity ^0.4.19; import "./ownable.sol"; contract ZombieFactory is Ownable { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; // 在這里添加數據 uint32 level; uint32 readyTime; } 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; 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); } }五、時間單位
level 屬性表示僵尸的級別。以后,在我們創建的戰斗系統中,打勝仗的僵尸會逐漸升級并獲得更多的能力。
readyTime 稍微復雜點。我們希望增加一個“冷卻周期”,表示僵尸在兩次獵食或攻擊之之間必須等待的時間。如果沒有它,僵尸每天可能會攻擊和繁殖1,000次,這樣游戲就太簡單了。
為了記錄僵尸在下一次進擊前需要等待的時間,我們使用了 Solidity 的時間單位。
時間單位Solidity 使用自己的本地時間單位。
變量 now 將返回當前的unix時間戳(自1970年1月1日以來經過的秒數)。我寫這句話時 unix 時間是 1515527488。
注意:Unix時間傳統用一個32位的整數進行存儲。這會導致“2038年”問題,當這個32位的unix時間戳不夠用,產生溢出,使用這個時間的遺留系統就麻煩了。所以,如果我們想讓我們的 DApp 跑夠20年,我們可以使用64位整數表示時間,但為此我們的用戶又得支付更多的 gas。真是個兩難的設計啊!
Solidity 還包含秒(seconds),分鐘(minutes),小時(hours),天(days),周(weeks) 和 年(years) 等時間單位。它們都會轉換成對應的秒數放入 uint 中。所以 1分鐘 就是 60,1小時是 3600(60秒×60分鐘),1天是86400(24小時×60分鐘×60秒),以此類推。
下面是一些使用時間單位的實用案例:
uint lastUpdated; // 將‘上次更新時間’ 設置為 ‘現在’ function updateTimestamp() public { lastUpdated = now; } // 如果到上次`updateTimestamp` 超過5分鐘,返回 "true" // 不到5分鐘返回 "false" function fiveMinutesHavePassed() public view returns (bool) { return (now >= (lastUpdated + 5 minutes)); }
有了這些工具,我們可以為僵尸設定”冷靜時間“功能
實戰演練現在咱們給DApp添加一個“冷卻周期”的設定,讓僵尸兩次攻擊或捕獵之間必須等待 1天。
1、聲明一個名為 cooldownTime 的uint,并將其設置為 1 days。(沒錯,”1 days“使用了復數, 否則通不過編譯器)
2、因為在上一章中我們給 Zombie 結構體中添加 level 和 readyTime 兩個參數,所以現在創建一個新的 Zombie 結構體時,需要修改 _createZombie(),在其中把新舊參數都初始化一下。
3、修改 zombies.push 那一行, 添加加2個參數:1(表示當前的 level )和uint32(now + cooldownTime 現在+冷靜時間)(表示下次允許攻擊的時間 readyTime)。
注意:必須使用 uint32(...) 進行強制類型轉換,因為 now 返回類型 uint256。所以我們需要明確將它轉換成一個 uint32 類型的變量。
now + cooldownTime 將等于當前的unix時間戳(以秒為單位)加上”1天“里的秒數 - 這將等于從現在起1天后的unix時間戳。然后我們就比較,看看這個僵尸的 readyTime是否大于 now,以決定再次啟用僵尸的時機有沒有到來。
下一節中,我們將討論如何通過 readyTime 來規范僵尸的行為。
zombiefactory.sol
pragma solidity ^0.4.19; import "./ownable.sol"; contract ZombieFactory is Ownable { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; // 1. 在這里定義 `cooldownTime` uint cooldownTime = 1 days; struct Zombie { string name; uint dna; uint32 level; uint32 readyTime; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string _name, uint _dna) internal { // 2. 修改下面這行: uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 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); } }六、時間周期定時器
現在,Zombie 結構體中定義好了一個 readyTime 屬性,讓我們跳到 zombiefeeding.sol, 去實現一個”冷卻周期定時器“。
按照以下步驟修改 feedAndMultiply:
1、”捕獵“行為會觸發僵尸的”冷卻周期“
2、僵尸在這段”冷卻周期“結束前不可再捕獵小貓
這將限制僵尸,防止其無限制地捕獵小貓或者整天不停地繁殖。將來,當我們增加戰斗功能時,我們同樣用”冷卻周期“限制僵尸之間打斗的頻率。
首先,我們要定義一些輔助函數,設置并檢查僵尸的 readyTime。
將結構體作為參數傳入
由于結構體的存儲指針可以以參數的方式傳遞給一個 private 或 internal 的函數,因此結構體可以在多個函數之間相互傳遞。
遵循這樣的語法:
function _doStuff(Zombie storage _zombie) internal { // do stuff with _zombie }
這樣我們可以將某僵尸的引用直接傳遞給一個函數,而不用是通過參數傳入僵尸ID后,函數再依據ID去查找。
實戰演練1、先定義一個 _triggerCooldown 函數。它要求一個參數,_zombie,表示一某個僵尸的存儲指針。這個函數可見性設置為 internal。
2、在函數中,把 _zombie.readyTime 設置為 uint32(now + cooldownTime)。
3、接下來,創建一個名為 _isReady 的函數。這個函數的參數也是名為 _zombie 的 Zombie storage。這個功能只具有 internal 可見性,并返回一個 bool 值。
4、函數計算返回(_zombie.readyTime <= now),值為 true 或 false。這個功能的目的是判斷下次允許獵食的時間是否已經到了。
pragma solidity ^0.4.19; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } // 1. 在這里定義 `_triggerCooldown` 函數 function _triggerCooldown(Zombie storage _zombie) internal { _zombie.readyTime = uint32(now + cooldownTime); } // 2. 在這里定義 `_isReady` 函數 function _isReady(Zombie storage _zombie) internal view returns (bool) { return (_zombie.readyTime <= now); } function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } }七、公有函數和安全性
現在來修改 feedAndMultiply ,實現冷卻周期。
回顧一下這個函數,前一課上我們將其可見性設置為public。你必須仔細地檢查所有聲明為 public 和 external的函數,一個個排除用戶濫用它們的可能,謹防安全漏洞。請記住,如果這些函數沒有類似 onlyOwner 這樣的函數修飾符,用戶能利用各種可能的參數去調用它們。
檢查完這個函數,用戶就可以直接調用這個它,并傳入他們所希望的 _targetDna 或 species 。打個游戲還得遵循這么多的規則,還能不能愉快地玩耍??!
仔細觀察,這個函數只需被 feedOnKitty() 調用,因此,想要防止漏洞,最簡單的方法就是設其可見性為 internal。
實戰演練1、目前函數 feedAndMultiply 可見性為 public。我們將其改為 internal 以保障合約安全。因為我們不希望用戶調用它的時候塞進一堆亂七八糟的 DNA。
2、feedAndMultiply 過程需要參考 cooldownTime。首先,在找到 myZombie 之后,添加一個 require 語句來檢查 _isReady() 并將 myZombie 傳遞給它。這樣用戶必須等到僵尸的 冷卻周期 結束后才能執行 feedAndMultiply 功能。
3、在函數結束時,調用 _triggerCooldown(myZombie),標明捕獵行為觸發了僵尸新的冷卻周期。
zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } function _triggerCooldown(Zombie storage _zombie) internal { _zombie.readyTime = uint32(now + cooldownTime); } function _isReady(Zombie storage _zombie) internal view returns (bool) { return (_zombie.readyTime <= now); } // 1. 使這個函數的可見性為 internal function feedAndMultiply(uint _zombieId, uint _targetDna, string species) internal { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; // 2. 在這里為 `_isReady` 增加一個檢查 require(_isReady(myZombie)); _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); // 3. 調用 `triggerCooldown` _triggerCooldown(myZombie); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24135.html
摘要:接上篇文章,這里繼續學習高級理論。實戰演練我們來寫一個返回某玩家的整個僵尸軍團的函數。但這樣每做一筆交易,都會改變僵尸軍團的秩序。在這里開始五可支付截至目前,我們只接觸到很少的函數修飾符。 接上篇文章,這里繼續學習Solidity高級理論。 一、深入函數修飾符 接下來,我們將添加一些輔助方法。我們為您創建了一個名為 zombiehelper.sol 的新文件,并且將 zombiefee...
摘要:接上篇文章,這里繼續學習高級理論。將這個函數的定義修改為其使用修飾符。我們將用一個到的隨機數來確定我們的戰斗結果。在這個教程中,簡單起見我們將這個狀態保存在結構體中,將其命名為和。在第六章我們計算出來一個到的隨機數。 接上篇文章,這里繼續學習Solidity高級理論。 一、重構通用邏輯 不管誰調用我們的 attack 函數 —— 我們想確保用戶的確擁有他們用來攻擊的僵尸。如果你能用其他...
摘要:接上一節,繼續學習高級語法。添加語句,并且將后兩位數替換為添加參數四部署以太坊實現實現我們只用編譯和部署,就可以將這個合約部署到以太坊了。 接上一節,繼續學習solidity高級語法。 一、使用接口 繼續前面上一節 NumberInterface 的例子,我們既然將接口定義為: contract NumberInterface { function getNum(address _...
摘要:使用基于以太坊的智能合約的集成開發環境。以太坊教程,主要介紹智能合約與應用開發,適合入門。以太坊,主要是介紹使用進行智能合約開發交互,進行賬號創建交易轉賬代幣開發以及過濾器和事件等內容。 Solidity是一種以智能合約為導向的編程語言。這是一種只有四年的年輕語言,旨在幫助開發基于以太坊數字貨幣的智能合約。 理解它官方文檔應該是學習Solidity的最佳來源:solidity.read...
閱讀 2521·2023-04-25 17:27
閱讀 1830·2019-08-30 15:54
閱讀 2374·2019-08-30 13:06
閱讀 2985·2019-08-30 11:04
閱讀 751·2019-08-29 15:30
閱讀 732·2019-08-29 15:16
閱讀 1735·2019-08-26 10:10
閱讀 3607·2019-08-23 17:02