見出し画像

初心者が一からブロックチェーンを活用したアプリケーション(Dapps)を作る(③コントラクト中級編)

 暑い日が続いていますが(2018年8月)みなさん水分不足には気をつけてくださいね。これを書いている人は絶賛リモートワーク中なので特に夏を感じることもなく快適に労働してます。リモートワーク最高。

他の記事
 第一回 環境構築編

 第二回 コントラクト基礎編
 第四回 セキュリティ、ガスなど編
 第五回 ERC721トークン、セキュリティ続き編

 前回貼ったコントラクトの説明がまだ途中だったので、今回はその続きから始めて行きたいと思います。

// cryptomongenerator.sol

pragma solidity ^0.4.19;

contract CryptomonGenerator {

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

    uint dnaDigits = 18;
    uint dnaModulus = 10 ** dnaDigits;

    struct Monster {
        string name;
        uint dna;
    }

    Monster[] public monsters;

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

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

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

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

}

目次
 ①mapping
 ②msg.sender
 ③keccak256
 ④require
 ⑤import
 ⑥コントラクトの継承
 ⑦storageとmemory
 ⑧interface
 ⑨複数の返り値を受け取る方法


①mapping

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

 モンスターを作っても誰のものだか分からなければ相棒と言えませんよね。
 ここではモンスターのidをキー、オーナーのアドレスを値とした連想配列、オーナーのアドレスをキー、オーナーが持っているモンスターの数を値とした連想配列を定義しています。
 Solidityではmappingとして連想配列を定義することができます。
 ここでは、モンスターが誰の所有物かというのと、あるオーナーが何匹モンスターを所有しているかを定義しています。なぜ、オーナーがどのモンスターを所有しているかの連想配列ではないのかについてはあとで説明します。

②msg.sender

uint id = monsters.push(Monster(_name, _dna)) - 1;
monsterToOwner[id] = msg.sender;

 ここでは先ほど説明したmappingに、キーをモンスターのid、値をオーナーのアドレスとして追加しています。
 Solidityにはデフォルトで用意されているグローバル変数が複数あり、msg.senderもその中の一つです。この変数には、このコントラクトを実行したユーザーのアドレスが入ります。よって、このコードのようにすることで、mappingにユーザーのアドレスを入れることができます。

③keccak256

uint rand = uint(keccak256(_str));

 モンスターのdnaの元となるハッシュを名前から作ります。
 ここでは、モンスターの名前として渡された文字列をハッシュ関数のKeccak-256を使いハッシュ化しています。この例では文字列をハッシュ化し、モンスターのdnaとしているだけですが、Solidityでは文字列同士の比較ができないため、代わりに文字列のハッシュどうしを比較するという使い方もできます。

④require

function createRandomMonster(string _name) public {  
  require(ownerMonsterCount[msg.sender] == 0);
   .
   .
}

 前回の冒頭で、このコントラクトは最初の相棒を作成するコントラクトだと説明しました。createRandomMonsterの関数はpublicのため、ユーザは好きな時にこの関数を呼び出すことができます。つまり何体でも相棒を作ることができるわけです。そのままではまずいので、条件が満たされない時は処理を止めてくれるrequireを使って、ユーザーが持つモンスターの数が0でない時は処理を止めるようにしています。

 

 さて、このコントラクトで使っている文法の説明が一通り終わりました。このコントラクトで外部からアクセスできる関数はcreateRandomMonsterのみです。このようにしているのは、ユーザーが勝手に自分の好きなdnaを生成して好きなモンスターを自由に追加したりなどのこちらの意図しない動作を防ぐためです。

 ゲームを実装するためにはもっと機能をコントラクトに追加したいのですが、一つのファイルに多くを書き込むのはあまりよろしくないというのはわかると思います。そこで、コントラクトを複数のファイルに書き、コントラクトを継承するという方法で解決しましょう。そのためにSolidityではコントラクトの多重継承がサポートされています。

モジュール化

pragma solidity ^0.4.19;

import "./cryptomongenerator.sol"; // ⑤import

