はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、コントラクトのバイトコードを記録して、コントラクトの発見や検証をサポートする仕組みを提案しているERC7744についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

ERC7744では、Ethereumのコントラクトをバイトコードのハッシュによってインデックス化する標準インターフェースを提案しています。 従来のアドレスベースの識別方法では、プロキシを利用することで悪意あるコードを隠すことが可能ですが、バイトコードのハッシュを基準にすることでコードの発見や検証が信頼性を持って行えるようになります。 これにより、バイトコードの署名、ホワイトリスト登録、分散型配布メカニズムなどのユースケースが実現できます。

動機

現在のEthereumにおけるコントラクトの発見方法はアドレスに依存しています。 しかし、アドレスはコントラクトがデプロイされるたびに変わるため、同じバイトコードを持つコントラクトでも異なるアドレスを持ちます。 また、プロキシコントラクトを用いることで、使用されるアドレスと実際のロジックを分離できるため、コードの識別が困難になります。

このような課題を解決するために、バイトコードのハッシュによる一意の識別方法を導入してコントラクトコードの透明性と信頼性を向上させます。

セキュリティ監査とコード検証の向上

例えば、あるセキュリティ監査人が、コントラクトの安全性を確認してそれを保証したい場合を考えます。 現在の方法では、監査人はコントラクトのデプロイされたアドレスを確認し、デプロイ時のパラメータやストレージの状態も含めて精査する必要があります。 しかし、アドレスだけでは本当に監査対象のバイトコードが実行されるかどうかを保証できません。

これに対し、バイトコードのハッシュを基準とした識別を導入すれば、監査人はコードの内容そのものを直接精査できます。 アドレスによる識別ではなく、コードの真正性そのものを検証できるため、バイトコードが改変されていないことを保証することができます。 これにより、監査対象が明確になり監査プロセスの信頼性が向上します。

デプロイ前のホワイトリスト登録

バイトコードのハッシュを用いた識別により、コントラクトがデプロイされる前にホワイトリスト登録することが可能になります。 これは、例えば以下のようなユースケースに役立ちます。

  • 開発者が事前にコードの承認を受ける

一部のプロトコルでは、特定のルールを満たしたコントラクトのみがやりとりできるようにする必要があります。 バイトコードのハッシュを登録することで、事前に承認されたコードのみをデプロイ可能にする仕組みを構築できます。

  • インフラの自動適応

例えば、特定のバイトコードがチェーン上にデプロイされた瞬間に、それを認識して動作を変更するコントラクトやオフチェーンサービスが考えられます。 ホワイトリスト登録によって、事前に定義されたコードが出現したときに、それに対応するアクションを取ることが可能になります。

プロキシを利用した攻撃や不正変更の防止

Ethereumでは、プロキシパターンを用いることで、使用されるアドレスは変更せずに実行されるコードだけを差し替えることが可能です。 この手法を悪用すれば、ある時点で正当なコードとして承認されたコントラクトが、後から悪意のあるコードに置き換えられる可能性があります。

しかし、バイトコードのハッシュに基づく識別を用いることで、本来意図されたコードが実行されているかどうかを直接チェックする仕組みを導入できます。 これは、長期的な信頼を構築する上で重要な要素であり、エンドユーザーに対しても透明性を確保する手段となります。

ガスコスト削減とリソース最適化

DAppsを開発する時、多くのコントラクトが共通のロジックを使用します。 しかし、従来の手法では、同じコードを持つコントラクトを個別にデプロイしなければならず、そのたびに大量のガスを消費していました。

バイトコードのハッシュを用いたインデックスが確立されると、開発者は既存のコードを参照するだけで済むため、新しくデプロイする必要がなくなります。 このアプローチにより、コントラクトの開発・運用にかかるコストを大幅に削減できます。

ERC標準としての必要性

このコードインデックス機能をEthereumの標準(ERC)として規定する理由は、以下のような点にあります。

  • エコシステム全体の統一性

個々のプロジェクトが独自のバイトコード識別方法を実装すると、互換性のない異なる方式が乱立し、エコシステムの分断を招きます。 ERCとして標準化することで、どのプロジェクトでも統一された方法でバイトコードの識別・検索ができるようになります。

  • 簡単な統合と導入

コントラクト開発者が統一されたインターフェースを利用できるため、新しいプロジェクトでも簡単にこの機能を取り入れることが可能になります。

  • 幅広いユースケースに対応できる柔軟性

