摘要:第一个例子,在你把智能协议传上以太坊之后,它就变得不可更改这种永固性意味着你的代码永远不能被调整或更新。允许将合约所有权转让给他人。为何要来驱动以太坊就像一个巨大缓慢但非常安全的电脑。
通过前边的 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个新功能:level 和 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"); } }
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/24135.html
摘要:接上篇文章,这里继续学习高级理论。实战演练我们来写一个返回某玩家的整个僵尸军团的函数。但这样每做一笔交易,都会改变僵尸军团的秩序。在这里开始五可支付截至目前,我们只接触到很少的函数修饰符。 接上篇文章,这里继续学习Solidity高级理论。 一、深入函数修饰符 接下来,我们将添加一些辅助方法。我们为您创建了一个名为 zombiehelper.sol 的新文件,并且将 zombiefee...
摘要:接上篇文章,这里继续学习高级理论。将这个函数的定义修改为其使用修饰符。我们将用一个到的随机数来确定我们的战斗结果。在这个教程中,简单起见我们将这个状态保存在结构体中,将其命名为和。在第六章我们计算出来一个到的随机数。 接上篇文章,这里继续学习Solidity高级理论。 一、重构通用逻辑 不管谁调用我们的 attack 函数 —— 我们想确保用户的确拥有他们用来攻击的僵尸。如果你能用其他...
摘要:接上一节,继续学习高级语法。添加语句,并且将后两位数替换为添加参数四部署以太坊实现实现我们只用编译和部署,就可以将这个合约部署到以太坊了。 接上一节,继续学习solidity高级语法。 一、使用接口 继续前面上一节 NumberInterface 的例子,我们既然将接口定义为: contract NumberInterface { function getNum(address _...
摘要:使用基于以太坊的智能合约的集成开发环境。以太坊教程,主要介绍智能合约与应用开发,适合入门。以太坊,主要是介绍使用进行智能合约开发交互,进行账号创建交易转账代币开发以及过滤器和事件等内容。 Solidity是一种以智能合约为导向的编程语言。这是一种只有四年的年轻语言,旨在帮助开发基于以太坊数字货币的智能合约。 理解它官方文档应该是学习Solidity的最佳来源:solidity.read...
阅读 2479·2023-04-25 17:27
阅读 1790·2019-08-30 15:54
阅读 2334·2019-08-30 13:06
阅读 2943·2019-08-30 11:04
阅读 706·2019-08-29 15:30
阅读 669·2019-08-29 15:16
阅读 1696·2019-08-26 10:10
阅读 3564·2019-08-23 17:02