スマートコントラクトの代表的な脆弱性であるReentrancy問題 を考えます。
Reentrancy問題 とは、複数の呼び出し元から同時に呼び出された場合に問題が発生してしまうことを指します。
サンプルケース
例として、コントラクトに送金されたetherをユーザごとに管理し、ユーザは残高分だけ引き出し(返金)できるコントラクトを作成します。
返金する際の流れは下記の通りです。
① 残高を確認する。 ② 残高の全額を引き出す(呼び出し元へ送金) ③ 管理している残高を0にする。
実装(攻撃される側)
サンプルケースを実装したコントラクトのソースは下記の通りです。
[ソースコード]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 pragma solidity ^0.4.11; contract VictimBalance { // アドレス毎に残高を管理 mapping (address => uint) public userBalances; // メッセージ表示用のイベント event MessageLog(string); // 残高表示用のイベント event BalanceLog(uint); /// コンストラクタ function VictimBalance() { } /// 送金される際に呼ばれる関数 function addToBalance() public payable { userBalances[msg.sender] += msg.value; } /// etherを引き出す時に呼ばれる関数 function withdrawBalance() public payable returns(bool) { MessageLog("== withdrawBalance started =="); BalanceLog(this.balance); // ①残高を確認 MessageLog("1. check balances"); if(userBalances[msg.sender] == 0) { MessageLog("No Balance."); return false; } // ②呼出し元に返金 MessageLog("2. send ether."); if (!(msg.sender.call.value(userBalances[msg.sender])())) { throw; } // ③残高を更新 MessageLog("3. update balances."); userBalances[msg.sender] = 0; MessageLog("== withdrawBalance finished =="); return true; } }
返金処理はwithdrawBalance関数 (22行目)で行います。
この関数では、マップを使って呼び出し元のアドレスの残高を確認し、残高が0でなければ呼び出し元に全て返金し、返金が完了したらマップで管理している残高を0にしています。
MessageLog関数 を使って、どの処理まで行われたかを確認できるようにしてあります。
実は、このコントラクトはReentrancy問題 を抱えています。
次回は、このコントラクトに対して攻撃を行うコントラクトを実装します。