前回投稿

https://qiita.com/kackytw/items/b4cec532eb10340e4879

今回の目標

今回の目標は、タイトルにもあるとおり、スマートコントラクトを使ってガチャを実装することにします。

なぜガチャなのか?

それは、ソーシャルゲームクリエイターとしてガチャは必須機能であると考えるからです。

また、実装難易度的にそんなに難しいものではないし、言語の機能にそれなりに触れられるため、Lv.2としてはまあ適度かなと思いました。

前回の投稿ではスマートコントラクトの開発環境の立ち上げから、デバッグの重要性について解説しましたが、今回ではSolidityのプログラムの概要を理解できるようにしていきたいと思います。

参考資料

http://solidity.readthedocs.io/en/develop/index.html

Solidityのドキュメントです。

なんとなくの要件定義と設計

そもそもガチャとは何なのか、要件定義しておきましょう。
ガチャは、仮想通貨を消費することによって確率的に報酬を受けるものと定義します。確率によって、レアリティの高いものから低いものまで順位がつけられ、それに応じた報酬を受け取ります。一般的にレアリティの高いものは当たる確率は低く、レアリティの低いものは当たる確率は高くなります。

今回の設計では、消費する仮想通貨をEthereum上のtokenとして、受け取る報酬をEtherとしましょう。

だいたい、こんな感じの実装になることが想像できます。

function gacha() {
  lotteryWeight <- (抽選確率のテーブル)
  rewardTable <- (報酬テーブル)

  (仮想通貨を消費する)

  reward <- (lotteryWeight,rewardTableから抽選した報酬)
  (Etherを送信する) 
}

スマートコントラクトのプログラムを改めて勉強する

さあ、これをSolidityで実装することになるわけですが、その前に改めて前回の仮想通貨のコードを眺めてみることにしましょう。

pragma solidity ^0.4.15;

contract MyToken { 
    string public name;
    string public symbol;
    uint8 public decimals;

    mapping (address => uint256) public balanceOf;

    event Transfer(address indexed from, address indexed to, uint256 value);

    function MyToken(uint256 _supply, string _name, string _symbol, uint8 _decimals) {
        if (_supply == 0) _supply = 1000000;

        balanceOf[msg.sender] = _supply;
        name = _name;     
        symbol = _symbol;

        decimals = _decimals;
    }

    function transfer(address _to, uint256 _value) {
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        balanceOf[_to] += _value;
        balanceOf[_to] -= _value;

        Transfer(msg.sender, _to, _value);
    }
}

一見するとJavaScriptっぽい構文のように見えます。しかし、JavaScriptと違い、静的型付け(変数の型はコンパイル時に決定し、変更できない)言語だということがわかります。また、contractというくくりのclassに近い概念があります。そのメンバー変数として仮想通貨の残高や名前などが、functionとしてcontractに対する操作(作成、送金など)が定義されることがわかります。

変数の型について

Solidityで使える変数の型を抑えておきましょう。
前節のプログラムから

  • 整数型 (uint、int ... etc)
  • 文字列型 (string)
  • 連想配列型 (mapping)
  • Etherアドレス型 (address)

が存在することがわかります。

これらの他

  • bool型 (bool)
  • 配列型 (int[] .. etc)
  • バイト配列型 (bytes32, bytes16 ... etc)
  • 固定小数点型 (fixed128x19 ... etc)

