Ethereum(36) - Reentrancy問題④(実装編 - 改善版)

Reentrancy問題を解決するためのソース修正を行います。

処理フロー検討

問題のあったコントラクトの返金処理フローは下記のようになっていました。

 ① 残高を確認する。
 ② 残高の全額を引き出す(呼び出し元へ送金)
 ③ 管理している残高を0にする。

問題が発生した原因は、②の処理中に再度返金処理をコールされる可能性があるからでした。

この場合、③の残高を0にする前に①と②が再度実行されてしまい、再び送金処理が実行されてしまいます。

ということは②の送金処理の前に③の残高を0にする処理を実行すれば問題が解決するような気がします。

処理フロー(改善版)

処理フローを次のように改善します。

 ① 残高を確認する。
 ② 残高更新前に送金額を退避する。
 ③ 管理している残高を0にする。
 ④ 残高の全額を引き出す(呼び出し元へ送金)

④の送金する前に③の管理残高を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
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);

// ①残高を確認
if(userBalances[msg.sender] == 0) {
MessageLog("No Balance.");
return false;
}

// ②残高更新前に送金額を退避
uint amount = userBalances[msg.sender];

// ③残高を更新
userBalances[msg.sender] = 0;

// ④呼出し元に返金
if (!(msg.sender.call.value(amount)())) { throw; }

MessageLog("=== withdrawBalance finished. ==");
return true;
}
}

修正箇所は23行目から35行目の返金フローです。


次回は、このコントラクトに対して再度同じ攻撃を行い問題が解決しているかどうかを確認します。