はじめに

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

今回は、Smart Account が信頼できるモジュールだけを選び出すためのレジストリの仕組みを提案しているERC7484についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC4337は、コントラクトアカウントの実行フローを標準化し、ERC7579はそれらを構成するモジュールを組み合わせて使える仕組みを標準化しています。 これにより、多くの開発者が「モジュール式スマートアカウント(Smart Account)」向けのモジュールを自由に開発できるようになっています。

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

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

しかし、第三者が作ったモジュールを安全性を確認せずにSmart Accountへ追加すると、攻撃の入り口を広げてしまいます。 そこでERC7484では、モジュールの安全性に関する情報(=セキュリティに関する証明や評価)を保存し、Smart Accountが利用前に確認できる仕組みとして、Module Registryを標準化します。

Smart Accountは、このModule Registryに直接アクセスするのではなく、「Registry Adapter」を通して安全に照会します。 Registry Adapterは、Smart AccountとModule Registryの橋渡しをし、Smart Accountがどの種類のRegistryを使う場合でも正しく、安全に照会できるようにします。

また、ERC7484では参考実装として、誰の所有にもならずどのSmart Accountも利用できるSingleton Module Registryも提供します。 多くのSmart AccountがこのSingletonを使うことで、統一された安全性チェックが期待できますが、強制ではありません。 Smart Accountは、この標準インターフェースに対応する任意のRegistryを利用できます。

動機

ERC4337とERC7579によって、Smart Accountの仕組みは拡張性が高く、自由度の高いものになりました。 特にERC7579によって、アカウントの機能をモジュールという形で追加できるため、外部開発者も独自のモジュールを作成できます。

しかし、モジュールが誰でも作れてしまうという点は、セキュリティの観点では大きな問題を引き起こします。 信頼できないモジュールをSmart Accountに導入すると、不正操作や資産流出などの危険性が高まります。 この状況に対応するために、Smart Accountがモジュールの「安全性の証明」を確認できる仕組みが必要になりました。

その解決策として、Module Registryが登場します。Module Registryは、モジュールごとの安全性情報を保持し、Smart Accountが使用前にその情報を取得できます。 ただし、Registryごとに振る舞いや設計が異なるとSmart Account側で個別対応が必要になり、開発者の負担が増えます。 そこで、以下の標準化が重要になります。

標準化される内容

標準化対象 説明
Module Registry のインターフェースと必要機能 Smart AccountがどのRegistryを使う場合でも、同じ方法で安全性情報を照会できるようにします。
Registry Adapterの挙動 Smart AccountがRegistryへアクセスするとき、アーキテクチャや実行フローが違っても同じ手順で扱えるようにします。

これにより、Smart AccountがどのRegistryを相手にしても、モジュールの安全性を一貫した方法でチェックでき、モジュール利用時のリスクを減らせます。

さらに参考実装として、誰の所有にもならないSingleton Module Registryが提供されます。 多くのSmart Accountがこの1つのRegistryを利用するなら、エコシステム全体で安全性評価を共有でき、統一性のメリットが生まれます。

ただし、Singletonを必ず使う必要はありません。 Smart Accountは標準化されたインターフェースに準拠するRegistryであればどれでも利用できるため、柔軟性と互換性を両立できます。

Smart AccountがModule Registryを利用することで、モジュール導入時のリスクを適切に管理でき、エコシステム全体の安全性と利便性が向上します。

仕様

用語の意味

仕様の理解のために、文中で使われる主要な用語を整理します。

用語 説明
Smart Account ERC7579に準拠したモジュール式スマートアカウント。
Module Smart Account に追加できる独立した機能。
Attestation モジュールに関する安全性などをチェーン上で示す証明情報。
Attester Attestation を発行する主体。
Registry モジュールに対する Attestation を保持するコントラクト。
Adapter Smart Account が Registry に問い合わせる際の処理を担当する機能。

