Solidity入門基礎編【3】 – CryptoZombiesで学習 –

  • このエントリーをはてなブックマークに追加
  • LINEで送る

第2回に引き続きSolidity入門基礎編を進めていきます。
ここまでくると基礎的なものは身についてきたかも知れないですね。

前回のはこちら。
Solidity入門基礎編【2】 – CryptoZombiesで学習 –

初めての方はこちらから。
Solidity入門基礎編【1】 – CryptoZombiesで学習 –

始めるまえにこちらへアクセスしてください。
https://cryptozombies.io/jp/lesson/3/chapter/1/

チャプター 1: コントラクトの不変性

スマートコントラクトを実装するうえで重要な点として、一度ブロックチェーンに書き込まれたものは変更や削除が出来ないということが挙げられます。途中でバグがみつかった場合、修正後再度デプロイする必要がありますが、そうするとコントラクトアドレスが変わってしまいます。

コントラクトは変更しない前提で組む必要があるので、パラメータをつかって柔軟性を持たせる必要があります。

例えば外部を参照するための固定値アドレスを入れたりすると、そのアドレスが変更になったときにデプロイし直しになってしまうので、パラメータで自由に変更できるようにしようねというお話です。

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 {
    // インターフェイスにアドレスをいれて使用できるようにする
    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");
  }

}

チャプター 2: Ownable コントラクト

Ownable(所有可能な)コントラクトを作っていきます。

例えばあるコントラクト内の関数を誰もが自由に実行してしまえるようであれば、セキュリティ上問題ですよね。

なので、実行できる人を制限しようというものです。

このチャプターでは一旦、importと継承のみを行っています。

pragma solidity ^0.4.19;

// ownable.solをインポート
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;
    }

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

}

チャプター 3: onlyOwner 関数の修飾子

このチャプターでは関数の修飾子 modifierについて学んでいきます。

”関数の修飾子”というのはざっくりいうと、関数の動きに制限や変更を加えるものです。

例えば関数実行直後にrequire文でエラーチェックしたりとか。

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;

  // ownable.sol内にある以下を読み込む。externalの右隣に付けると、modifier内のプログラム(require~~)が実行される
  //   modifier onlyOwner() {
  //     require(msg.sender == owner);
  //     _;
  //   }
  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");
  }

}

チャプター 4: ガス(燃料)

ここではイーサリアムの特徴であるガスについて触れています。

トランザクション(ブロックチェーンへの書き込み)が発生する際に、ガス代が実行者に対して掛かります。

で、このガス代は処理の種類によって変わってくるので、その辺りはエンジニアの腕次第といった感じです。

変数に保存したり、送金したり、変数内の値を変えたり…でガス代が掛かります。

例えば、構造体(struct)内にuint8,uint32,uint32,uint8のプロパティを設定する場合は、桁数の近いものを隣に並べることでガス代が安くなるとのこと。uint8,uint8,uint32,uint32

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

}

チャプター 5: 時間の単位

”タイムスタンプ”の取得方法についてメインで学びます。

nowをつかうと、現在のunixタイムスタンプが取得できます。
また、1 days、1 hoursをつかうと、それぞれの秒数を取得できます。

1 days 24時間 x 60 分 x 60 秒=86400秒
1 hours 60 秒 x 60 分=3600秒

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 {
        // 構造体の引数にlevelとreadyTimeを追加する
        // levelには1、readyTimeには現在時刻にcooldownTimeを1 days分足したものを入れる
        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);
    }

}

チャプター 6: ゾンビのクールダウン

ゾンビが無限に子猫を捕食して増殖することを防ぐ目的で
この機能を実装していきます。
1.捕食することでゾンビのクールダウンが始まる。
2.ゾンビはクールダウン期間が終わるまで子猫を捕食することはできない。

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 {
    // readyTime(完了日時)に現在の時刻にcooldownTime(1日)をプラスする
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  // クールダウン中かどうかを判定する
  function _isReady(Zombie storage _zombie) internal view returns(bool) {
    // ()で囲うと結果をbool型 trueまたはfalseで返せる
    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");
  }

}

チャプター 7: Public関数とセキュリティ

publicやexternalという修飾子をつけると、外部から関数が実行できるためセキュリティ的な観点からいくと宜しくない。

