Solidity与dapp开发学习记录3(初级=》中级)

目录

合约的不可变性 Immutability

外部依赖 External dependencies

受掌管的合约 Ownable Contracts

运用

onlyOwner 函数修饰符

燃料 Gas

节省Gas

运用

时间单位 Time Units

运用

僵尸的冷却时间

公共函数与安全性

更多有关函数修饰符

自定义修饰符的运用

用“view” Functions来节省Gas

存储storage是很昂贵的

在内存memory中声明数组 

运用

For循环应用场景

运用

回顾一下


合约的不可变性 Immutability

Solidity看起来和java很相似,但是还有很多不一样的地方。其中最突出的一点就是合约的不可变性。一旦在链上部署,合约代码就不可更改,换句话说,合约就是法律。如果有更改的需求,则需要重新部署更换合约地址了。

外部依赖 External dependencies

前篇,我们将CryptoKitties合约地址硬编码到DApp中。但是,如果CryptoKitties合约出现bug,有人摧毁了所有的小猫,会发生什么?
这不太可能,但如果真的发生了,这将使我们的DApp完全无用——我们的DApp将指向一个不再返回任何kitty的硬编码地址。我们的僵尸将无法以小猫为食,我们也无法修改合约来修复它。
出于这个原因,拥有允许您更新DApp关键部分的功能通常是有意义的。
例如,我们不应该将CryptoKitties合约地址硬编码到我们的DApp中,我们可能应该有一个setKittyContractAddress函数,在CryptoKitty合约出现问题时,该函数允许我们在将来更改该地址。

pragma solidity >=0.5.0 <0.6.0;

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. Remove this:
  
  // 2. Change this to just a declaration:
  KittyInterface kittyContract;

  // 3. Add setKittyContractAddress method here
  function setKittyContractAddress(address _address) external {
    kittyContract = KittyInterface(_address);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _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);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

受掌管的合约 Ownable Contracts

前面学的合约不安全,需要有一个合约掌管者owner专门控制合约,这才相对更安全。

OpenZeppelin's Ownable contract

OpenZeppelin是一个安全且经过社区审核的智能合约库,您可以在自己的DApp中使用。强烈建议查看他们的网站以进一步学习!

下面是 取自OpenZeppelin Solidity图书馆的Ownable合约代码样例

