Solidity与dapp开发学习记录4

目录

函数修饰符进阶

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dwoura犀利

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值