なので、本当にpublicやexternalをつけるべきかを改めて見返す必要がある。といった内容。

コントラクト内でしか呼ばれていない関数をprivateやinternalに変えていくことで対処できる。

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

  // この関数は内部からしか呼んでいないのでinternalに設定
  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // _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");
  }

}

チャプター 8: 関数修飾子の続き

チャプター3の続き。

関数修飾子(modifierで宣言するやつ)に引数を渡せるよということを学ぶ。

modifierは関数を制御する関数のようなものと紹介しました。

で、制御するための関数にも引数が渡せちゃいます。

目的:ゾンビのlevelプロパティの値により特殊能力を制限する

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // aboveLevelというmodifier を作成
  // level判定につかうための_level (uint) とzombieを特定するためのID _zombieId (uint) の2つの引数を設定
  modifier aboveLevel(uint _level, uint _zombieId) {
    // zombies[_zombieId].levelが_level以上であることを確認
    require(zombies[_zombieId].level >= _level);
    // _;はmodifierの最後に必ずつける必要がある。
    _;
  }
}

チャプター 9: ゾンビ修飾子

以下目的の機能を実装するためのチャプター。

目的:
ゾンビのレベルが 2以上なら、ユーザーは名前を変更できるようになる。
ゾンビのレベルが 20以上なら、カスタムDNAを与えることができるようになる。

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

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

  // 名前を変更するための関数 関数を使える条件としてはゾンビのレベルが2以上であること
  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    // オーナーのゾンビであることを確認 他人のものを変更できないように
    require(msg.sender == zombieToOwner[_zombieId]);
    // ゾンビの名前を新しい名前に変更
    zombies[_zombieId].name = _newName;
  }
  // DNAを変更するための関数 関数を使える条件としてはゾンビのレベルが20以上であること
  function changeDna(uint _zombieId, uint _newDna ) external aboveLevel(20, _zombieId) {
    // オーナーのゾンビであることを確認 他人のものを変更できないように
    require(msg.sender == zombieToOwner[_zombieId]);
    // ゾンビのDNAを新しいDNAに変更
    zombies[_zombieId].dna = _newDna;
  }
}

チャプター 10: View 関数でガスを節約

ここでは以下目的にそった機能を実装します。
目的:自身がもっているゾンビたちを表示させる

”表示させる”とありますが、以前紹介した読み取り専用関数 viewをつかうことで、ガス代を節約できます。

ガス代が掛かるのはトランザクションが発生したときで、ブロックチェーン上のデータを参照するだけならガス代は掛かりません

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

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

  function changeName(uint _zombieId, string _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付関数を作る
  // あるオーナーの持っているゾンビ一覧を表示させたいので、引数にオーナーのアドレスを設定
  // 返り値としては複数のゾンビIDを返すので、uint型の配列を指定
  function getZombiesByOwner(address _owner) external view returns(uint[]){
  }
}

チャプター 11: Storageのコストは高い

storageへの保存はガス代が高いので、関数内でのみ使えるmemoryを使おうねというお話。

目的:特定のユーザーが保有している全てのゾンビを、uint[]配列として返すための変数をつくる

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

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

  function changeName(uint _zombieId, string _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[]) {
    // ゾンビの数分の配列で出来たuint型を宣言する(ゾンビ数分の仕切りで分かれた箱を用意するイメージ)
    // 型[] memory 変数名 = new 型[](仕切りの数)
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    return result;
  }

}

チャプター 12: For ループ

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

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

  function changeName(uint _zombieId, string _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[]) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    // カウンターを0に設定(後ほど配列で必要になる)
    uint counter = 0;
    // 全ゾンビ分ループさせる
    for (uint i = 0; i < zombies.length; i++) {
      // ゾンビのオーナーが指定されたオーナーと一致する場合
      if(zombieToOwner[i] == _owner) {
        // result配列にIDをセット
        result[counter] = i;
        // カウンターを1繰り上げる
        counter++;
      }
    }
    return result;
  }

}

レッスン3はここまで。
ガスを意識したプログラミングはSolidity独特の概念かも知れませんね。

続きは次回記事にて書いていきます。

Solidity入門基礎編【4】 – CryptoZombiesで学習 –

  • このエントリーをはてなブックマークに追加
  • LINEで送る

SNSでもご購読できます。

コメントを残す

*