サイオステクノロジーの菊地啓哉です。今回は、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 の実装
とても深い理由があって(冗談です)、名前は EvilChouhanCaller
Contractとしています。長いのでエビルちゃんと呼びます。
ひとまず、コードをば。
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
関数では、Chouhan0
の bet
関数を呼び出しています。
今回は 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 usingreceive() external payable {...}
(without thefunction
keyword). This function cannot have arguments, cannot return anything and must haveexternal
visibility andpayable
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
でなければなりません。
ただし、virtual
や override
であっても良いです。
この 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
になっている betBy
や firstFlag
を呼び出して確認してしまえば必ず勝ててしまいますし、勝敗が決まる度に transfer
するのもなんだか無駄が多そうですね。その辺りは、割り切って読んでいただければと思います。
まとめ
今回は、丁半博打Contract をつくってみました。また、SmartContract から、別の SmartContract を呼び出す実装や、SmartContract に ETH を送る時には、受け取る側の SmartContract に特別な実装が必要なことなどを確認しました。
また例によって、説明を飛ばしているところがあります。fallback
とか address(this).balance
とかですね。ご興味のある方は調べてみてください。
またかきます
またね