Ethereum contractからetherを送るときに、安易にtransferやsendを使うと危ないという話。


Ethereum上で動くゲームを現在開発中で、スマートコントラクトではお決まりの?
コントラクトに送金したお金(ETH)を引き出す、という処理をする必要が出て、solidityのドキュメントを見ていたら write your contract using a pattern where the recipient can withdraw Ether instead.(受取人がETHをwithdraw(引き出す)するパターンを使ってコントラクトを実装しなさい) と書いてあり、そのあたりを少し調べたので備忘録に。

一応、下記のドキュメントで一通り説明してあるので、こちら読んでもいいと思います。
http://solidity.readthedocs.io/en/develop/security-considerations.html?highlight=call.value#sending-and-receiving-ether

contractでETHを受け取る

ここら辺に書いてありますが、contract は、fallback 関数を実装することでETHを受け取ることができるようになります。
ICOならtokenと引き換えにETHをcontractが受け取る、みたいな感じですね。そんで、その受け取ったETHをあるユーザのアカウントに送金したり、払い戻ししたりする処理をするときに、<address>.send<address>.transfer を安易に使うとsecurity上問題があるらしいのです。

ETHをtransferで送金する場合の問題点

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

<address>.transfer は こちらに書いてあるように、transferは、引数がcontractのアドレスだった場合に、そのfallback 関数を実行します。そしてもしそのfallback関数がエラーを起こしたり、fallback関数を実行するためにgasが不足したりすると、現在実行中のcontractの処理が止まってしまいます。

ETHをsendで送金する場合の問題点

一方、sendは call stack depth が1024に達するとエラーをはき、これもまた処理を中断させてしまいます。
そしてこのcall stack depthは関数の呼び出し側がコントロールすることができるので、悪意を持ったユーザーが攻撃するとエラーを引き起こせる、ということです。

以上のように、transferもsendも多少難点?があり、sendを使いつつ、返り値の値を確認して(sendはエラーの場合はfalseが返ってくる)色々やるのが良いですね。
と書いてあるのですが、そうではなくて、withdraw patterを使って解決するのがもっと良いですよ、ということらしいです。

Withdraw Pattern

下記のサンプルはオークション系のContractですね。(おそらく)
becomeRichest関数では、もっともETHを支払ったユーザーとそのvalueを設定しています。
withdraw関数が上記に示したような問題点を解決する、withdraw patternによる、ETHの引き出し(送金)です。

Withdraw pattern

pragma solidity ^0.4.11;

contract WithdrawalContract {
    address public richest;
    uint public mostSent;

    mapping (address => uint) pendingWithdrawals;

    function WithdrawalContract() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            pendingWithdrawals[richest] += msg.value;
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // Remember to zero the pending refund before
        // sending to prevent re-entrancy attacks
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

Withdraw patternじゃない場合

pragma solidity ^0.4.11;

contract SendContract {
    address public richest;
    uint public mostSent;

    function SendContract() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            // This line can cause problems (explained below).
            richest.transfer(msg.value);
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }
}

withdraw patterじゃない場合は、richest を決定する処理と、変更があった場合にETHを返金する処理(transfer)が同じ関数becomeRichestのなかで行われているため、transferのfallback関数がエラーになるような悪意のある攻撃をされると、contractのstateが更新できなくなる可能性があります。(この場合richest変数が更新できなくなる。)

一方、下記のようなwithdraw関数を導入することで、richestの決定と、返金の処理を分けることで、もしwithdraw関数内のtransferがエラーになるような攻撃をユーザーがしたとしても、そのユーザーに関係するstate(この場合はpendingWithdrawals['userのaddress'])のみが変更不可能になるだけで、他のユーザーに関係する変数の更新が妨げられることはありません。

function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // Remember to zero the pending refund before
        // sending to prevent re-entrancy attacks
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }

という感じで、contractに対して送金をさせ、さらにそれを返金するような処理を含む場合は気をつけましょう、ということでした。

何か間違いや気になる点などあったらコメントでバンバンお願いいたします。
あとEthereumで面白いアプリ作れないかなーと毎日考えてるので面白いアイデアあったら、コメントや[email protected]まで連絡ください!

面白そうだったら一緒に作りましょう!