以太坊开发实战学习-高级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");
  }

}
相关文章
相关标签/搜索