Smart Accountがモジュールを利用する前に、そのモジュールの安全性が第三者(Attester)によって確認されているかどうかをRegistryを通して照会する仕組みです。

シーケンス図

以下は、モジュールの安全性を確認しながらインストール・実行するまでの流れを表した図です。

図には6つの主要ステップがあります。

  1. AttesterがRegistryに対してモジュールの安全性を証明するAttestationを発行 Registryはこの情報を保存します。
  2. Smart Account(Adapter を内蔵) がRegistryにモジュールの安全性を確認 この段階では、モジュールをインストールする前のチェックです。
  3. Smart Accountがモジュールをインストール ここは任意ですが、一般的にはインストール前の安全性確認を行うことが推奨されます。
  4. モジュール実行前に再度Registryに安全性を確認 モジュールを使う直前に、最新の安全性が確保されているかを確認します。
  5. Smart Accountがモジュールを実行 実行後にさらにチェックを入れることも可能です。
  6. モジュール実行後の状態に対して再確認 **Attesterが後から Attestation を取り消す可能性があるため、実行後の安全性確認もオプションとして設けられています。

Smart Accountは Registry がエラー(revert)を返した場合、危険と判断し、取引を中断するべきとしています。

Registry が備えるべき機能

Registryは、Smart Accountがモジュールの安全性を判定するために利用します。 仕様では、Registryが必ず備えなければならない機能と、備えることが望ましい機能が明確に定義されています。

必須機能

  • Attesterが本当に attestation を作成した本人かどうか確認できること 例:msg.sender を確認する、署名を検証するなど。
  • Attesterが過去に作成した attestation を取り消せること
  • Registryは attestation データを保存するか保存先の参照を保存すること

追加が推奨される機能

  • Attestationに有効期限を設定可能にする 有効期限切れの場合はチェック時にエラーを返す。
  • Adapterや外部クライアントが、特定の attestation データを読み取れる view 関数。

check 関数の仕様

Registryの中心となる機能が check 関数群です。 Registryは以下の場合に必ずエラー(revert)を返す必要があります。

条件 説明
Attesterの数が閾値より少ない場合 安全性を満たさないため使用不可。
Attesterが attestation を取り消した場合 モジュールの安全性が無効になった状態。
Attesterが重複している、またはソートされていない場合 不正データを防ぐため。

moduleType を利用する check 関数の仕様

Registry内に保存されている moduleType が、呼び出し時に与えられた moduleType と異なる場合も、エラーを返します。

内部 attester を使う check 関数

Smart Accountあるいは msg.sender 用に Registry 内に attester 情報が保存されている必要があります。

  • 保存されていなければ revert。
  • Smart Account が指定されていない場合は msg.sender の attester を参照。

trustAttesters 関数の仕様

Smart Account(またはユーザー)は、信頼する attester と閾値をRegistryに登録できます。

  • 指定された attester リストは重複がなくソートされていなければならない
  • threshold と attester リストをRegistryに保存する

Adapter が持つべき挙動

Smart Accountは、Registryと正しく連携するために Adapter を備える必要があります。

必須機能
  • モジュール A を初めて呼び出す前(または呼び出し時)に必ずRegistryに問い合わせる。
  • Registryが revert した場合は危険と判断する。
推奨
  • Registryが revert した場合は取引全体を revert する
  • モジュールをインストールする時に Registry へのチェックを行う
  • モジュールを実行する際にもRegistryへのチェックを行う

インターフェース

IERC7484Registry.sol
interface IERC7484Registry {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               Check with internal attester(s)              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    function check(address module) external view;

    function checkForAccount(address smartAccount, address module) external view;

    function check(address module, uint256 moduleType) external view;