バイトコードの識別は、監査、ホワイトリスト管理、コードの参照、ガスコスト削減など、多様なユースケースに対応可能です。 そのため、Ethereumの標準規格として策定することで、より多くの開発者が恩恵を受けられます。

仕様

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.28;

interface IERC7744 {
    event Indexed(address indexed container, bytes32 indexed codeHash);
    error alreadyExists(bytes32 id, address source);

    function register(address container) external;

    function get(bytes32 id) external view returns (address);
}

/**
 * @title Byte Code Indexer Contract
 * @notice You can use this contract to index contracts by their bytecode.
 * @dev This allows to query contracts by their bytecode instead of addresses.
 * @author Tim Pechersky (@Peersky)
 */
contract ERC7744 is IERC7744 {
    mapping(bytes32 => address) private index;

    function isValidContainer(address container) private view returns (bool) {
        bytes memory code = container.code;
        bytes32 codeHash = container.codehash;
        bytes32 eip7702Hash = bytes32(0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329);
        // Contract should have non-empty code and valid codehash
        return (code.length > 0 && codeHash != bytes32(0) && codeHash != eip7702Hash);
    }

    /**
     * @notice Registers a contract in the index by its bytecode hash
     * @param container The contract to register
     * @dev `msg.codeHash` will be used
     * @dev It will revert if the contract is already indexed or if returns EIP7702 hash
     */
    function register(address container) external {
        address etalon = index[container.codehash];
        require(isValidContainer(container), "Invalid container");
        if (etalon != address(0)) {
            if (isValidContainer(etalon)) revert alreadyExists(container.codehash, container);
        }
        index[container.codehash] = container;
        emit Indexed(container, container.codehash);
    }

    /**
     * @notice Returns the contract address by its bytecode hash
     * @dev returns zero if the contract is not indexed
     * @param id The bytecode hash
     * @return The contract address
     */
    function get(bytes32 id) external view returns (address) {
        return index[id];
    }
}

IERC7744

javascript
interface IERC7744 {
    event Indexed(address indexed container, bytes32 indexed codeHash);
    error alreadyExists(bytes32 id, address source);

    function register(address container) external;

    function get(bytes32 id) external view returns (address);
}

概要
Ethereumのコントラクトをバイトコードのハッシュでインデックス化するための標準インターフェース。

詳細
このインターフェースは、コントラクトをそのバイトコードのハッシュを基に登録・取得するための機能を提供します。
特定のアドレスではなく、バイトコードの一意な識別子であるcodeHashを使うことで、コードの信頼性や識別性を向上できます。

  • Indexed
    • 特定のコントラクトがバイトコードのハッシュとともに登録されたことを通知するイベント。
  • alreadyExists
    • 登録しようとしたバイトコードのハッシュが既に登録されている場合に発生するエラー。
  • register
    • 特定のコントラクトのバイトコードをインデックスに登録する関数。
  • get
    • 登録済みのバイトコードハッシュに対応するコントラクトアドレスを取得する関数。

Indexed

javascript
event Indexed(address indexed container, bytes32 indexed codeHash);

概要
コントラクトがバイトコードのハッシュとともに登録されたことを通知するイベント。

詳細
このイベントは、register関数を実行した時に発行され、登録されたコントラクトのアドレス (container) と、そのバイトコードのハッシュ (codeHash) を記録します。 これにより、ブロックチェーン上でどのコードが登録されたのかを追跡できるようになります。

パラメータ

  • container
    • 登録されたコントラクトのアドレス。
  • codeHash
    • 登録されたコントラクトのバイトコードのハッシュ。

alreadyExists

javascript
error alreadyExists(bytes32 id, address source);

概要
登録しようとしたバイトコードハッシュが既に存在する場合に発生するエラー。

詳細
新しいコントラクトをインデックスに追加する時に、既に同じバイトコードハッシュのコントラクトが登録されていた場合にこのエラーが返されます。 これにより、同じバイトコードの重複登録を防ぐことができます。

パラメータ

  • id
    • 既に登録されているバイトコードのハッシュ。
  • source
    • 登録しようとしたコントラクトのアドレス。

index

javascript
mapping(bytes32 => address) private index;

概要
バイトコードのハッシュとコントラクトのアドレスを対応付けるマッピング。

