目录
函数修饰符进阶
Payable修饰符
运用
取款 Withdraws
运用
准备好设计僵尸对战
随机数 Random Numbers
通过keccak256生成随机数
此方法容易受到不诚实节点的攻击
如何在以太坊中安全地生成随机数?
运用
僵尸对战
重构公共逻辑
运用
回到攻击函数
僵尸的输赢机制
僵尸获胜机制
僵尸获败机制
函数修饰符进阶
前面学了好多函数修饰符,一下子很难记完,先快速回顾一下吧。
可见性修饰符(控制何时何地可以调用函数):
private意味着它只能从合约内的其他函数调用;
internal类似于private,但也可以由继承自此的合约调用;
external只能在合同外调用;
最后,public可以在内部和外部的任何地方调用。
状态修饰符(告诉我们该函数如何与区块链交互):
view告诉我们,通过运行该函数,不会保存/更改任何数据。
pure告诉我们,该函数不仅不会将任何数据保存到区块链,而且也不会从区块链读取任何数据。如果从合约外部调用这两个函数,它们都不需要花费任何气体来调用(但如果由另一个函数内部调用,它们确实需要花费气体)。
自定义修饰符,例如,前面了解到的:onlyOwner和aboveLevel。对于这些,我们可以定义自定义逻辑来确定它们如何影响函数。
这些修饰符都可以按如下方式堆叠在函数定义上:
function test() external view onlyOwner anotherModifier { /* ... */ }
我们将再引入一个函数修饰符:payable。
Payable修饰符
Payable函数是Solidity和以太坊很酷的一部分——它们是一种可以接收以太(币)的特殊类型。
当你在正常的web服务器上调用API函数时,你不能在函数调用的同时发送美元,也不能发送比特币。
但在以太坊中,由于货币(Ether)、数据(transaction payload)和合约代码本身都存在于以太坊上,因此您可以调用函数并同时向合约付款。
这样就可以用一些非常有趣的逻辑,比如需要向合同支付一定的款项才能执行函数。
让我们看一个示例:
contract OnlineStore {
function buySomething() external payable {
// Check to make sure 0.001 ether was sent to the function call:
require(msg.value == 0.001 ether);
// If so, some logic to transfer the digital item to the caller of the function:
transferThing(msg.sender);
}
}
msg.value是一种查看以太币被发送到合同中的方式,以太币是一个内置单位。
有人会从web3.js(从DApp的JavaScript前端)调用函数,如下所示:
// Assuming `OnlineStore` points to your contract on Ethereum:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})
注意value字段,javascript函数调用指定要发送多少以太(0.001)。如果你把交易想象成一个信封,而你发送给函数调用的参数是你放进去的信的内容,那么添加一个value就像把现金放进信封一样——信和钱一起送到收件人手中。
注意:如果某个功能未标记为payable,并且您试图如上所述向其发送以太币,则该功能将拒绝您的交易。
运用
假设我们的游戏有一个功能,用户可以支付ETH来升级他们的僵尸。ETH将存储在您拥有的合约中-这是一个简单的例子,说明您如何在游戏中赚钱!
contract ZombieHelper is ZombieFeeding {
// 1. Define levelUpFee here
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 2. Insert levelUp function here
function levelUp(uint _zombieId) external payable{
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
取款 Withdraws
您可以编写一个函数从合约中提取Ether,如下所示:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);//转换后才可以使用.transfer
}
}
需要注意的是,您不能将Ether转移到某个地址,除非该地址属于payable地址类型。但是_owner变量的类型为uint160,这意味着我们必须显式地将其转换为地址payable。
您可以使用转账将资金发送到任何以太坊地址。例如,您可以使用一个函数,如果对方为某个项目多付了费用,则将Ether传输回msg.sender:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者,在与买家和卖家使用的合约中,您可以将卖家的地址保存在存储器中,然后当某人购买他的物品时,将买家支付的费用转移给他:seller.transfer(msg.value)。
这些是让以太坊编程变得非常酷的一些例子——你可以拥有像这样不受任何人控制的去中心化市场。
运用
// 1. Create withdraw function here
function withdraw() external onlyOwner{
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
// 2. Create setLevelUpFee function here
function setLevelUpFee(uint _fee) external onlyOwner{
levelUpFee = _fee;
}
准备好设计僵尸对战
让我们回顾一下创建新合约的过程。重复才能掌握!
如果您记不住执行这些操作的语法,请查看zombiehelper.sol的语法-但试着不先偷看来测试你的知识。
在文件顶部声明我们使用的是Solidity version >=0.5.0 <0.6.0。
从zombiehelper.sol导入。
声明从ZombieHelper继承的名为ZombieAttack的新合约,暂时将合同正文留空。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper{
}
随机数 Random Numbers
让我们先搞清楚一下僵尸对战的逻辑。
所有好的游戏都需要一定程度的随机性。那么我们如何在Solidity中生成随机数呢?
实际上真正的随机数是不能安全地生成的。
通过keccak256生成随机数
Solidity中最好的随机性来源是keccak256哈希函数。
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
randNonce++;
uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
这将做的是获取现在的时间戳,即msg.sender和一个递增的nonce(一个只使用一次的数字,因此我们不会使用相同的输入参数运行相同的哈希函数两次)。
然后,它将“打包”输入,并使用keccak将其转换为随机散列。接下来,它将把哈希转换为uint,然后使用%100只取最后2位。这将给我们一个介于0和99之间的完全随机数。
此方法容易受到不诚实节点的攻击
在以太坊中,当您调用合约上的函数时,事务将广播到网络上的一个或多个节点。然后,网络上的节点收集一堆事务,尝试第一个解决计算密集型数学问题,作为“工作证明”,然后将该组事务及其工作证明(PoW)作为块发布给网络的其他部分。
一旦一个节点解决了PoW,其他节点就停止尝试解决PoW,验证其他节点的事务列表是否有效,然后接受该块并继续尝试解决下一个块。 这使得我们的随机数函数是可利用的。
假设我们有一个掷硬币合约-正面你的钱翻倍,反面你失去一切。假设它使用上述随机函数来确定头部或尾部。(随机>=50为头部,随机<50为尾部)。
如果我正在运行一个节点,我只能将事务发布到我自己的节点,而不能共享它。然后我可以运行硬币翻转功能,看看我是否赢了-如果我输了,选择不在我要解决的下一个区块中包含该交易。我可以无限期地这样做,直到我最终赢得了硬币翻转,解决了下一个区块,并获得了利润。
利用个人节点提前知道自己是否赢了。
如何在以太坊中安全地生成随机数?
因为所有参与者都可以看到区块链的全部内容,这是一个难题。你可以阅读这个StackOverflow线程来获得一些想法。一个想法是使用预言机从以太坊区块链外部访问随机数函数。
当然,由于网络上数以万计的以太坊节点正在竞争解决下一个区块,我解决下一块的几率极低。我需要花费大量时间或计算资源才能从中获利,但如果回报足够高(比如我有机会在硬币翻转功能上赚取100000000美元),我就值得去攻击。
因此,虽然这种随机数生成在以太坊上并不安全,但在实践中,除非我们的随机函数在线上有很多钱,否则游戏的用户可能没有足够的资源来攻击它。
运用
实现一个随机数函数,我们可以使用它来确定我们的战斗结果,即使它不完全安全。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
// Start here
uint randNonce = 0;
function randMod(uint _modulus) internal returns(uint){
randNonce++;
return uint(keccak256(abi.encodePacked(now,msg.sender,randNonce))) % _modulus;
}
}
僵尸对战
上面我们已经实现了合约里的随机性,我们可以应用在僵尸对战中计算结果。
我们的僵尸战斗逻辑将如下:
你选择一个僵尸,然后选择对手的僵尸进行攻击。
攻击僵尸有70%的几率获胜。防守的僵尸将有30%的胜算。
所有僵尸(攻击和防御)将有一个winCount和一个lossCount,这将根据战斗的结果而增加。
如果攻击僵尸获胜,它会升级并产生一个新的僵尸。
如果它丢失,则不会发生任何事情(除了它的lossCount递增)。
无论它是赢还是输,攻击僵尸的冷却时间都会被触发。
这是一个需要实现的逻辑,所以我们将在接下来分几部分来实现。
先做个小准备
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
// Create attackVictoryProbability here
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
// Create new function here
function attack(uint _zombieId,uint _targetId) external{
}
}
重构公共逻辑
我们不希望别人能使用我们的僵尸来攻击,因此我们需要确保用户真正拥有他们的僵尸,这涉及到一个安全问题。
如何用一个函数来检查调用该函数的人是否是他提供的_zombieId的所有者?
很简单,前面changeName()等函数中有用过的:
require(msg.sender == zombieToOwner[_zombieId]);
这是我们攻击函数所需要的逻辑。由于我们多次使用相同的逻辑,让我们将其移入自己的修饰符中,以清理代码并避免重复。
运用
创建名为ownerOf的modifier(确保是_zombieId的所有者)。它需要一个参数_zombieId(一个uint)。
记得modifier函数体以 _; 结尾。
// 1. 创建修饰符
modifier ownerOf(uint _zombieId){
require(msg.sender==zombieToOwner[_zombieId]);
_;
}
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);
}
// 2. 在函数定义中添加新建的修饰符(别忘了传参_zombieId):
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal ownerOf(_zombieId){
// 3. 删除下面//这行
//require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
后面把其他几个需要修改的函数changeName()、changeDna() 都加个修饰符即可
回到攻击函数
做好ownerOf修饰符修改后,我们继续写zombieattack.sol中的攻击函数。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
// 1. Add modifier here
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId){
// 2. Start function definition here
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
//0-100的随机数
}
}
僵尸的输赢机制
对于我们的僵尸游戏,我们将要跟踪我们的僵尸赢得和输掉了多少场战斗。这样我们就可以在游戏状态下保持“僵尸排行榜”。
我们可以在DApp中以多种方式存储这些数据——以单个映射、排行榜结构或僵尸结构本身的形式。
根据我们打算如何与数据交互,每种方法都有自己的好处和权衡。为了简单起见,我们将把统计数据存储在Zombie结构中,并将其称为winCount和lossCount。
修改我们的Zombie结构,使其具有另外两个属性:
a、 winCount,一个uint16
b、 lossCount,也是一个uint16
注意:请记住,因为我们可以在结构中打包uint,所以我们希望使用我们可以避免溢出的最小uint。uint8太小了,因为2^8=256-如果我们的僵尸每天攻击一次,他们可能会在一年内溢出。但2^16是65536,因此,除非用户连续179年每天都输赢,否则我们在这里应该是安全的。
既然我们在Zombie结构上有了新的属性,我们需要在_createZombie()中更改函数定义。
更改创建僵尸的定义,使其创建每个新的僵尸,0胜0负。
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
// 1. Add new properties here
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
// 2. Modify new zombie creation here:
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime),0,0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
僵尸获胜机制
现在我们有了winCount和lossCount,我们可以根据哪个僵尸赢得了战斗来更新它们。
前面我们计算了一个从0到100的随机数。现在让我们使用这个数字来确定谁赢得了比赛,并相应地更新我们的统计数据。
前面已经设置了attackVictoryProbability=70,也就是说随机数取在70以内我们的僵尸就算胜利,这时候设置我的僵尸winCount+1,等级+1,敌方僵尸lossCount+1。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
// Start here
if(rand <= attackVictoryProbability){
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
//通过feedAndMultiply运行内部冷却的代码实现我的僵尸冷却,并感染战败僵尸。
}
}
}
僵尸获败机制
在我们的游戏中,当僵尸输掉比赛时,他们不会降低level-只是在lossCount加个1,以及他们的冷却时间被触发,因此他们必须等待一天才能再次进攻。
由于没打过敌方僵尸,所以只有冷却没有感染。
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else{
// start here
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}