    function checkForAccount(
        address smartAccount,
        address module,
        uint256 moduleType
    )
        external
        view;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   Set internal attester(s)                 */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function trustAttesters(uint8 threshold, address[] calldata attesters) external;


    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*              Check with external attester(s)               */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function check(
        address module,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view;

    function check(
        address module,
        uint256 moduleType,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view;
}

関数

check
javascript
function check(address module) external view;

モジュールの安全性を内部 attester の設定に基づいて確認する関数。 Registryに登録済みの内部 attester 情報を使用して、指定されたモジュールが必要な安全性の条件を満たしているか確認します。

引数

  • module
    • チェック対象となるモジュールアドレス。
checkForAccount
javascript
function checkForAccount(address smartAccount, address module) external view;

特定のSmart Account用に保存されている内部 attester を使ってモジュールの安全性を確認する関数。 Registryは、smartAccount に紐付けて保存された attester 情報を使用してモジュールの安全性を検証します。

引数

  • smartAccount
    • attester 設定を保持しているSmart Accountのアドレス。
  • module
    • チェック対象モジュール。
check (module + moduleType)
javascript
function check(address module, uint256 moduleType) external view;

モジュールの安全性と、登録されている moduleType が一致するかを確認する関数。 Registryに保存されている moduleType が、呼び出し時の値と異なる場合はエラーを返します。

引数

  • module
    • チェック対象モジュール。
  • moduleType
    • モジュールの種類を表す数値。
checkForAccount (module + moduleType)
javascript
function checkForAccount(
    address smartAccount,
    address module,
    uint256 moduleType
) external view;

特定Smart Account用の内部 attester を使い、moduleType も含めてモジュールの安全性を確認する関数。 Smart Accountに保存されている attester と moduleType 情報に基づいて検証を行います。

引数

  • smartAccount
    • attester 設定の保持者。
  • module
    • チェック対象モジュール。
  • moduleType
    • モジュールの種類。
trustAttesters
javascript
function trustAttesters(uint8 threshold, address[] calldata attesters) external;

msg.sender が信頼する attester と閾値をRegistryに登録する関数。 attester リストがソートされ重複がないか検証し、Registryに保存します。

引数

  • threshold
    • 必要とする attester 数。
  • attesters
    • 信頼する attester の配列。
check (external attester 指定)
javascript
function check(
    address module,
    address[] calldata attesters,
    uint256 threshold
) external view;

外部から attester リストと閾値を直接指定し、モジュールの安全性を確認する関数。 呼び出し側が attester と閾値を指定します。 Registryは、指定された attester が実際に attestation を発行しているか確認します。

引数

  • module
    • チェック対象モジュール。
  • attesters
    • 検証に使用する attester のリスト。
  • threshold
    • 必要な attestation の数。
check (moduleType + external attester)
javascript
function check(
    address module,
    uint256 moduleType,
    address[] calldata attesters,
    uint256 threshold
) external view;

外部指定 attester を使い、かつ moduleType 一致も検証する関数。 Registryは、指定された moduleType がRegistry に保存されている moduleType と一致しているか確認し、不一致の場合は revert します。

引数

  • module
    • チェック対象モジュール。
  • moduleType
    • モジュールの種類。
  • attesters
    • 使用する attester のリスト。
  • threshold
    • 必要 attestation 数。

補足

Attestations(アテステーション)

Attestationは、あるモジュールに関する「チェーン上の証明」です。 内容は大きく分けて、セキュリティ、標準準拠、その他の性質などについての主張であり、Smart Accountがモジュールを利用する際の判断材料になります。

チェーン上で検証できない情報も扱える意味

多くの主張は、チェーン上だけで検証するのが難しいものです。 例えば以下のようなケースがあります。

  • モジュールが特定のストレージスロットにしかアクセスしないかどうかを確認したい 特に DELEGATECALL のようにSmart Accountのストレージに影響を与える操作がある場合、どこに書き込む可能性があるか把握しておくことは非常に重要です。

しかしこれはチェーン上で完全に検証することはほぼ不可能です。 そこで、Attesterがオフチェーンで調査を行い、結果をチェーン上に「証明」として残します。

こうすることで、Smart Accountはオフチェーン検証の結果をチェーン上で参照できるようになります。

Attestation データの自由度

ERC7484は、Attestationのデータ形式を特定の形に制限していません。 データそのものでも、データの参照でも構わない設計になっています。

これにより、以下のように幅広いケースに対応できます。

種類 内容
単純なフラグ 「このモジュールは安全です」という boolean のような簡易データ。
詳細な証明 解析結果、外部の検証レポートの参照など。
実行時の動作を示す複雑なデータ runtime を記録した高度なデータ形式。

こうした柔軟性により、どのような監査方法にも対応できます。

Singleton Registry(単一レジストリ)

Attestationをチェーン上で確認できるようにするには、どこかのコントラクトに記録する必要があります。 ERC7484では、参考実装として「オーナーのいないSingleton Registry」が提供されています。

これは、すべてのモジュールに対するAttestationを集約する「単一の情報源」であり、Smart Accountはここを確認することで安全性を判断できます。

Singleton Registryを採用する理由

セキュリティ向上

1つのレジストリにAttestationが集められることで、次の利点が生まれます。

  • 多くのAttesterが同じ場所で主張を行うため、モジュールごとのAttestationが豊富になる
  • Smart Accountは複数のRegistryの信頼性を比較・検証する必要がなくなる
  • 多くの attester からのチェーン上の主張をまとめて取得でき、gas コストを抑えられる

Securityの観点では「信頼の集中」による利便性が高くなります。

相互運用性の向上

Singleton Registryを利用することで、モジュールの流動性(liquidity)と互換性が向上します。

  • 開発者はモジュールを1つのRegistryに登録すれば済むため、Attestationの分散が防げる。
  • Attesterは過去の監査結果を再利用したり、依存関係をたどった attestation を作成しやすくなる。
  • バージョン管理や、歴史的な attestation の追跡も容易になる。

これにより、エコシステム全体のモジュール利用が滑らかになります。

Singleton のトレードオフ

便利で強力ですが、いくつかの弱点もあります。

単一障害点

唯一のRegistryが攻撃されると、Smart Account全体に深刻な影響があります。

最悪ケースとしては、攻撃者が「信頼されている attester のふりをして悪質なモジュールにAttestationを発行する」ことです。

複数レジストリ方式のメリットもある

複数のRegistryを併用すれば、次のようなメリットもあります。

  • 各Registryの変更がすぐに他へ波及しないため、情報の伝播が遅延するが、逆に攻撃者が注目するまでの時間稼ぎになる場合がある。

ただし、この遅延は脆弱性悪用の機会を増やしてしまう場合もあるため、メリットとは一概に言い切れません。

Singletonは柔軟設計が必要になる

幅広いケースに対応する必要があるため、Singleton Registryは構造が複雑になりがちです。

  • その結果、計算量が増え、gas コストが高くなる傾向がある。
  • ただし、多数の attestation を一度に扱う場合は、複数Registryを横断するより安定して効率的。

ERC7484の参考実装では gas 最適化も考慮されており、大幅に改善可能な余地は少ないとされています。

Module Types(モジュールの種類)

モジュールには「タイプ」を設定することができます。 これは Smart Accountが期待する機能をきちんと持っているかを確認するために重要です。

例として、Smart Accountの「署名検証ロジック」を担うモジュールを追加したい場合を考えます。

  • そのモジュールが本当に署名検証をできるかどうかは、アカウントの根本的安全性に関わります。
  • Attesterが適切な type を持つことを確認し、Registryに保存しておけば、Smart Accountは間違ったモジュールを採用してしまうリスクを減らせます。

Registryは moduleType の意味を知らなくてよい

Registryそのものは「type が何を意味するのか」を理解している必要はありません。

  • type の意味はAttesterが判断する。
  • Registryはその値を保存し、 Smart Accountのチェックに利用できる形で保持するだけでよい。

この分離により、Registryを汎用的なデザインに保つことができます。

参考実装の設計は Ethereum Attestation Service(EAS)から大きな影響を受けています。 ただし、ERC7484 扱うユースケースは特殊であり、EASをそのまま利用するには不十分でした。

追加で必要だった機能は次の通りです。

必要だったもの 理由
Smart AccountとModuleの関係に特化したチェック 一般的な attestation の仕組みだけでは不十分。
moduleType の扱い モジュール特有の要件を表現する必要がある。
Adapterとの連携前提の処理 Smart Account 側が Registry をどのように使うか明確にする必要がある。

そのため、EAS をベースにしつつも独自実装が必要でした。

参考実装

Adapter.sol

Adapter.sol
contract Adapter {
    IRegistry registry;

    function checkModule(address module) internal {
        // Check module attestation on Registry
        registry.check(module);
    }

    function checkModuleWithModuleTypeAndAttesters(address module, address[] memory attesters, uint256 threshold,  uint16 moduleType) internal {
        // Check list of module attestations on Registry
        registry.check(module, attesters, threshold, moduleType);
    }

}

Account.sol

Account.sol
contract Account is Adapter {
    ...

    // installs a module
    function installModule(
        uint256 moduleTypeId,
        address module,
        bytes calldata initData
    )
        external
        payable
    {
        checkModule(module);
        ...
    }

    // executes a module
    function executeFromExecutor(
        ModeCode mode,
        bytes calldata executionCalldata
    )
        external
        payable
        returns (bytes[] memory returnData)
    {
        checkModule(module);
        ...
    }

    ...
}

Registry

Registry.sol
/**
* @dev this implementation is unoptimized in order to make the reference implementation shorter to read
* @dev some function implementations are missing for brevity
*/
contract Registry is IERC7484Registry {
    ...

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               Check with internal attester(s)              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    function check(address module) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(msg.sender);

        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function checkForAccount(address smartAccount, address module) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(smartAccount);

        ...
    }

    function check(address module, uint256 moduleType) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(msg.sender);

        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;

            AttestationRecord storage attestation = _getAttestation(module, attester);
            if (attestation.moduleType != moduleType) revert ModuleTypeMismatch();
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function checkForAccount(
        address smartAccount,
        address module,
        uint256 moduleType
    )
        external
        view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(smartAccount);

        ...
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   Set internal attester(s)                 */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function trustAttesters(uint8 threshold, address[] calldata attesters) external {
        ...
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*              Check with external attester(s)               */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function check(
        address module,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view
    {
        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function check(
        address module,
        uint256 moduleType,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view
    {
        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;

            AttestationRecord storage attestation = _getAttestation(module, attester);
            if (attestation.moduleType != moduleType) revert ModuleTypeMismatch();
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         Internal                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function _check(address module, address attester) external view returns (bool isValid){
        AttestationRecord storage attestation = _getAttestation(module, attester);

        uint48 expirationTime = attestation.expirationTime;
        uint48 attestedAt =
            expirationTime != 0 && expirationTime < block.timestamp ? 0 : attestation.time;
        if (attestedAt == 0) return;

        uint48 revokedAt = attestation.revocationTime;
        if (revokedAt != 0) return;

        isValid = true;
    }

    function _getAttestation(
        address module,
        address attester
    )
        internal
        view
        virtual
        returns (AttestationRecord storage)
    {
        return _moduleToAttesterToAttestations[module][attester];
    }

    function _getAttesters(
        address account
    )
        internal
        view
        virtual
        returns (address[] calldata attesters, uint256 threshold)
    {
        ...
    }

    ...
}

引用

Konrad Kopp (@kopy-kat), zeroknots (@zeroknots), "ERC-7484: Registry Extension for ERC-7579 [DRAFT]," Ethereum Improvement Proposals, no. 7484, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7484.

最後に

今回は「Smart Account が信頼できるモジュールだけを選び出すためのレジストリの仕組みを提案しているERC7484」についてまとめてきました! いかがだったでしょうか?

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

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