iT邦幫忙

2022 iThome 鐵人賽

DAY 11
2
Web 3

從以太坊白皮書理解 web 3 概念系列 第 12

從以太坊白皮書理解 web 3 概念 - Day11

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day11

Learn Solidity - day 3 Advance Solidity

前兩天,透過建立了 ZombieFactory 與 ZombieFeeding

講解關於基礎 Contract 的發佈與其他 Contract 互動。

今天將會透過 Lession 3: Advanced Solidity 更深入去理解一些細節

Immutability of Contracts

到目前為止, solidity 語法類似於 javascript 語言。

但是 Contract 與一般的應用程式具有很大的不同。

其中一個不同就是發佈到鏈上的 Contract 程式碼無法更改。

一旦發佈上去,就永遠被紀錄在鏈上。

因此,程式碼必須很小心寫,否則一旦有錯,基本上無法修補。

只能重新發新的 Contract 來進版。

外部依賴

如同上述所說,

之前 CryptoKitty 的 Contract address 被 hard-coded 在 ZombieFeeding 這樣一來。

一旦 CryptoKitty Contract 被下架,只能 ZombieFeeding 的功能將無法作用。

所以這時,就不能使用 hard-coded 的方式,而需要開放一個 function setKittyContractAddress 來開放 Contract address 做修改。

所以 ZombieFeeding 的生命周期就依賴於 CryptoKitty 這個外部 Contract 。

更新 ZombieFeeding

  1. 刪除 hard-coded 變數 ckAddress
  2. 修改 kittyContract 初始化的邏輯
  3. 建立一個 function 叫作 setKittyContractAddress。
    需要一個參數 _address(address)。
    讀取權限為 external
  4. 在 setKittyContractAddress function 初始化 kittyContract = KittyInterface(_address);

更新如下

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 {

  
  // 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 Contract

可以注意到上面 setKittyContractAddress function 是 external 代表任何其他 Contract 都可以呼叫這個 function 來對 Contract Address 做修改。

並不是一個很安全的作法。

比較好的作法是只允許自己的 Contract 去修改 address 。

要這樣做就比需使用 Ownable 這個概念。

在 solidity , Ownable 代表可以限定一個 owner 所具有的權限。

以下是一個 OpenZeppelin Solidity Library 所實作 Ownable Contract 。 OpenZeppelin 是一個用來做安全性審查以及社群審查的 Smart Contract Library 。

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

以下是一些新的概念

  1. Constructors: constructor() 是一個建構子。會在 Contract 建構時執行。
  2. Function Modifiers: modifier onlyOwner()。
    modifier 是一個用來宣告裝飾子函數的保留字,其宣告出來的函數可以用來修改其他函數。
    onlyOwner 裝飾子函數。用來限制 function 只有 Contract owner 才能執行。
  3. indexed 關鍵字:當下還不會用到這個概念。

規結下來, Ownable contract 做到以下事情:

  1. 當一個 Contract 建立,其建構子會把 owner 設定為 msg.sender
  2. 新增了一個 onlyOwner 修飾子,這個修飾子可以用來限制 owner 才能執行該 function

更新 setKittyContractAddress 權限: 引入 ownable

  1. import "./ownable.sol"; 到 ZombieFactory
  2. 讓 ZombieFactory 繼承 Ownable。

如下:

pragma solidity >=0.5.0 <0.6.0;

// 1. Import here
import "./ownable.sol";
// 2. Inherit here:
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 memory _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 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);
    }

}

更新 setKittyContractAddress 權限: 新增 onlyOwner

目前已知到繼承關係如下

ZombieFactory is Ownable
ZombieFeeding is ZombieFactory

所以 ZombieFeeding 也可以使用 Ownable 內部的 public/external 的資源。

這邊就可以直接新增 onlyOwner 到 setKittyContractAddress 後面。

更新如下

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;

  // Modify this function:
  function setKittyContractAddress(address _address) external onlyOwner {
    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");
  }

}

Gas

Gas 是執行 Smart Contract 所需要消耗的燃料。

這是一種避免 Smart Contract 因為不當操作導致過度消耗 EVM 資源的機制。

在 solidity , 每個使用者想要執行 Smart Contract 都必須要使用一個代幣叫作 gas 。

使用者需要使用 Ether 來購買代幣 gas , 也就是說執行 Smart Contract 是需要花費 Ether。

要花費多少代幣 gas 來執行 Smart Contract 跟 Smart Contract 執行內容的複雜度成正相關。

