Skip to content
On this page

スマートコントラクトアドレスの予測可能性とセキュリティリスク:Ethernaut Recoveryチャレンジの詳細解説

はじめに

Ethereumスマートコントラクトの世界では、コントラクトアドレスの生成方法がセキュリティにおいて重要な要素となります。EthernautのRecoveryチャレンジは、コントラクトアドレスの予測可能性に潜むリスクを実践的に学ぶための優れた教材です。本記事では、このチャレンジを通じて、コントラクトアドレス生成の仕組み、その予測可能性、および関連するセキュリティ対策について詳細に解説します。

チャレンジ概要と背景

問題設定

Recoveryチャレンジでは、ユーザーがトークンファクトリーコントラクトをデプロイし、そのファクトリーを通じて新しいトークンコントラクトを作成するシナリオが提示されています。問題の核心は以下の点にあります:

  1. コントラクト作成者が最初のトークンコントラクトを作成した後、0.001 etherを送金して追加トークンを取得
  2. その後、作成したトークンコントラクトのアドレスを紛失
  3. 目的は、紛失したコントラクトアドレスから0.001 etherを回収(または削除)すること

この問題は一見すると不可能に見えますが、Ethereumのコントラクトアドレス生成アルゴリズムを理解することで解決可能です。

技術的背景:Ethereumコントラクトアドレス生成

CREATEオペコードの仕組み

Ethereumでは、newキーワードまたはCREATEオペコードを使用してコントラクトをデプロイする際、生成されるコントラクトアドレスは決定論的に計算されます。この計算式は以下の要素に基づいています:

address = keccak256(rlp.encode([sender_address, nonce]))[12:]

ただし、このチャレンジで使用されているSolidityバージョン(^0.8.0)では、CREATE2オペコードが使用される場合があります。実際のコードではnew SimpleToken(...)を使用しているため、CREATEオペコードが使用されます。

コントラクトからのコントラクト作成

重要な点は、Recoveryコントラクト(ファクトリー)からSimpleTokenコントラクトが作成されることです。この場合:

  • 送信者アドレス:Recoveryコントラクトのアドレス
  • Nonce:Recoveryコントラクトが作成したコントラクトの数(この場合は最初のコントラクトなのでnonce=1)

しかし、実際の計算はより複雑で、EIP-161で規定された仕様に従います。

ソリューションの詳細な技術解説

アドレス計算の実際の実装

提供されたソリューションコードでは、CREATEオペコードを使用したコントラクトアドレスの計算方法が示されています:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AddressRecovery {
    constructor() {}
    function cAddrRecover(address _creator) external pure returns (address) {
        return address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            bytes1(0xd6), 
                            bytes1(0x94), 
                            _creator, 
                            bytes1(0x01)
                        )))));
    }
}

計算式の分解

この計算式を分解して理解しましょう:

  1. エンコードされたデータ

    • bytes1(0xd6): RLPエンコーディングのプレフィックス(0xd6 = 0xc0 + 0x16)
    • bytes1(0x94): アドレス長のプレフィックス(0x94 = 0x80 + 0x14)
    • _creator: ファクトリーコントラクトのアドレス
    • bytes1(0x01): nonce値(最初のコントラクトなので1)
  2. 計算プロセス

    • abi.encodePacked(): パラメータを連結
    • keccak256(): ハッシュ計算
    • 型変換: uint256uint160address
  3. RLPエンコーディングの詳細: RLP(Recursive Length Prefix)エンコーディングは、Ethereumでデータをシリアライズする標準的な方法です。コントラクトアドレス計算では、送信者アドレスとnonceのRLPエンコードされたデータのKeccak256ハッシュの最後の20バイト(160ビット)が使用されます。

テストコードの分析

提供されたTypeScriptテストコードは、実際の環境でソリューションを検証する方法を示しています:

typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { Recovery, AddressRecovery } from "../typechain-types";

describe("Recovery", function () {
  describe("Recovery testnet online sepolia", function () {
    let addressRecovery: AddressRecovery;

    before(async () => {
      const AddressRecoveryFactory = await ethers.getContractFactory("AddressRecovery");
      addressRecovery = (await AddressRecoveryFactory.deploy()) as AddressRecovery;
      await addressRecovery.waitForDeployment();
    });

    it("testnet online sepolia Recovery", async function () {
      const CREATOR = "0x..."; // 実際のファクトリーコントラクトアドレス
      const simpleTokenAddr = await addressRecovery.cAddrRecover(CREATOR);
      const SimpleTokenFactory = await ethers.getContractFactory("SimpleToken");
      const simpleTokenAbi = SimpleTokenFactory.interface.format();

      const deployer = await ethers.getNamedSigner("deployer");
      const simpleTokenContract = new ethers.Contract(simpleTokenAddr, simpleTokenAbi, deployer);

      const tx = await simpleTokenContract.destroy(deployer.address);
      await tx.wait();

      const ethBalance = await ethers.provider.getBalance(simpleTokenAddr);
      expect(ethBalance).to.be.equals(0);
    });
  });
});