contract KittyInterface { // ⑧interface
  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 { // ⑥コントラクトの継承

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);

  function evolve(uint _monsterId, uint _targetDna) public {
    require(msg.sender == monsterToOwner[_monsterId]);
    Monster storage myMonster = monsters[_monsterId]; // ⑦storageとmemory
    myMonster.dna = (myMonster.dna + _targetDna) / 2;
  }

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

  function takeKitty(uint _monsterId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); // ⑨複数の返り値を受け取る方法
    unionKitty(_monsterId, kittyDna, "kitty");
  }

}

 

 上のコードは今まで説明してきたCryptomonGeneratorコントラクトを継承したcryptomonEvolveコントラクトです。やっぱりポ〇〇ンといったら進化ですよね。自分の相棒をEvolveさせましょう。

⑤import

import "./cryptomongenerator.sol";

 他の言語と同じようにSolidityでもimport文を使うことができます。別ファイルにあるCryptomonGeneratorコントラクトをインポートします。

⑥コントラクトの継承

contract CryptomonEvolve is CryptomonGenerator {
  .
  .
  .
}

 Solidityでコントラクトの継承をしたい時はこのように書くことができます。コントラクトを継承した場合、継承元のprivate出ない関数や変数にアクセスすることができます。これでCryptomonEvolveコントラクトをデプロイするだけで、CryptomonGeneratorコントラクトの機能を使ってモンスターを生成することができるようになります。

⑦storageとmemory

 Solidityが変数を保存する領域にはstorageとmemoryとstackがあります。

 storage はブロックチェーン上のコントラクトストレージに永久に格納される変数です。使用には高額なgasがかかります。memoryは関数が実行された時に確保され、関数の実行が終わった時に解放される変数です。storageより安くすみます。stackは関数内のローカル変数や関数の返り値に使われます。使用はほぼ無料ですが使用量はとても限られています。
 
まとめると、
state variables are always in storage
(コントラクトのトップレベルで宣言される状態変数は常にstorageに保存される。)
function arguments are in memory by default
 (関数の引数はデフォルトではmemoryが使われる。)
local variables of struct, array or mapping type reference storage by   default
 (関数の中で宣言されるローカル変数の中で、構造体・配列・連想配列はデフォルトではstorageが使われる。)
local variables of value type (i.e. neither array, nor struct nor mapping) are stored in the stack
 (関数の中で宣言されるローカル変数の中で、配列や構造体、連想配列でないものはstackが使われる。)

Monster storage myMonster = monsters[_monsterId];
myMonster.dna = (myMonster.dna + _targetDna) / 2;
// monsters[_monsterId]は変更される。

// Monster memory myMonster = monsters[_monsterId];
// myMonster.dna = (myMonster.dna + _targetDna) / 2;
// monsters[_monsterId]は変更されない

 基本的にSolidityが自動でうまくやってくれるのですが、関数内で配列や連想配列を使用する場合は明示的に指定しなければいけません。指定しない場合、Solidityから警告がでます。
 関数内でarrayやmappingを参照する場合、storageを使うとその変数はポインタになり、memoryを使うとその変数は参照元のコピーになります。上のコードのevolve関数では、状態変数のmappingを変更したいので、storageを使っています。

⑧interface

 ブロックチェーン上にあるデータはすべて公開されています。例えばあのcryptokittiesの猫の情報も取得できるわけです。CryptomonEvolveコントラクトでは普通の進化とは別に、cryptokittyのdna(gene)を使った進化も用意しています。
 ブロックチェーン上の別のコントラクトとやりとりしたければinterfaceを用意しましょう。

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

 上の例がcryptokittiesから猫の情報を取得するinterfaceです。見た目はコントラクトのようですが、{}の中に関数のようなものを定義しており、その関数も処理自体は定義されておらず、引数と返り値のみ定義されています。この形をSolidityはinterfaceと認識します。

address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);

 interfaceは使用したいコントラクトのコントラクトアドレスを入力し、初期化することで使用することができます。

⑨複数の返り値を受け取る方法

(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);


 上で紹介したgetKitty関数は複数の返り値を持ちます。その中で必要なのは最後のkittyDnaだけです。このような場合は上のように、返り値の数に対応したコンマを使うことで目的の返り値のみを得ることができます。


ここまでで基本的なコントラクトの文法は紹介しました。次回はセキュリティに関するコントラクトの文法を紹介していこうと思います。

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


参考にしたもの:

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

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

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


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