每一個單一運算具有一個代幣消耗值 gas cost 是根據多少運算資源來執行該步驟而定。

舉例來說: 寫一個要寫入 storage 的邏輯會貴於只運算加兩個數值的邏輯

整個功能的代幣消耗 gas cost 是每一個運算代幣消耗的總和。

為何需要 gas

Ethereum 可以想像是一個巨大,緩慢,但安全的電腦。

當你執行一個功能,每個在共識網路的單一節點需要去執行相同功能來驗證結果。

Ethereum 的創作者不希望 EVM 因為執行一個無窮回圈,導致整個鏈上的節點都癱瘓,

所以設計出根據使用量來付費的機制。

Struct packing to save gas

因為 gas 機制,所以在撰寫 Contract 時

會儘可能減少消耗運算資源

在 solidity ,使用子型別一般來說是沒有太多好處。

對於 uint 來說, 不論使用 uint8, uint16, uint32。 solidity 預設會保留 256 bit 來做儲存。

然後如果是在 struct 內,就會有影響

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

在 struct 中, solidity 會選擇以儘可能小的方式做資料打包。

並且要儘量讓同型別的變數放在一起,舉例來說:

struct Smaller {
     uint c; uint32 a; uint32 b;
}
struct Larger {
     uint32 a; uint c; uint32 b;
}

同型別放在一起打包會有可以有比較多壓縮空間。

更新 ZombieFactory

  1. 新增兩個屬性到 Zombie struct: level(uint32), readyTime(uint32)
    為了讓這兩個資料打包再一起,所以一起放到 struct 最後。

更新如下:

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;

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

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string memory _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 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);
    }

}

時間單位

level 這個屬性是為了之後要做戰鬥系統所設計的。

readyTime 屬性是為了讓 feed zombie 速度不要過快的冷卻時間。

為了讓 feed zombie 不要過度執行,

因此需要使用時間單位來紀錄剩下多久時間一個 zombie 可以繼續 feed。

Solidity 預設提供 now 變數來取得最後區塊當下的 unix time(timestamp)以秒為單位。

其他還有 seconds, minutes, hours, days, weeks, years 等等。

這些都會被轉變回 uint 並且以秒為單位做呈獻。

範例如下:

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

實作 cooldown 邏輯

  1. 宣告一個 uint 變數 cooldownTime,並且設定值為 1 days 。
  2. 修改 _createZombie 內部,關於 struct Zombie 的部份
    多帶入兩個參數值: 1(給level), uint32(now + cooldownTime)(給 readyTime)

更新如下

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

}

為了完成 coolDown 功能

需要再修改 feedAndMultiply 使得

  1. Feeding 會觸發該 zombie 檢查功能
  2. Zombie 只有在冷卻時間結束才能繼續 Feeding

傳遞 struct 當作參數

可以傳遞 storage 指標當作一個參數到 private 或 internal 的函數

範例如下:

function _doStuff(Zombie storage _zombie) internal {
    // do stuff with _zombie
}

透過這種方式就不需要傳遞一個 id 再去察看對應的 zombie 物件

具體修改步驟如下:

  1. 定義一個 function 叫作 _triggerCooldown 。
    需要一個參數 _zombie(Zombie storage pointer)
    讀取權限是 internal
  2. function 內容更新 _zombie.readyTime = uint32(now + coolDownTime)
  3. 定義一個 function 叫作 _isReady 。
    需要一個參數 _zombie(Zombie storage pointer)
    讀取權限是 internal
    function 類型是 view
    回傳型別是 bool 。
  4. _isReady 的 function 內容是 return (_zombie.readyTime <= now) 。

更新 ZombieFeeding 如下

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

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

}

public functions & Security

把 cooldown 機制放入 feedAndMultiply function 內。

首先要思考 public 與 external 的 function 是否可能造成誤用

除了 onlyOwner 可以限定 owner 才能執行外。

為了不讓 feedAndMultiply function 被任意呼叫

因此需要做一些讀取權限的修改。

具體作法如下:

  1. 更改 feedAndMultiply function 讀取權限為 internal
  2. 新增 require(_isReady(myZombie)) 邏輯到 feedAndMultiply function
  3. 最後再呼叫 _triggerCooldown(myZombie);

更新 ZombieFeeding 如下

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

}

更多的 function modifiers

為了讓 Zombie 具有更多功能

所以要把新功能加入到 zombiehelper.sol 內

並且在 zombiefeeding.sol 內引用

Function modifiers with arguments

前面學到 onlyOwner 這個 modifier。