詳細
このマッピングは、bytes32型のバイトコードハッシュをキーとし、それに対応するコントラクトアドレスを値として保持します。 これにより、コントラクトのバイトコードを基に特定のアドレスを取得できるようになります。

パラメータ

  • bytes32
    • バイトコードのハッシュ。
  • address
    • 対応するコントラクトのアドレス。

isValidContainer

javascript
function isValidContainer(address container) private view returns (bool) {
    bytes memory code = container.code;
    bytes32 codeHash = container.codehash;
    bytes32 eip7702Hash = bytes32(0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329);
    return (code.length > 0 && codeHash != bytes32(0) && codeHash != eip7702Hash);
}

概要
コントラクトのアドレスが有効かどうかを検証する関数。

詳細
この関数は、指定されたcontainerのバイトコードの存在とハッシュの有効性を確認し、有効なコントラクトかどうかを判定します。 特に、EIP7702の特殊なコードハッシュが含まれる場合は無効とします。

EIP7702については以下の記事を参考にしてください。

引数

  • container
    • 検証対象のコントラクトアドレス。

戻り値

  • bool
    • 有効な場合はtrue、無効な場合はfalse。

register

javascript
function register(address container) external {
    address etalon = index[container.codehash];
    require(isValidContainer(container), "Invalid container");
    if (etalon != address(0)) {
        if (isValidContainer(etalon)) revert alreadyExists(container.codehash, container);
    }
    index[container.codehash] = container;
    emit Indexed(container, container.codehash);
}

概要
新しいコントラクトをバイトコードのハッシュをキーとしてインデックスに登録する関数。

詳細
この関数は、指定されたcontainerのバイトコードのハッシュを取得し、それをキーとしてindexに登録します。
既に同じバイトコードが登録されている場合はエラー (alreadyExists) を返します。
isValidContainerを用いて、コードが空ではないことと無効なハッシュを持たないことを確認します。
正常に登録された場合、Indexedイベントを発行します。

引数

  • container
    • 登録するコントラクトのアドレス。

get

javascript
function get(bytes32 id) external view returns (address) {
    return index[id];
}

概要
指定されたバイトコードハッシュに対応するコントラクトアドレスを取得する関数。

詳細
この関数は、indexに格納されたバイトコードハッシュ (id) に対応するコントラクトのアドレスを返します。
登録されていない場合は0x0(アドレスのゼロ値)を返します。
これにより、外部からコントラクトのバイトコードを基に対応するアドレスを検索できます。

引数

  • id
    • 取得したいコントラクトのバイトコードハッシュ。

戻り値

  • address
    • バイトコードハッシュに対応するコントラクトアドレス。登録がない場合は 0x0。

デプロイ方法

CodeIndexコントラクトは0xC0dE1D2F7662c63796E544B2647b2A94EE658E07にデプロイされています。 このアドレスは、CREATE2命令を使用して決定論的にデプロイされています。

デプロイ時には0x70b27c94ed692bfb60748cee464ef910d4bf768ac1f3a63eeb4c05258f629256というsalt(CREATE2によるデプロイ時にコントラクトアドレスを決定するための任意のデータ)が指定されています。 このsaltは、バニティアドレス(特定の意味を持つアドレス)を得るために調整されたもので、生成されたアドレスには"Code ID"(0xC0dE1D...)という文字列が含まれています。

補足

バイトコードを基準にする理由

アドレスではなくバイトコードを基準にコントラクトをインデックス化する理由は、バイトコードが決定論的であり、ブロックチェーン上で検証可能なためです。 対して、アドレスは非決定論的であり、デプロイのたびに変化するため一意性の保証が難しいです。

再登録時のエラー発生

理論的には、バイトコードのハッシュが衝突する(異なるコードが同じハッシュを持つ)可能性はゼロではありません。 しかし、その確率は極めて低いため、インデックスの更新を許可せず一度登録されたバイトコードハッシュが異なるアドレスを指すことがないようにすることで、潜在的な攻撃リスクを低減しています。

インターフェースのシンプルさ

本仕様では、最小限のインターフェース設計を採用し、必要な機能だけを提供しています。 この目的は、他のコントラクトとの統合を容易にし、実装の負担を軽減するためです。 シンプルな設計を維持することで、開発者が容易にこの機能を組み込めるようになります。

ライブラリではなくERC標準としての実装

