以太坊開發實戰學習-高級Solidity理論(四)

經過前邊的 Solidity 基礎語法學習,咱們已經有了Solidity編程經驗,在這節就要學學 Ethereum 開發的技術細節,編寫真正的 DApp 時必知的: 智能協議的全部權Gas的花費代碼優化,和 代碼安全

1、智能協議的永固性

到如今爲止,咱們講的 Solidity 和其餘語言沒有質的區別,它長得也很像 JavaScript.git

可是,在有幾點以太坊上的 DApp 跟普通的應用程序有着天壤之別。算法

第一個例子,在你把智能協議傳上以太坊以後,它就變得不可更改, 這種永固性意味着你的代碼永遠不能被調整或更新。編程

你編譯的程序會一直,永久的,不可更改的,存在以太網上。這就是Solidity代碼的安全性如此重要的一個緣由。若是你的智能協議有任何漏洞,即便你發現了也沒法補救。你只能讓你的用戶們放棄這個智能協議,而後轉移到一個新的修復後的合約上。安全

但這剛好也是智能合約的一大優點。 代碼說明一切。 若是你去讀智能合約的代碼,並驗證它,你會發現, 一旦函數被定義下來,每一次的運行,程序都會嚴格遵守函數中原有的代碼邏輯一絲不苟地執行,徹底不用擔憂函數被人篡改而獲得意外的結果。網絡

外部依賴關係

在上邊的文章中,咱們將加密小貓(CryptoKitties)合約的地址硬編碼到DApp中去了。有沒有想過,若是加密小貓出了點問題,比方說,集體消失了會怎麼樣? 雖然這種事情幾乎不可能發生,可是,若是小貓沒了,咱們的 DApp 也會隨之失效 -- 由於咱們在 DApp 的代碼中用「硬編碼」的方式指定了加密小貓的地址,若是這個根據地址找不到小貓,咱們的殭屍也就吃不到小貓了,而按照前面的描述,咱們卻無法修改合約去應付這個變化!app

所以,咱們不能硬編碼,而要採用「函數」,以便於 DApp 的關鍵部分能夠以參數形式修改。dom

比方說,咱們再也不一開始就把獵物地址給寫入代碼,而是寫個函數 setKittyContractAddress, 運行時再設定獵物的地址,這樣咱們就能夠隨時去鎖定新的獵物,也不用擔憂加密小貓集體消失了。編程語言

實戰演練

請修改前邊的代碼,使得能夠經過程序更改CryptoKitties合約地址。ide

  • 一、刪除採用硬編碼 方式的 ckAddress 代碼行。
  • 二、以前建立 kittyContract 變量的那行代碼,修改成對 kittyContract 變量的聲明 -- 暫時不給它指定具體的實例。
  • 三、建立名爲 setKittyContractAddress 的函數, 它帶一個參數 _address(address類型), 可見性設爲external。
  • 四、在函數內部,添加一行代碼,將 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");
  }

}

2、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 合約基本都會這麼幹:

  • 一、合約建立,構造函數先行,將其 owner 設置爲msg.sender(其部署者)
  • 二、爲它加上一個修飾符 onlyOwner,它會限制陌生人的訪問,將訪問某些函數的權限鎖定在 owner 上。
  • 三、容許將合約全部權轉讓給他人。

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);
    }

}

3、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 的訪問,除了咱們本身,誰都沒法去修改它。

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

}

4、Gas

如今咱們懂了如何在禁止第三方修改咱們的合約的同時,留個後門給我們本身去修改。

讓咱們來看另外一種使得 Solidity 編程語言不同凡響的特徵:

Gas-驅動以太坊DApps的能源

在 Solidity 中,你的用戶想要每次執行你的 DApp 都須要支付必定的 gas,gas 能夠用以太幣購買,所以,用戶每次跑 DApp 都得花費以太幣

一個 DApp 收取多少 gas 取決於功能邏輯的複雜程度。每一個操做背後,都在計算完成這個操做所須要的計算資源,(好比,存儲數據就比作個加法運算貴得多), 一次操做所須要花費的 gas 等於這個操做背後的全部運算花銷的總和。

因爲運行你的程序須要花費用戶的真金白銀,在以太坊中代碼的編程語言,比其餘任何編程語言都更強調優化。一樣的功能,使用笨拙的代碼開發的程序,比起通過精巧優化的代碼來,運行花費更高,這顯然會給成千上萬的用戶帶來大量沒必要要的開銷。

爲什麼要gas來驅動?

以太坊就像一個巨大、緩慢、但很是安全的電腦。當你運行一個程序的時候,網絡上的每個節點都在進行相同的運算,以驗證它的輸出 —— 這就是所謂的」去中心化「 因爲數以千計的節點同時在驗證着每一個功能的運行,這能夠確保它的數據不會被被監控,或者被刻意修改。