などがあるそうです( https://solidity.readthedocs.io/en/develop/types.html )。ただし、固定小数点型は

Warning

Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from.

と書かれていて、まだ対応は道半ばといったようです。残念ながら、現状浮動小数点型(float、 double)や関数型(functionあるいはlambda)は存在しません。

※すみません、うそです。関数型ありました。訂正します。

整数型にはuint、intといった正負の有り無しの識別の後に、ビットサイズを指定する数値を8〜256の間で8ビット刻みで記述することができます。uint、intといったサイズを指定しない記述はそれぞれ、uint256、int256の短縮構文です。

int / uint: Signed and unsigned integers of various sizes. Keywords uint8 to uint256 in steps of 8 (unsigned of 8 up to 256 bits) and int8 to int256. uint and int are aliases for uint256 and int256, respectively.

配列定義だ!

というわけで、まずは確率と報酬のテーブルを作ってみましょう。
えいや!

function gacha() {
    int[] lotteryWeight = [70, 20, 10];
    int[] reward = [1, 2, 10];
}

スクリーンショット 2017-11-07 21.19.08.png

はい、さっそくコンパイルエラー出ました

おお、Lv.2 勇者kackyよ、配列定義ごときで死んでしまうとはなにごとだ。
エラーメッセージをよく読んで修正するのだ。

browser/GCoin.sol:30:9: TypeError: Type uint8[3] memory is not implicitly convertible to expected type int256[] storage pointer.

ふむふむ、エラー文を読む限り、左辺と右辺で型が異なることによるエラーのようです。右辺の型はuint8[3] memoryとして定義されており、左辺であるint256[] storage型へは入れられないと来ました。なるほど、配列はデフォルトではuint8[]型で定義されるようです。また、固定長配列と可変長配列は別の型として定義されることが読み取れます。そして、memoryとstorage・・・はて?

memory参照とstorage参照

ここで新たなkeywordが生まれました。memoryとstorageというものです。これは何を示しているのでしょう?Solidityでは配列は参照型として扱われます。C言語でいうポインタですね。このポインタが指す領域を指定するkeywordがmemoryとstorageになります。それぞれ、

  • memory ・・・ 揮発性メモリ (トランザクションを終えると消える)
  • storage ・・・ 状態変数 (トランザクションが終わっても残り続ける)

といった特性を持っているので使い分ける必要があります。

また、このkeywordを指定しない場合のデフォルトは、変数を定義する場所によって異なり、

  • グローバル変数 → storage
  • 関数引数 → memory
  • ローカル変数 → storage

となります。なんだかとっても面倒くさい。でも仕様なので仕方ありません。

というわけで、こうしてみます。

function gacha() {
    uint8[3] memory lotteryWeight = [70, 20, 10];
    uint8[3] memory reward = [1, 2, 10];
}

スクリーンショット 2017-11-07 21.54.05.png

今度は無事にコンパイルが通ったようです。でも、なんだかとっても長くて煩わしいですよね。そんなあなたにとっておきのモノがあります。

function gacha() {
    var lotteryWeight = [70, 20, 10];
    var reward = [1, 2, 10];
}

一発解決varです!ただし、JavaScriptのvarとは違い、あくまで型推論により右辺の型と同じものを定義しているに過ぎません。C++でいうauto、C#におけるvarと同じ意味合いです。つまり、varを単独で定義することはできず、必ず右辺値とセットで扱う必要があることを忘れないでください。

ついでにもう一つ。じゃあ、int型の配列を定義するにはどうするねんという話。Solidityは独特で、配列リテラルの型は最初の要素の型に揃えられるという仕様になっています。よってこのように書けばint型の配列が作れます。

function gacha() {
    int[3] memory lotteryWeight = [int(70), 20, 10];
    int[3] memory reward = [int(1), 2, 10];
}

乱数作るぜ!

確率と報酬のテーブルは用意できました。次は報酬を抽選する処理を書きましょう。
JavaScriptなら、

var r = Math.floor(Math.random() * sum);

とか書けばおしまいです。しかし、Solidityには残念ながら乱数を生成するメソッドは用意されていません

/(^o^)\ナンテコッタイ

Lv.2 勇者にとってはここが一番の難題でしょう。しかし、熟練のプログラマーであれば、プログラム上で乱数(疑似乱数)を作るには

  • どっかから予測不能な数値を取得して乱数の種にして
  • 何らかの計算をして所望の乱数を得る

という処理をすればよいことを知っています。乱数の種としては、実行している時刻を持ってくることが多いです。幸いSolidityには時刻をとってくるblock.timestamp(あるいはnow)というメソッドが用意されています。今回は簡易的に、

int r = block.timestamp % sum;

こんな形で 0~(sum-1)の乱数を持ってくることにしましょう。ただし、これをビジネスのコードに使おうなどと絶対に思わないでください。簡単に理由を説明すると、block.timestampは実際には予測不能な数値ではないので、レアアイテムがいくらでも取り放題のクソゲーができあがります。また、block.timestampはブロック毎にしか更新されないため、同じブロックにいた人は全く同じアイテムが取れるという不公平なクソゲーになります。

抽選処理を書く

テーブルと乱数はできたので、抽選処理を書きましょう。ここまでくればあとは学生のプログラミングですね。

function gacha() {
    var lotteryWeight = [70, 20, 10];
    var rewards = [1, 2, 10];
    uint sum = 0;
    uint i;
    for (i = 0; i < lotteryWeight.length; i++) {
        sum += lotteryWeight[i];
    }
    /* ガチャの結果に応じて報酬ゲット */
    uint r = block.timestamp % sum;
    uint index = 0;
    uint s = 0;
    for (i = 0; i < lotteryWeight.length; i++) {
        s += lotteryWeight[i];
        if (s > r) {
            break;
        }
        index++;
    }
    uint reward = rewards[index]; // 報酬
}

これで、
rが 0~69 ⇒ 1
rが 70~89 ⇒ 2
rが 90~99 ⇒ 10
という報酬を返すことができました。

できるプログラマーであれば、メソッドの中でベタにfor文を回すのはダサいですよね。できれば、sumとか、抽選とかはメソッド化してしまいたいものです。やろうと思うとわかるのですが、Solidityでは固定長配列を可変長にすることは認められていないため、

function calcSum(uint8[3] array) returns (uint sum) {
    sum = 0;
    for (uint i = 0; i<array.length; i++) {
        sum += array[i];
    }
}

function sample(uint8[3] array) returns (uint index) {
    uint r = block.timestamp % calcSum(array);
    uint s = 0;
    for (uint i = 0; i < array.length; i++) {
        s += array[i];
        if (s > r) {
            break;
        }
        index++;
    }
}

function gacha() {
    var lotteryWeight = [70, 20, 10];
    var rewards = [1, 2, 10];
    /* ガチャの結果に応じて報酬ゲット */
    uint index = sample(lotteryWeight);
    uint reward = rewards[index]; // 報酬
}

こんな感じで、メソッド引数の配列長が固定されてしまいます。報酬を増やそうと配列長を変えると、メソッドの引数の型も変えなければなりません。ぐぬぬ。JavaやC#でいうところのGenericができるとよいのですがね。

仮想通貨をやりとりする

最後に仮想通貨とEtherをやりとりする処理を書きます。仮想通貨の方は、前回の投稿にあったとおり、残高を保持している連想配列から料金を引き算するだけです。引き算を行う前にassertを入れて残高があることを確認することを忘れないでください。無限にガチャが引けてしまいますよ^^

Etherを送金するにはaddress型のtransferメソッド(sendメソッドでも送れますが、エラー処理が行われないためtransferが推奨されています)を呼び出すことで、そのアドレスへEtherを送金できます。

ということで、

function calcSum(uint8[3] array) returns (uint sum) {
    sum = 0;
    for (uint i = 0; i<array.length; i++) {
        sum += array[i];
    }
}

function sample(uint8[3] array) returns (uint index) {
    uint r = block.timestamp % calcSum(array);
    uint s = 0;
    for (uint i = 0; i < array.length; i++) {
        s += array[i];
        if (s > r) {
            break;
        }
        index++;
    }
}

function gacha() {
    var lotteryWeight = [70, 20, 10];
    var rewards = [1, 2, 10];
    uint fee = 100;
    /* 仮想通貨を消費する */
    assert(balanceOf[msg.sender] >= fee);
    balanceOf[msg.sender] -= fee;
    /* ガチャの結果に応じて報酬ゲット */
    uint index = sample(lotteryWeight);
    uint reward = rewards[index]; // 報酬

    msg.sender.transfer(reward);
}

ひとまず完成しました。パチパチパチ

テスト実行

できたのでひとまず、solidity-browser上で実行してみましょう。contractデプロイだーん。

スクリーンショット 2017-11-08 12.02.00.png

お、gachaボタンありますね。押してみましょう。ポチ。

スクリーンショット 2017-11-08 12.02.29.png

あれ、なんかエラーが出ています。。
デバッガを起動して、調べてみると、

スクリーンショット 2017-11-08 12.07.41.png

msg.sender.transfer(reward);

の部分で落ちてしまうことがわかります。なぜでしょうか?
答えはとても簡単でした。それはこのコントラクトが無一文だからです
Etherを持っていないのにEtherを送ることはできないのは当たり前の話ですよね。。つまり、誰かさん(オーナー)がこのコントラクトにEtherを送ってあげないと、このガチャは実行できないことになります。

Etherの送金処理 payable

Etherを送金する処理を書くにはどうすればよいでしょうか。これは、payableと修飾されたメソッドを用意することで実現します。このメソッドがないコントラクトへはEtherを送金することができません。もしこの状態で本番環境へデプロイしてしまったら、ガチャを引けないガチャコインという笑えない黒歴史が爆誕することになります。デバッグだいじに。

function pay() payable { }

こんな感じで適当に空の送金メソッドを定義しておきます。

solidity-browserでEtherを送金するには、タブの上部にあるパラメータのvalueというところに送金するEtherの量を入力して、payableのメソッドを呼び出せば送金できます。ひとまず50Etherくらい送っておきますか。

スクリーンショット 2017-11-08 12.26.29.png

スクリーンショット 2017-11-08 12.26.58.png

送れました。今、テストアカウントは49.9999...etherを持っています。

では、あらためてガチャを引いてみましょう。えい!

スクリーンショット 2017-11-08 12.28.10.png

今度はエラーは出ませんでした。でも、あれ、Etherが増えてない、むしろ減っているような、どうしてこうなった・・・。

Etherの単位について

ここで、transferの仕様を確認してみましょう。

It is possible to query the balance of an address using the property balance and to send Ether (in units of wei) to an address using the transfer function:
(訳:transfer関数を使用してaddressにEther(weiの単位)を送信することが可能です。)

in units of wei、、weiって何だ?ってなりますね。
実はEtherには複数の単位が存在しています。

Unit Wei Value Wei
wei 1 wei 1
Kwei (babbage) 1e3 wei 1,000
Mwei (lovelace) 1e6 wei 1,000,000
Gwei (shannon) 1e9 wei 1,000,000,000
microether (szabo) 1e12 wei 1,000,000,000,000
milliether (finney) 1e15 wei 1,000,000,000,000,000
ether 1e18 wei 1,000,000,000,000,000,000

Etherの単位換算表は上記の形です。つまり、

msg.sender.transfer(1);

は、1wei = 0.000000000000000001 etherを送ったことになります。どおりで残高が増えないわけです。トランザクションを作成するには手数料としてEtherを使うため、ガチャを引いたらEtherが減ったという現象に陥りました。

では、1etherを送るにはどうすればよいでしょうか。愚直に行けば、

msg.sender.transfer(1000000000000000000);

とすればよいですが、桁が多すぎてコーディング時に間違えてしまいそうです。これを避けるためにSolidityでは次のような簡便な表現ができるようになっています。

msg.sender.transfer(1 ether);

これなら直感的に1etherを送金していることがわかりますね。
ではガチャの報酬をether単位にしましょう。

function gacha() {
    var lotteryWeight = [70, 20, 10];
    var rewards = [1, 2, 10];
    uint fee = 100;
    /* 仮想通貨を消費する */
    assert(balanceOf[msg.sender] >= fee);
    balanceOf[msg.sender] -= fee;
    /* ガチャの結果に応じて報酬ゲット */
    uint index = sample(lotteryWeight);
    uint reward = rewards[index]; // 報酬

    msg.sender.transfer(reward * 1 ether);
}

スクリーンショット 2017-11-08 15.37.25.png

では、あらためてポチりましょう。

スクリーンショット 2017-11-08 15.37.39.png
無事 1etherゲットしましたー。

まとめ

今回のガチャ実装で学んだことをまとめました。

  • Solidityは静的型付け言語なので型を理解しないとコンパイルすら通らない
  • 参照型にはmemory参照とstorage参照が存在する
  • 乱数は独自に頑張って実装する
  • 固定長配列と可変長配列は別もの
  • Etherを入金するにはpayableメソッドを実装する
  • Etherの単位に注意して送金処理を書く

今回の実装でまだ足りない部分はいっぱいありますが、その辺はまたおいおい勉強しましょう。