/**
 * @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 private _owner;

  event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
  );

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  constructor() internal {
    _owner = msg.sender;
    emit OwnershipTransferred(address(0), _owner);
  }

  /**
   * @return the address of the owner.
   */
  function owner() public view returns(address) {
    return _owner;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(isOwner());
    _;
  }

  /**
   * @return true if `msg.sender` is the owner of the contract.
   */
  function isOwner() public view returns(bool) {
    return msg.sender == _owner;
  }

  /**
   * @dev Allows the current owner to relinquish control of the contract.
   * @notice Renouncing to ownership will leave the contract without an owner.
   * It will not be possible to call the functions with the `onlyOwner`
   * modifier anymore.
   */
  function renounceOwnership() public onlyOwner {
    emit OwnershipTransferred(_owner, address(0));
    _owner = address(0);
  }

  /**
   * @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 {
    _transferOwnership(newOwner);
  }

  /**
   * @dev Transfers control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function _transferOwnership(address newOwner) internal {
    require(newOwner != address(0));
    emit OwnershipTransferred(_owner, newOwner);
    _owner = newOwner;
  }
}

构造函数:constructor()是一个构造函数,它是一个可选的特殊函数,与合约同名。它只会在合约首次创建时执行一次。

函数修饰符:修饰符onlyOwner()。修饰符是一种半函数,用于修改其他函数,通常用于在执行之前检查某些需求。在这种情况下,只能使用owner来限制访问,因此只有合约的所有者才能运行此功能。

因此,Ownable Contracts基本上包括以下内容:
1.创建合约时,其构造函数将所有者设置为msg.sender(部署它的人)
2.它添加了一个onlyOwner修饰符,可以将对某些函数的访问限制为只有所有者
3.它允许您将合约转让给新所有者

运用

设置

pragma solidity >=0.5.0 <0.6.0;

// 1. Import here
import "./ownable.sol";
// 2. Inherit here:
contract ZombieFactory is Ownable{

onlyOwner 函数修饰符

我们的合约ZombieFactory继承自Ownable,我们也可以在ZombieFeeding中使用onlyOwner函数修饰符。

  // Modify this function:
  function setKittyContractAddress(address _address) external onlyOwner{
    kittyContract = KittyInterface(_address);
  }

燃料 Gas

在Solidity中,执行一个函数时都必须付费。执行一个函数需要多少Gas取决于该函数的逻辑有多复杂。每个操作的Gas成本大致取决于执行该操作需要多少计算资源(例如,写入存储比添加两个整数要昂贵得多)。功能的总燃气成本是其所有单独操作的燃气成本之和。

因为运行函数会为用户带来成本,所以代码优化在以太坊中比在其他编程语言中更重要。

节省Gas

使用uint8而不是uint(uint256)不会节省任何Gas,因为Solidity保留256位存储空间。

但有一个例外:内部结构。

如果在一个结构中有多个uint,则在可能的情况下使用较小大小的uint将允许Solidity将这些变量打包在一起以占用较少的存储空间。例如:

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// `mini` will cost less gas than `normal` because of struct packing
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30); 

出于这个原因,在结构中,尽可能用小的整数子类型。

您还需要将相同的数据类型聚集在一起(即将它们放在结构中彼此相邻的位置),以便Solidity可以最小化所需的存储空间。例如,具有字段uint c的结构;uint32 a;uint32 b;将比具有字段uint32 a的结构消耗更少的气体;uint c;uint32 b;

运用

    struct Zombie {
        string name;
        uint dna;
        // Add new data here
        uint32 level;
        uint32 readyTime;
    }

时间单位 Time Units

为了跟踪僵尸需要等待多长时间才能再次攻击,我们可以使用Solidity的时间单位。

我们需要僵尸有随着时间推移而升级的等级,以及有进食或攻击后的冷却期cooldown period。

Solidity提供了一些处理时间的原生单位。

该变量现在将返回最新块的当前unix时间戳(自1970年1月1日以来经过的秒数)。 

注意:Unix时间传统上以32位数字存储。这将导致“2038年”问题,届时32位unix时间戳将溢出并破坏许多遗留系统。因此,如果我们希望我们的DApp在20年后继续运行,我们可以使用64位数字,但同时我们的用户将不得不花费更多的精力来使用我们的DApp。

 示例:

uint lastUpdated;

// Set `lastUpdated` to `now`
function updateTimestamp() public {
  lastUpdated = now;
}

// Will return `true` if 5 minutes have passed since `updateTimestamp` was 
// called, `false` if 5 minutes have not passed
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

运用

给DApp添加一个冷却时间,让僵尸在攻击或进食后必须等待1天才能再次攻击。

pragma solidity >=0.5.0 <0.6.0;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    // 1. Define `cooldownTime` here
    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 memory _name, uint _dna) internal {
        // 2. Update the following line:
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
        
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        emit NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

    function createRandomZombie(string memory _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

僵尸的冷却时间

1.对一个僵尸触发冷却的函数

2.查询僵尸冷却时间是否结束的函数

  // 1. Define `_triggerCooldown` function here
  function _triggerCooldown(Zombie storage _zombie) internal{
    _zombie.readyTime = uint32(now + cooldownTime);
  }
  // 2. Define `_isReady` function here
  function _isReady(Zombie storage _zombie) internal view returns(bool){
    return (_zombie.readyTime <= now);
  }


公共函数与安全性

我们不希望任何人可以喂食他们想要的dna给僵尸,所以要把feed函数public修饰符改为internal。

如果冷却时间未到,停止后续逻辑。如果冷却时间结束,执行dna融合操作后要重新让原僵尸进入冷却。

pragma solidity >=0.5.0 <0.6.0;

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. Make this function internal
  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal{
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // 2. Add a check for `_isReady` here
    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);
    // 3. Call `_triggerCooldown`
    _triggerCooldown(myZombie);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

更多有关函数修饰符

为了后续给僵尸升级后添加特殊功能,我们需要先学习更多一些有关函数修饰符的内容。

带参数的函数修饰符
之前我们看了onlyOwner的简单示例。但是函数修饰符也可以接受参数。

在ZombieHelper中,创建名为aboveLevel的修饰符。它需要两个参数,_level(一个uint)和_zombieId(也是一个uit)。
修饰符体应确保僵尸[_zombieId].level大于或等于_level。
记住让修饰符的最后一行用_;调用函数的其余部分。

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // Start here
  modifier aboveLevel(uint _level,uint _zombieId){
    require(zombies[_zombieId].level >= _level);
    _;
  }
}

自定义修饰符的运用

1. 对于僵尸级别2和更高,用户可以更改其名称。
2. 对于20级及更高级别的僵尸,用户将能够为其提供自定义DNA。

创建一个名为changeName的函数。两个参数:_zombieId(一个uint)和_newName(一个数据位置设置为calldata的字符串),并将其设置为external。它应该具有aboveLevel修饰符,并且应该为_level参数传入2。(不要忘记传递_zombieId)。
注意:calldata在某种程度上类似于内存,但它只适用于外部函数
在这个函数中,首先我们需要使用require语句来判断msg.sender等于zombieToOwner[_zombieId]。

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // Start here
  function changeName(uint _zombieId,string calldata _newName) external aboveLevel(2, _zombieId){
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId,uint _newDna) external aboveLevel(20, _zombieId){
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }
}

用“view” Functions来节省Gas

当用户从外部调用视图view函数时,不需要花费任何Gas。

我们需要一个getZombiesByOwner函数来获取一个用户所有的僵尸,这是一个只读的函数。

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  // Create your function here
  function getZombiesByOwner(address _owner) external view returns(uint[] memory){
    
  }
}

存储storage是很昂贵的

Solidity中最昂贵的操作之一是使用存储,尤其是写入。编程时要注意避免低效率的编程逻辑,比如:每次调用函数时都在内存中重建一个数组,而不是简单地将该数组保存在变量中以便快速查找。

在大多数编程语言中,在大型数据集上循环的开销很大。但在Solidity中,如果使用外部视图功能,这比使用存储要便宜得多,因为视图view功能不会花费用户任何Gas。

在内存memory中声明数组 

您可以在数组中使用memory关键字在函数中创建一个新数组,而无需向存储storage中写入任何内容。数组只会存在直到函数调用结束,如果是外部调用的视图函数,那么这比在无存储环境中更新数组要便宜得多。 

示例:

function getArray() external pure returns(uint[] memory) {
  // Instantiate a new array in memory with a length of 3
  uint[] memory values = new uint[](3);

  // Put some values to it
  values[0] = 1;
  values[1] = 2;
  values[2] = 3;

  return values;
}

运用

在getZombiesByOwner函数中,我们希望返回一个uint[]数组,其中包含特定用户拥有的所有僵尸。


  function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
    // Start here
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    return result;
  }


For循环应用场景

有时需要使用for循环在函数中构建数组的内容,而不是简单地将该数组保存到存储中。 

为什么?

对于我们的getZombiesByOwner函数,一个简单的实现是在ZombieFactory合约中存储所有者到僵尸们的映射:

mapping (address => uint[]) public ownerToZombies

然后,每当我们创建一个新的僵尸时,我们只需使用ownerToZombies[owner].push(zombieId)将其添加到所有者的僵尸数组中。getZombiesByOwner会是一个非常简单的函数:

function getZombiesByOwner(address _owner) external view returns (uint[] memory) {
  return ownerToZombies[_owner];
}

但是用这种方法存在一些问题

这种方法很简单,很有吸引力。但是,让我们看看如果我们稍后添加一个函数,将僵尸从一个所有者转移transfer到另一个所有者,会发生什么情况?
该传递transfer函数需要:
step1.将僵尸push新所有者的僵尸数组,
step2.将僵尸从旧所有者的僵尸数组中remove,
step3.将旧主人数组中的每一个僵尸都向上移动一个位置,以填充空隙hole,然后将数组长度减少1。

step3将会很费gas,因为我们必须为每个改变位置的僵尸进行写入操作。如果一个拥有者有20个僵尸并交易掉第一个僵尸,我们将不得不执行19次写入操作来维持阵列的顺序。

注意:当然,我们可以只移动数组中的最后一个僵尸来填充空隙,并将数组长度减少一。但是我们每次交易都会改变僵尸军队的顺序。

 由于视图函数在外部调用时不会耗费大量的gas,因此我们可以简单地使用getZombiesByOwner中的for循环来迭代整个僵尸数组,并构建属于该特定所有者的僵尸数组。那么我们的传递函数会便宜得多,因为我们不需要重新排序存储中的任何数组,而且这种方法总体上更便宜。

简而言之,利用view函数配合for循环获取数组(省了gas),相当于从外部重新编排好一个数组再执行写入操作。这样避免了在内部频繁执行写入的操作。

运用

 类似于JavaScript。

让我们通过编写一个for循环来完成getZombiesByOwner函数,该循环遍历DApp中的所有僵尸,比较它们的所有者以判断是否匹配,并在return之前将它们push到结果数组。 

定义一个计数器赋值为0,for循环中迭代僵尸->所有者的映射来判断该僵尸是否为目标所有人,如果真则记录当前映射索引到结果集中。结果集用单独的计数器作为索引迭代赋值。

  function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    // Start here
    uint counter = 0;
    for(uint i=0;i < zombies.length;i++){
      if(zombieToOwner[i] == _owner){
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

回顾一下

我们添加了一种更新CryptoKitties合约的方法

我们已经学会了只使用Owner来保护核心功能

我们了解了Gas和Gas优化

我们为僵尸增加了等级和冷却

我们现在有了当僵尸达到一定等级后更新其名称和DNA的功能

加油,第四篇继续!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Dwoura犀利

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

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

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

打赏作者

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

抵扣说明:

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

余额充值