Solidity

【Solidity】第7章第2回:再入可能性攻撃(Reentrancy)の防止策

本記事では、再入可能性攻撃(Reentrancy Attack)の基本的な仕組みと、その防止策について解説します。再入可能性攻撃は、スマートコントラクトのセキュリティ上の大きなリスクであり、適切な対策を講じることが重要です。

0. 記事の概要

この記事を読むメリット

  • 再入可能性攻撃の仕組みを理解:攻撃が発生する原因と仕組みを学べます。
  • 防止策を習得:セキュアなスマートコントラクト設計が可能になります。
  • 実践的なスキルの向上:安全なDAppを構築するための知識を得られます。

この記事で学べること

  • 再入可能性攻撃の基本概念と影響
  • 攻撃の防止策と具体的なコード例
  • 再入可能性攻撃を考慮したベストプラクティス

1. 再入可能性攻撃とは?

1.1 基本的な仕組み

再入可能性攻撃とは、スマートコントラクトの関数を外部コントラクトから繰り返し呼び出し、意図しない動作を引き起こす攻撃手法です。この攻撃は主に以下の条件が揃った場合に発生します:

  • 外部アカウントやコントラクトへの送金が行われる
  • 送金後に状態が更新される

1.2 被害の実例

過去には、再入可能性攻撃による重大な被害が発生しています:

  • DAO事件:6000万ドル以上のETHが盗まれる結果となりました。
  • Bankor事件:流動性プールの資金が抜き取られる被害が発生しました。

2. 再入可能性攻撃の具体例

2.1 脆弱なコード例

// 脆弱なスマートコントラクトの例
pragma solidity ^0.8.0;

contract Vulnerable {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
        balances[msg.sender] -= amount;
    }
}

動作解説

このコードでは、送金処理が完了する前に、balancesが更新されていません。このため、攻撃者がwithdrawを繰り返し呼び出すことで、不正に資金を引き出すことが可能です。

3. 再入可能性攻撃の防止策

3.1 チェック・エフェクト・インタラクションの原則

// セキュアなスマートコントラクト例
pragma solidity ^0.8.0;

contract Secure {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

動作解説

このコードでは、balancesを更新してから送金処理を行うことで、再入可能性攻撃を防いでいます。

3.2 ReentrancyGuardの使用

// ReentrancyGuardを利用した例
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureWithGuard is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

動作解説

このコードでは、OpenZeppelinのReentrancyGuardを使用して再入可能性攻撃を防いでいます。nonReentrant修飾子を追加することで、再入可能性を防ぎます。

4. 練習問題

以下の課題に挑戦してみましょう:

  1. 再入可能性攻撃を防ぐためにReentrancyGuardを使用して安全な送金関数を実装してください。
  2. チェック・エフェクト・インタラクションの原則に基づいたコードを作成し、再入可能性攻撃を防いでください。

5. まとめ

本記事では、再入可能性攻撃の仕組みとその防止策について詳しく解説しました。セキュリティを考慮したスマートコントラクトを設計することで、より安全なDAppを構築することが可能です。