見出し画像

初心者が一からブロックチェーンを活用したアプリケーション(Dapps)を作る(④セキュリティ、ガスなど編)

 今回はDappsを作る上で重要なセキュリティやガスなどについて説明していきたいと思います。

他の記事
 第一回 環境構築編
 第二回 コントラクト基礎編
 第三回 コントラクト中級編
 第五回 ERC721トークン、セキュリティ続き編

 実は前回までに貼ってきたコントラクトはセキュリティやガスの節約、冗長性という点では足りないところがたくさんありました。なので、適宜修正しながら気をつけるべきところを説明していこうと思います。(今まで紹介した他のコードもこの章を参考にして修正しましょう)

目次
 ① modifier
 ② Ownable
 ③ Dappsの更新
 ④ データ型によるガスの節約
 ⑤ view修飾子によるガスの節約
 ⑥ solidityでの時間の扱い
 ⑦ payable修飾子
 ⑧ withdraw関数
 ⑨ memoryを利用したガスの節約
 ⑩ ランダムの扱い

 

// cryptomongenerator.sol

pragma solidity ^0.4.19;

import "./ownable.sol";

contract CryptomonGenerator is Ownable {

    event NewMonster(uint monsterId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Monster {
      string name;
      uint dna;
      uint32 level; // ④ データ型によるガスの節約
      uint32 readyTime;
      uint16 winCount;
      uint16 lossCount;
    }

    Monster[] public monsters;

    mapping (uint => address) public monsterToOwner;
    mapping (address => uint) ownerMonsterCount;

    function _createMonster(string _name, uint _dna) internal {
        uint id = monsters.push(Monster(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
        monsterToOwner[id] = msg.sender;
        ownerMonsterCount[msg.sender]++;
        NewMonster(id, _name, _dna);
    }

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

    function createRandomMonster(string _name) public {
        require(ownerMonsterCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createMonster(_name, randDna);
    }

}
// cryptomonevolve.sol

pragma solidity ^0.4.19;

import "./cryptomongenerator.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 CryptomonEvolve is CryptomonGenerator {

  KittyInterface kittyContract;

  modifier onlyOwnerOf(uint _monsterId) { // ① modifier
    require(msg.sender == monsterToOwner[_monsterId]);
    _;
  }

  function setKittyContractAddress(address _address) external onlyOwner { // ② Ownable
    kittyContract = KittyInterface(_address); // ③ Dappsの更新
  }

  function _triggerCooldown(Monster storage _monster) internal {
    _monster.readyTime = uint32(now + cooldownTime); // ④ データ型によるガスの節約
  }

  function _isReady(Monster storage _monster) internal view returns (bool) { // ⑤ view修飾子によるガスの節約
      return (_monster.readyTime <= now); // ⑥ solidityでの時間の扱い
  }

  function evolve(uint _monsterId, uint _targetDna) public onlyOwnerOf(_monsterId){
    Monster storage myMonster = monsters[_monsterId];
    require(_isReady(myMonster));
    myMonster.dna = (myMonster.dna + _targetDna) / 2;
    _triggerCooldown(myMonster);
  }

  function unionKitty(uint _monsterId, uint _targetDna, string _species) internal onlyOwnerOf(_monsterId) {
    Monster storage myMonster = monsters[_monsterId];
    require(_isReady(myMonster));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myMonster.dna + _targetDna) / 2;
    if (keccak256(_species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createMonster("NoName", newDna);
    _triggerCooldown(myMonster);
  }

  function takeKitty(uint _monsterId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_monsterId, kittyDna, "kitty");
  }
}
// cryptomonhelper.sol

pragma solidity ^0.4.19;

import "./cryptomonevolve.sol";

contract CryptomonHelper is CryptomonEvolve {

  uint levelUpFee = 0.001 ether;

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

  function withdraw() external onlyOwner { // ⑧ withdraw関数
    owner.transfer(this.balance);
  }

  function setLevelUpFee(uint _fee) external onlyOwner {
    levelUpFee = _fee;
  }

  function levelUp(uint _monsterId) external payable { // ⑦ payable修飾子
    require(msg.value == levelUpFee);
    monsters[_monsterId].level++;
  }

  function changeName(uint _monsterId, string _newName) external aboveLevel(2, _monsterId) onlyOwnerOf(_monsterId) {
    monsters[_monsterId].name = _newName;
  }

  function changeDna(uint _monsterId, uint _newDna) external aboveLevel(20, _monsterId) onlyOwnerOf(_monsterId) {
    monsters[_monsterId].dna = _newDna;
  }

  function getMonstersByOwner(address _owner) external view returns(uint[]) {
    uint[] memory result = new uint[](ownerMonsterCount[_owner]); // ⑨ memoryを利用したガスの節約
    uint counter = 0;
    for (uint i = 0; i < monsters.length; i++) {
      if (monsterToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

}
// cryptomonattack.sol

pragma solidity ^0.4.19;

import "./cryptomonhelper.sol";

contract CryptomonAttack is CryptomonHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) { // ⑩ ランダムの扱い
    randNonce++;
    return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
  }

  function attack(uint _monsterId, uint _targetId) external onlyOwnerOf(_monsterId) {
    Monster storage myMonster = monsters[_monsterId];
    Monster storage enemyMonster = monsters[_targetId];
    uint rand = randMod(100);
    if (rand <= attackVictoryProbability) {
      myMonster.winCount++;
      myMonster.level++;
      enemyMonster.lossCount++;
      feedAndMultiply(_monsterId, enemyMonster.dna, "monster");
    } else {
      myMonster.lossCount++;
      enemyMonster.winCount++;
      _triggerCooldown(myMonster);
    }
  }
}

① modifier

modifier onlyOwnerOf(uint _monsterId) { // ① modifier
  require(msg.sender == monsterToOwner[_monsterId]);
  _;
}

 これまでにも関数にinternalだったりviewだったりと修飾子をつけてきましたが、modifierはそれらのように自分で定義した修飾子を作成し、関数を修飾するためのものです。
 前回まで、操作しようとしているモンスターの所有者がコントラクトを送ったユーザーなのかを確認するために、

require(msg.sender == monsterToOwner[_monsterId]);

このようにrequireを使っていました。しかし、この確認は複数の関数で使用したいので、タイポや記述忘れ防止のためにmodifierをつかってロジックの共通化をすることができます。

② Ownable

function setKittyContractAddress(address _address) external onlyOwner { // ② Ownable
  kittyContract = KittyInterface(_address); // ③ Dappsの更新
}

 external関数は誰でも呼び出すことができます。ただ、ユーザーに使ってもらうことを想定した関数はそのままでいいのですが、ゲームの設定などといった不特定多数のユーザーに触ってもらいたくない関数もexternalにせざるを得ない場面も多々あります。そういったときにその関数を特定のユーザーのみが扱えるようにする修飾子がOpenZeppelinのownableコントラクトで実装されているonlyOwner修飾子です。これはownableコントラクトをインポートし、コントラクトを継承することで使えるようになります。

③ Dappsの更新
 2018年8月現在、Ethereum上のスマートコントラクトのコードは変更不可能です。コントラクトをメインネットワークにデプロイした後、何かしらの問題が起きてコードの変更が必要になった場合、すでにデプロイされているコントラクトのコードは変更不可なので、新しいコントラクトを新規にデプロイしそちらを使ってもらうようにするしかありません。そうした事態を避けるために、後ほど変更が必要になるかもしれないと予想される部分はコード外に持たせるという方法があります。

function setKittyContractAddress(address _address) external onlyOwner { // ② Ownable
  kittyContract = KittyInterface(_address); // ③ Dappsの更新
}

 ここでは、以前ハードコーディングしていたCryptoKittiesのコントラクトアドレスを関数の引数で与えるようにし、その引数で初期化したインターフェイスを状態変数に保存しています。

④ データ型によるガスの節約

struct Monster {
  string name;
  uint dna;
  uint32 level; // ④ データ型によるガスの節約
  uint32 readyTime;
  uint16 winCount;
  uint16 lossCount;
}
function _triggerCooldown(Monster storage _monster) internal {
  _monster.readyTime = uint32(now + cooldownTime); // ④ データ型によるガスの節約
}

 実はuintにはuint8、uint16、uint32のサブクラスがあります。それぞれ符号なしの8、16、32ビット分の整数を格納できる領域を確保します。しかし2018年8月現在では構造体の内部での使用以外では256ビット分の領域を確保するようになっているようです。
 以前の章で説明したように、状態変数はコントラクト内のストレージに保存されます。コントラクト内のストレージのように、ブロックチェーンに何か保存したり、保存されたものを変更したりといった操作にはガスという手数料がかかり、そのガスの額にはデータの容量も大きく影響します。なので、できるだけコントラクトが扱うデータの量は減らすように心がけるべきでしょう。
 ここでは、モンスターのプロパティの中で、あまり大きい値にならないと予想されるプロパティのデータ型を、より容量の小さいものに変更してガスを節約しています。

⑤ view修飾子によるガスの節約

function _isReady(Monster storage _monster) internal view returns (bool) { // ⑤ view修飾子によるガスの節約
    return (_monster.readyTime <= now); // ⑥ solidityでの時間の扱い
}

 上で説明したように、ブロックチェーンに何か保存したり、変更したり、といった処理にはガスがかかります。逆にいうと、ブロックチェーンを読み取ったり、ブロックチェーンへ何も処理をしない場合はガスがかからないということになります。
 ブロックチェーンから読み取るだけの場合はview、ブロックチェーンを参照することもしない場合はpureと関数の後につけることで、コントラクトを呼び出すスクリプト(web3.jsなど)にガスがかからないことを伝えることができます。(view修飾子をつけた場合はローカルノードに保存されている過去ブロックの情報を見に行くようです)
 上のコードブロックでは、モンスターの次の行動までの待ち時間を参照するだけの関数なので、view修飾子を追加しています。

⑥ solidityでの時間の扱い

function _isReady(Monster storage _monster) internal view returns (bool) { // ⑤ view修飾子によるガスの節約
    return (_monster.readyTime <= now); // ⑥ solidityでの時間の扱い
}
uint cooldownTime = 1 days;

.
.
.

function _triggerCooldown(Monster storage _monster) internal {
  _monster.readyTime = uint32(now + cooldownTime); // ④ データ型によるガスの節約
}

 Solidityでも他の言語と同じように時間を扱う方法が用意してあります。
nowは現在のunixタイムスタンプを返します。また、タイムスタンプの操作用にseconds、minutes、hours、days、weeks 、yearsといった単位も用意されており、uintの秒数に変換されます。

⑦ payable修飾子

function levelUp(uint _monsterId) external payable { // ⑦ payable修飾子
  require(msg.value == levelUpFee);
  monsters[_monsterId].level++;
}

 payable修飾子をつけた関数は関数呼び出しの際に一緒にetherを送ることができます。送られたetherの額はmsg.valueで確認することができます。

⑧ withdraw関数

function withdraw() external onlyOwner { // ⑧ withdraw関数
  owner.transfer(this.balance);
}

 payable関数でおくったetherは、コントラクトのアドレスに紐つけられます。コントラクトに紐つけられたetherはそのままではコントラクトに永久に閉じ込められたままですが、withdraw(出金)関数を用意しておくことで(名前はなんでもいいです)、コントラクトに貯められたetherを引き出すことができるようになります。
 コントラクトに貯められたetherはthis.balanceで取得することができます。
 上のコードではtransfer関数を使い、コントラクトのオーナーのアドレス(ownerというaddress型変数)にthis.balance全額を送っていますが、もちろん下のコードのように他のユーザーにもetherを送ることができます。

seller.transfer(msg.value)

⑨ memoryを利用したガスの節約

function getMonstersByOwner(address _owner) external view returns(uint[]) {
  uint[] memory result = new uint[](ownerMonsterCount[_owner]); // ⑨ memoryを利用したガスの節約
  uint counter = 0;
  for (uint i = 0; i < monsters.length; i++) {
    if (monsterToOwner[i] == _owner) {
      result[counter] = i;
      counter++;
    }
  }
  return result;
}

 前回の章で、変数がコントラクトのどの保存領域を使うかという説明をしました。その中で、関数の中で宣言されるローカル変数の中で、構造体・配列・連想配列はデフォルトではstorageが使われる、ということを紹介しました。しかし、関数内で宣言するローカル変数を全てコントラクトのストレージに保存する必要はないでしょう。そこで、memoryキーワードを使って配列を宣言することで、ガスの節約ができます。
 上のコードでは、オーナーの持つモンスターの配列を、オーナーの持つモンスターの数の連想配列と、モンスターのオーナーの連想配列からmemory変数で作成しています。将来的にモンスターを交換したり、自然に返したりする機能を配列で実装する場合、オーナーの手持ちの変更のたびに配列が大きく変わり、高額のガスが必要になることが予想されます。それをmemoryで行えば、その分のガスが0になるため、必要最低限の情報のみストレージに保存することが求められるでしょう。

⑩ ランダムの扱い

function randMod(uint _modulus) internal returns(uint) { // ⑩ ランダムの扱い
  randNonce++;
  return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
 
function attack(uint _monsterId, uint _targetId) external onlyOwnerOf(_monsterId) {
  Monster storage myMonster = monsters[_monsterId];
  Monster storage enemyMonster = monsters[_targetId];
  uint rand = randMod(100);
  if (rand <= attackVictoryProbability) {
    myMonster.winCount++;
    myMonster.level++;
    enemyMonster.lossCount++;
    feedAndMultiply(_monsterId, enemyMonster.dna, "monster");
  } else {
    myMonster.lossCount++;
    enemyMonster.winCount++;
    _triggerCooldown(myMonster);
  }
}

 ゲームの勝敗などをランダムに決めるというスマートコントラクトを実装しようとすると、少し大変です。なぜかというと、ランダムに勝敗が決まるトランザクションを攻撃する側のユーザーがノードに送るという実装をした場合、トランザクションを作成し、そのトランザクションを自分のノードに送れば勝敗がわかってしまい、さらにそのトランザクションを他のノードに送らないようにすれば、そのトランザクションをブロックに含むかどうかの選択ができてしまうからです。なので、例の場合に厳密にランダムを扱いたい場合はランダム性をコントラクトに含めない方が無難です。厳密にランダムを扱い人のためにoraclizeなどのoracleサービスではrandamな結果を提供するサービスが用意されています。

 ただ上のコードでは、悪意のあるユーザーが不正をしたところで新しいモンスターを作成できるだけで、そのモンスター自体にも価値の上下というのはないので、大きな問題は発生しません。なのでkeccak256を使い、擬似的なランダムな数字を作成し、それを使っています。もちろん不正できない手法で実装する方が良いのですが、ここではoracleの使い方は省略します。


 今回はセキュリティの担保とガスの節約の方法をいくつか紹介しました。次回はコントラクトが壊れないように気をつけることと、CryptoKittiesのようなERC721トークンの実装を紹介していきたいと思います。

 書いているのは初心者のため、間違い、浅い理解などあるかもしれません。その場合はぜひ指摘してもらえればと思います。喜んで修正します。

参考にしたもの:

↑ゲーム感覚でスマートコントラクトの書き方が学べます。


↑ブロックチェーンで開発する際必要なことはだいたい網羅されていると思います。とても勉強になります。

この記事を書いた人:
株式会社Calano
インターン 島村大
学部4年生で大学では機械学習と数理最適化、CalanoではアプリやGCPなどを担当。最近ブロックチェーン始めました。


この記事が気に入ったらサポートをしてみませんか?