然而 modifier 也可以帶入參數。

舉例如下:

// A mapping to store a user's age:
mapping (uint => uint) public age;

// Modifier that requires this user to be older than a certain age:
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// Must be older than 16 to drive a car (in the US, at least).
// We can call the `olderThan` modifier with arguments like so:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // Some function logic
}

olderThan 這個 modifier 需要帶入兩個參數 age 與 userId

接下來將會利用 level 這個屬性來建立 modifier 來做一些限制

新增 ZombieHelper

  1. 在 ZombieHelper ,建立一個 modifier 叫作 aboveLevel
    aboveLevel 需要兩個參數: _level(uint) , _zombieId(uint)
  2. function 內容是 require(zombies[_zombieId].level >= level);
  3. 最後一行需要加入 _; 代表執行原本裝飾的函式內容。

新增如下

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

Zombie Modifiers

使用 aboveLevel modifier 來建立一個功能

定義 Zombie 有兩個獎勵準則來讓使用者升級 Zombie:

  1. zombies level >= 2, 使用者可以改 zombie 名稱。
  2. zombies level >= 20, 使用者可以客製化 zombie DNA

具體作法如下:

  1. 建立 changeName function 。
    需要兩個參數 _zombieId(uint), _newName(string)
    並且需要把 _newName 資料存放位置設定為 calldata
    function 讀取權限是 external
    必須要有 aboveLevel modifier
  2. 在 changeName function 需要做以下驗證
    require(msg.sender == zombieToOwner[_zombieId]);
  3. 在 changeName function 更新 zombies[_zombieId].name = _newName;
  4. 建立 changeDNA function 。
    其內容大致與 changeName 一樣
    除了第2個參數是 _newDna(uint)還有需要修改 zombies[_zombieId].dna = _newDna;

更新如下

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 function 來降低 gas 消耗

新增一個 function 叫作 getZombiesByOwner

這個 function 需要從鏈上讀取資料,所以宣告成 view function。

View function 不需要花費 gas

當 view function 被外部使用者呼叫時,不會花費任何 gas

因為 view function 並不會修改任何鏈上的資料,只會讀取資料。因此,web3.js 看到 view function 時只需要去讀取 local Ethereum 節點,而不需要做任何交易到鏈上。

因此,我們可以透過把一些只讀取資料的功能設定為 external view 來節省 gas 。

實做 getZombiesByZombieOwner

  1. 建立 function 叫作 getZombiesByZombieOwner
    需要一個參數: _owner(address)
  2. 宣告 function 為 external view
  3. 設定 function 的回傳值為 uint[] 並且設定為 memory

更新 ZombieHelper contract 如下

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 操作是昂貴的

當運算會使用到 storage 時,gas 花費會很昂貴特別是寫入資料時。

為了讓 gas 花費降低儘可能不要使用 storage 來處理資料。

大部份程式語言中,查詢大量資料集的消耗是很昂貴的。
但在 solidity,當在 external view function 查詢大量資料卻比使用 storage 變數便宜,因為 view 只做查詢。

以下使用 memory 變數回傳 array 的範例

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

  1. 宣告一個 uint[] memory 變數 result
  2. 更新 result = new uint;
  3. 回傳 result

更新如下

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

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

}

For Loops

語法如下:

function getEvens() pure external returns(uint[] memory) {
  uint[] memory evens = new uint[](5);
  // Keep track of the index in the new array:
  uint counter = 0;
  // Iterate 1 through 10 with a for loop:
  for (uint i = 1; i <= 10; i++) {
    // If `i` is even...
    if (i % 2 == 0) {
      // Add it to our array
      evens[counter] = i;
      // Increment counter to the next empty index in `evens`:
      counter++;
    }
  }
  return evens;
}

更新 getZombiesByOwner 邏輯

具體步驟如下:

  1. 宣告 uint 變數 counter 並且設定值為 0
  2. 宣告一個 for loop , 從 uint 變數 i = 0 到 i < zombies.length
  3. 在 for loop 內宣告一個 if 判斷式
    if (zombieToOwner[i] == _owner)
  4. if 條件式如果成立 則執行以下步驟
    更新 result[counter] = i;
    更新 counter = counter + 1;
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;
  }

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

}

到此 zombie level 的邏輯就設定完了

下個章節將繼續討倫 Battle System 的互動性。


上一篇
從以太坊白皮書理解 web 3 概念 - Day10
下一篇
從以太坊白皮書理解 web 3 概念 - Day12
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言