可能會有用戶用無限循環堵塞網絡,抑或用密集運算來佔用大量的網絡資源,爲了防止這種事情的發生,以太坊的建立者爲以太坊上的資源制定了價格,想要在以太坊上運算或者存儲,你須要先付費

注意:若是你使用 側鏈,卻是不必定須要付費,好比我們在 Loom Network 上構建的 CryptoZombies 就免費。你不會想要在以太坊主網上玩兒「魔獸世界」吧? - 所須要的 gas 可能會買到你破產。可是你能夠找個算法理念不一樣的側鏈來玩它。咱們將在之後的課程中我們會討論到,什麼樣的 DApp 應該部署在太坊主鏈上,什麼又最好放在側鏈。

省gas的招數

省 gas 的招數:結構封裝(Struct packing)

在第1課中,咱們提到除了基本版的 uint 外,還有其餘變種 uintuint8uint16uint32等。

一般狀況下咱們不會考慮使用 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​​velreadyTime - 後者是用來實現一個「冷卻定時器」,以限制殭屍獵食的頻率。

讓咱們回到 zombiefactory.sol

  • 一、爲 Zombie 結構體 添加兩個屬性:leveluint32)和readyTimeuint32)。由於但願同類型數據打成一個包,因此把它們放在結構體的末尾。

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);
    }

}

5、時間單位

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天。

  • 一、聲明一個名爲 cooldownTimeuint,並將其設置爲 1 days。(沒錯,」1 days「使用了複數, 不然通不過編譯器)
  • 二、由於在上一章中咱們給 Zombie 結構體中添加 levelreadyTime 兩個參數,因此如今建立一個新的 Zombie 結構體時,須要修改 _createZombie(),在其中把新舊參數都初始化一下。
  • 三、修改 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);
    }

}

6、時間週期定時器

如今,Zombie 結構體中定義好了一個 readyTime 屬性,讓咱們跳到 zombiefeeding.sol, 去實現一個」冷卻週期定時器「。

按照如下步驟修改 feedAndMultiply

  • 一、」捕獵「行爲會觸發殭屍的」冷卻週期「
  • 二、殭屍在這段」冷卻週期「結束前不可再捕獵小貓

這將限制殭屍,防止其無限制地捕獵小貓或者成天不停地繁殖。未來,當咱們增長戰鬥功能時,咱們一樣用」冷卻週期「限制殭屍之間打鬥的頻率。

首先,咱們要定義一些輔助函數,設置並檢查殭屍的 readyTime。

將結構體做爲參數傳入
因爲結構體的存儲指針能夠以參數的方式傳遞給一個 private 或 internal 的函數,所以結構體能夠在多個函數之間相互傳遞。

遵循這樣的語法:

function _doStuff(Zombie storage _zombie) internal {
  // do stuff with _zombie
}

這樣咱們能夠將某殭屍的引用直接傳遞給一個函數,而不用是經過參數傳入殭屍ID後,函數再依據ID去查找。

實戰演練

  • 一、先定義一個 _triggerCooldown 函數。它要求一個參數,_zombie,表示一某個殭屍的存儲指針。這個函數可見性設置爲 internal
  • 二、在函數中,把 _zombie.readyTime 設置爲 uint32(now + cooldownTime)
  • 三、接下來,建立一個名爲 _isReady 的函數。這個函數的參數也是名爲 _zombie 的 Zombie storage。這個功能只具備 internal 可見性,並返回一個 bool 值。
  • 四、函數計算返回(_zombie.readyTime <= now),值爲 truefalse。這個功能的目的是判斷下次容許獵食的時間是否已經到了。
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");
  }

}

7、公有函數和安全性

如今來修改 feedAndMultiply ,實現冷卻週期。

回顧一下這個函數,前一課上咱們將其可見性設置爲public。你必須仔細地檢查全部聲明爲 publicexternal的函數,一個個排除用戶濫用它們的可能,謹防安全漏洞。請記住,若是這些函數沒有相似 onlyOwner 這樣的函數修飾符,用戶能利用各類可能的參數去調用它們。

檢查完這個函數,用戶就能夠直接調用這個它,並傳入他們所但願的 _targetDnaspecies 。打個遊戲還得遵循這麼多的規則,還能不能愉快地玩耍啊!

仔細觀察,這個函數只需被 feedOnKitty() 調用,所以,想要防止漏洞,最簡單的方法就是設其可見性爲 internal

實戰演練

  • 一、目前函數 feedAndMultiply 可見性爲 public。咱們將其改成 internal 以保障合約安全。由於咱們不但願用戶調用它的時候塞進一堆亂七八糟的 DNA。
  • 二、feedAndMultiply 過程須要參考 cooldownTime。首先,在找到 myZombie 以後,添加一個 require 語句來檢查 _isReady() 並將 myZombie 傳遞給它。這樣用戶必須等到殭屍的 冷卻週期 結束後才能執行 feedAndMultiply 功能。
  • 三、在函數結束時,調用 _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");
  }

}
相關文章
相關標籤/搜索