Solidity で丁半博打プログラムを作る

ETH で丁半博打

サイオステクノロジーの菊地啓哉です。今回は、Solidity でなんちゃって丁半博打を仲介してくれる SmartContract を作ります。

対象

  • Solidity で簡単な SmartContract を作れる人

前回の記事を読んでいただけているなら充分かと思います。

開発する SmartContract の概要

  • 1 Gwei を賭けて、丁半博打を行います。
  • true/false のどちらかを flag として指定します。
  • 先攻、後攻の参加者が居て、両者の flag が一致すると後攻の勝ち、一致しなかった場合は先攻の勝ちとなります。
  • 勝者には、2 Gwei が送られます。

このプログラムを作るだけだと少し寂しいので、上記の丁半博打Contract を呼び出す、別の SmartContract を用意して少し遊びます。

丁半博打Contract

大したことは書いていないので、サクっとコードを紹介します。ここでは丁半くんと呼びます。

contracts/Chouhan0.sol


// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 

bet関数の処理では、まず送られた ETH が参加費の 1 Gweiとなっているか、チェックします。その後は、先攻と後攻で処理が分かれます。先攻の処理では、アドレスとフラグを記録しています。後攻の処理ではフラグを基に勝者を判定し、勝者に対して ETH を送ります。

address型変数の初期値は address(0) で表すことができます。変数の初期化に delete を使うこともできるのですが、わかりやすく address(0) の代入としています。

はい、これで丁半くんは完成です。

実際に動かしていただくと、きっと期待する動きになっているかと思います。Remix で動かす際には、flag には true または false を入力して実行してください。

丁半博打Contract を呼び出す SmartContract の実装

とても深い理由があって(冗談です)、名前は EvilChouhanCallerContractとしています。長いのでエビルちゃんと呼びます。

ひとまず、コードをば。

contracts/EvilChouhanCaller.sol


// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import "./Chouhan0.sol";

/**
 * @custom:dev-run-script ./scripts/deployEvilChouhanCaller.ts
 */
contract EvilChouhanCaller{
    Chouhan0 chouhan;

    constructor(address _chouhan){
        chouhan = Chouhan0(_chouhan);
    }

    function bet(bool _flag) public payable{
        return chouhan.bet{value: msg.value}(_flag);
    }
    
    function withdrawAll()public{
        payable(msg.sender).transfer(address(this).balance);
    }
}

まず、constructor があります。これは deploy 時に実行されます。ここでは、deploy 済みの丁半くんのアドレスを受け取って、Chouhan0型として保持しています。ここで、Chouhan0型は import することで、bet関数を持っていると期待していること等をコード内で認識できています。

bet関数では、Chouhan0bet関数を呼び出しています。

今回は ETH を送る必要があるため、送る量を valueで指定しています。(参考

ここで、deploy には、丁半くんのアドレスが必要になるので、deploy script は以下のようにして、丁半くんとエビルちゃんをセットで deploy しています。

scripts/deployEvilChouhanCaller.ts


import { deploy } from './ethers-lib'

(async () => {
    try {
        const chouhanResult = await deploy('Chouhan0', []);
        console.log(`chouhan address: ${chouhanResult.address}`);
        const evilResult = await deploy("EvilChouhanCaller", [chouhanResult.address]);
        console.log(`evil address: ${evilResult.address}`);
    } catch (e) {
        console.log(e.message);
    }
  })();

上記のエビルちゃんで丁半くんの bet関数を呼び出してみます。先攻で呼び出すと、正常に実行することができます。前回も書いたように、SmartContract は Ethereum アカウントの一種なので、期待通りの動きになります。

しかし、他のアカウントが後攻で、エビルちゃんが勝つようにしてしまうと、エラーになってしまいます。あるいは、先攻で誰かが bet関数を呼び出し済みの状態で、エビルちゃんが後攻で勝ってもエラーになります。

私が Remix で実行したところ、以下のようなエラーになりました。

revert
    The transaction has been reverted to the initial state.
Note: The called function should be payable if you send value and the value you send should be less than your current balance.
Debug the transaction to get more information.

どうやら、payable でない関数が呼ばれているようです。先攻では正常に実行できているので、transfer あたりが怪しそうです。やや天下り的かもしれませんが、次でこのエラーを解消します。

エラーを起こさず丁半博打Contract を呼び出す SmartContract の実装

次のように、NormalChouhanCaller を実装します。ノーマルちゃんと呼びます。

contracts/NormalChouhanCaller.sol


// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import "./Chouhan0.sol";

/**
 * @custom:dev-run-script ./scripts/deployNormalChouhanCaller.ts
 */
contract NormalChouhanCaller{
    Chouhan0 chouhan;

    constructor(address _chouhan){
        chouhan = Chouhan0(_chouhan);
    }

    receive() external payable{}

    function bet(bool _flag) public payable{
        return chouhan.bet{value: msg.value}(_flag);
    }
    
    function withdrawAll()public{
        payable(msg.sender).transfer(address(this).balance);
    }
}

エビルちゃんとの実質的な違いは、receiveという何かが定義されていることです。

英語のドキュメントですが、Receive Ether Function を見てみると、次のように書かれています。

A contract can have at most one receive function, declared using receive() external payable {...} (without the function keyword). This function cannot have arguments, cannot return anything and must have external visibility and payable state mutability. It can be virtual, can override and can have modifiers.

The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via.send()or.transfer()). If no such function exists, but a payable fallback  function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through a transaction that does not represent a payable function call and throws an exception.

拙訳)
SmartContract は receive関数を定義できます。
receive() external payable { ... } のシグネチャで、関数なのに functionキーワードはありません。
また、引数を持つこともできず、値を返すこともできず、external かつ payable でなければなりません。
ただし、virtualoverride であっても良いです。

この receive関数は、calldata の無い Contract呼び出しで呼ばれます。この SmartContract に対して、ただの Ether の送付(.send().transfer())で実行されます。
receive関数を持たない場合、ただの Ether の送付に対して、fallback関数が呼ばれます。(※ fallback はもう少し広く使われるものです。)
SmartContract が receive関数も fallback関数も定義されていない場合、ただの Ether の送付で SmartContract が呼ばれると、その SmartContract は Ether を受け取ることはできず、exception を throw します。

(拙訳おわり

見つけてしまえばすごく、「コレっぽい」ことが書いてますね。

そして、実行してみると、確かにノーマルちゃんが勝つパターンも正常に実行できることを確認できるかと思います。ノーマルちゃんが受け取った ETH を EOA が引き出すには、withdrawAll関数を呼び出します。

補足

今回つくった丁半博打Contract は、冒頭に「なんちゃって」と書いてあるように、あくまで学習用のプログラムです。例えば、後攻は先攻が賭けた内容を Etherscan などで確認したり、public になっている betByfirstFlag を呼び出して確認してしまえば必ず勝ててしまいますし、勝敗が決まる度に transferするのもなんだか無駄が多そうですね。その辺りは、割り切って読んでいただければと思います。

まとめ

今回は、丁半博打Contract をつくってみました。また、SmartContract から、別の SmartContract を呼び出す実装や、SmartContract に ETH を送る時には、受け取る側の SmartContract に特別な実装が必要なことなどを確認しました。

また例によって、説明を飛ばしているところがあります。fallback とか address(this).balance とかですね。ご興味のある方は調べてみてください。

 

またかきます

またね

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です