セキュリティリスクと対策

予測可能なアドレスのリスク

  1. 資金の盗難: 攻撃者が将来作成されるコントラクトのアドレスを事前に計算し、そのアドレスにETHを送金してコントラクトを乗っ取る可能性
  2. フロントランニング: トランザクションがマイニングされる前に、攻撃者が同じアドレスにコントラクトをデプロイする可能性
  3. リエントランシー攻撃: 予測可能なアドレスを悪用した高度な攻撃パターン

緩和策

  1. CREATE2の使用: ソルト値を導入することでアドレスの予測を困難に
  2. コントラクト作成の制限: 重要な資金を保持するコントラクトは、信頼できるエンティティのみが作成できるようにする
  3. アドレス計算の複雑化: 複数の要素を組み合わせたアドレス生成アルゴリズムの採用

実践的な応用例

マルチシグウォレットのアドレス事前計算

予測可能なアドレス生成は、必ずしも悪いことではありません。マルチシグウォレットのような場合、コントラクトアドレスを事前に計算しておくことで、デプロイ前に資金を送金することが可能になります。

solidity
// マルチシグウォレットファクトリーの例
contract MultiSigWalletFactory {
    function computeFutureAddress(address[] memory owners, uint256 required)
        public
        view
        returns (address)
    {
        bytes32 salt = keccak256(abi.encode(owners, required, block.chainid));
        bytes memory bytecode = type(MultiSigWallet).creationCode;
        bytes32 hash = keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(bytecode)
        ));
        return address(uint160(uint256(hash)));
    }
    
    function createWallet(address[] memory owners, uint256 required)
        public
        returns (address)
    {
        bytes32 salt = keccak256(abi.encode(owners, required, block.chainid));
        MultiSigWallet wallet = new MultiSigWallet{salt: salt}(owners, required);
        return address(wallet);
    }
}

高度なトピック:CREATE2の詳細

CREATE2のアドレス計算式

CREATE2では、より柔軟なアドレス計算が可能です:

address = keccak256(0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12:]

ここで:

  • 0xff: CREATE2を識別するための定数バイト
  • senderAddress: コントラクト作成者のアドレス
  • salt: 任意の32バイト値
  • init_code: コントラクトの初期化コード

セキュリティ強化のためのCREATE2活用

solidity
contract SecureTokenFactory {
    mapping(bytes32 => bool) private usedSalts;
    
    function createToken(string memory name, uint256 initialSupply, bytes32 salt)
        public
        returns (address)
    {
        require(!usedSalts[salt], "Salt already used");
        usedSalts[salt] = true;
        
        // コントラクトの作成
        SimpleToken token = new SimpleToken{salt: salt}(name, msg.sender, initialSupply);
        return address(token);
    }
    
    function computeAddress(string memory name, uint256 initialSupply, bytes32 salt)
        public
        view
        returns (address)
    {
        bytes memory initCode = abi.encodePacked(
            type(SimpleToken).creationCode,
            abi.encode(name, address(this), initialSupply)
        );
        
        bytes32 hash = keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(initCode)
        ));
        
        return address(uint160(uint256(hash)));
    }
}

結論

EthernautのRecoveryチャレンジは、Ethereumスマートコントラクトの基本的な特性であるアドレスの予測可能性について深く理解する機会を提供します。この知識は、セキュアなスマートコントラクト開発において不可欠です。

重要なポイントをまとめると:

  1. CREATEオペコードを使用したコントラクトアドレスは決定論的に計算可能
  2. この特性は、マルチシグウォレットの事前計算など有用な用途もある
  3. 一方で、セキュリティリスクも存在するため、適切な対策が必要
  4. CREATE2を使用することで、より制御可能で安全なアドレス生成が可能

スマートコントラクト開発者は、これらの特性を理解し、適切な設計パターンを選択することで、より安全で堅牢な分散型アプリケーションを構築することができます。アドレス生成の仕組みを深く理解することは、Ethereumエコシステム全体のセキュリティ向上に貢献する重要なステップです。

Built with AiAda