この仕組みを単なるライブラリとして提供する場合、コードの再利用が難しくなり、統一的なインデックスとして機能しづらくなります。 代わりにERC標準として定義することで、エコシステム全体で統一されたコード参照の仕組みを提供できます。 これにより、Ethereum上のさまざまなプロジェクトが標準に従うことで、一貫性のあるコード管理が可能になります。

参考実装

以下に実装のサンプルが置いてあります。 https://eips.ethereum.org/EIPS/eip-7744#reference-implementation

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.28;
import {IERC7744} from "./IERC7744.sol";

/**
 * @title Byte Code Indexer Contract
 * @notice You can use this contract to index contracts by their bytecode.
 * @dev This allows to query contracts by their bytecode instead of addresses.
 * @author Tim Pechersky (@Peersky)
 */
contract ERC7744 is IERC7744 {
    mapping(bytes32 => address) private index;

    function isValidContainer(address container) private view returns (bool) {
        bytes memory code = container.code;
        bytes32 codeHash = container.codehash;
        bytes32 eip7702Hash = bytes32(0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329);
        // Contract should have non-empty code and valid codehash
        return (code.length > 0 && codeHash != bytes32(0) && codeHash != eip7702Hash);
    }

    /**
     * @notice Registers a contract in the index by its bytecode hash
     * @param container The contract to register
     * @dev `msg.codeHash` will be used
     * @dev It will revert if the contract is already indexed or if returns EIP7702 hash
     */
    function register(address container) external {
        address etalon = index[container.codehash];
        require(isValidContainer(container), "Invalid container");
        if (etalon != address(0)) {
            if (isValidContainer(etalon)) revert alreadyExists(container.codehash, container);
        }
        index[container.codehash] = container;
        emit Indexed(container, container.codehash);
    }

    /**
     * @notice Returns the contract address by its bytecode hash
     * @dev returns zero if the contract is not indexed
     * @param id The bytecode hash
     * @return The contract address
     */
    function get(bytes32 id) external view returns (address) {
        return index[id];
    }
}

セキュリティ

悪意のあるコード

このCode Indexは、登録されたコントラクトの安全性や正当性を保証するものではありません。 したがって、ユーザーはインデックスに登録されているコードが悪意のあるものでないか、確認する必要があります。 単にget関数を使って取得したコントラクトのアドレスだからといって、そのコードが信頼できるとは限りません。

登録されたコントラクトのストレージ状態

インデックスにはバイトコードのみが登録され、コントラクトのストレージデータは記録されません。 したがって、たとえ同じバイトコードのコントラクトが登録されていたとしても、そのコントラクトがどのようなデータを保持しているかはわかりません。 そのため、ストレージの変更によって意図しない動作をする可能性があるため、注意が必要です。

EIP7702

EIP7702(Delegated Accounts)は、特殊な仕組みを持つアカウントの一つであり、通常のコントラクトとは異なる性質を持ちます。
このCode Indexは、EIP7702のデリゲートアカウントは登録できないようになっています。 その理由は、EIP7702のバイトコードは決定論的ではなく、一定の条件下で変化する可能性があるためです。
登録時にEXTCODEHASH(コントラクトのバイトコードハッシュを取得する命令)をチェックし、その値が0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329(keccak256(0xef01))に該当する場合、register関数はリバートする仕様になっています。

Self-Destructされたコントラクトの取り扱い

Ethereumでは、SELFDESTRUCT命令を使用することでコントラクトを削除することができます。 削除されたコントラクトのアドレスは空のストレージ状態(ゼロのバイトコード)となるため、Code Indexに登録されたコントラクトがSELFDESTRUCTで削除されると、インデックス内のデータが無効化されます。

この問題に対処するため、以下の対応をしています。

  • register関数を実行する時に、すでに登録されたアドレスが無効化されていないか検証する。
  • 無効化されていた場合のみ、新しいコントラクトを登録できるようにする。

この仕組みによって、削除されたコントラクトのバイトコードが異なるアドレスで再登録されることが可能になり、インデックスとしての整合性を維持できます。

引用

Tim Pechersky (@peersky) t@peersky.xyz, "ERC-7744: Code Index [DRAFT]," Ethereum Improvement Proposals, no. 7744, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7744.

最後に

今回は「コントラクトのバイトコードを記録して、コントラクトの発見や検証をサポートする仕組みを提案しているERC7744」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!