はじめに

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

今回は、最小限のモジュラースマートアカウントの仕組みを提案しているERC7579についてまとめていきます!

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

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

ERC7579は、EIP4337の仕組みを拡張しています。 以下の記事を参考にしてください。

ERC7579について以下の記事でより詳しく説明しています。

概要

ERC7579は、モジュール型のコントラクトアカウントおよびそのモジュール間の相互運用性を確保するために必要な最小限のインターフェースと動作仕様を定義しています。 コントラクトアカウントについては、実行インターフェース、設定インターフェース、フォールバックインターフェース、ERC165、ERC1271に準拠する必要があります。 モジュールについては、コアインターフェース、モジュールの種類、種類ごとのインターフェースが定義されています。

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

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

動機

コントラクトアカウントは現在広く採用されつつあり、多くがモジュール化されたアーキテクチャで構築されています。 これは、機能を外部コントラクト(モジュール)に分離することで、開発スピードの向上、将来の拡張性、開発者やユーザーによるカスタマイズ性を高めることを目的としています。

しかし、現状では各コントラクトアカウントが独自の設計で構築されており、モジュールの断片化や特定ベンダーへの依存(ベンダーロックイン)が発生しています。 この問題に対処するため、コントラクトアカウントを標準化することには以下のような利点があります。

  • モジュールを異なるコントラクトアカウント間で再利用可能にする相互運用性の実現
  • コントラクトアカウントが異なるウォレットアプリケーションやSDKで利用可能になる相互運用性の実現
  • ユーザーが特定のベンダーにロックインされるリスクの軽減

ただし、標準化にあたってはコントラクトアカウントの実装ロジックへの影響を最小限に抑えることが重要です。 これにより、アカウントの開発者が引き続き革新を進められると同時に、複数のアカウントに対応したモジュールエコシステムの発展が促されます。 ERC7579の目標は、必要最小限のインターフェースと動作仕様を定義することで、アカウントとモジュール間の相互運用性を確保することです。

仕様

用語の定義

スマートアカウント

モジュール型のアーキテクチャを持つコントラクトアカウントのことです。 機能の一部または全体を外部モジュールに委任しており、高い柔軟性と拡張性を持ちます。

モジュール

コントラクトアカウントで使用される各機能を実装しているコントラクトです。 特定の処理(検証、実行、フォールバックなど)を担います。

バリデータ(Validator)

コントラクトアカウントに対してトランザクションを実行できるか検証するモジュールです。 ERC4337を使用する場合は、validateUserOp 関数がこれに該当します。

エグゼキュータ(Executor)

コントラクトアカウントに代わってトランザクションを実行するモジュールです。コールバックを通じてアクションを実行します。

フォールバックハンドラ(Fallback Handler)

コントラクトアカウントのフォールバック機能(未定義の関数呼び出しなど)を拡張するためのモジュールです。

エントリーポイント(EntryPoint)

ERC4337の仕様に基づく信頼されたシングルトンコントラクトです。 ユーザー操作の検証および実行を一元的に管理します。

バリデーション(Validation)

コントラクトアカウントがトランザクションを実行すべきかどうかを判断するための機能全般を指します。 ERC4337では validateUserOp が該当します。

エグゼキューション(Execution)

コントラクトアカウントから、またはコントラクトアカウント上でオペレーションを実行する機能全般です。 ERC4337では、EntryPoint が userOp.callData を通じて呼び出します。

アカウント

Validation

ERC7579では、どのようにバリデータ(Validator)を選択するかについての実装方法は規定していません。 つまり、コントラクトアカウントがどのバリデータを使用するかの決定方法は自由です。

ただし、例えばERC4337を用いている場合に、userOp.signature などのデータフィールド内にバリデータ選択に関する情報を含める設計を行う場合には、コントラクトアカウント側でその値をsanitizeしてからバリデータを呼び出す必要があります。 sanitizeとは、悪意ある入力や不正な値を排除し、安全な形式に変換する処理を意味します。

また、コントラクトアカウントの validation 関数は、原則としてバリデータの戻り値をそのまま返す必要があります。 これはバリデータの結果を正確に反映するためです。

Executionの振る舞い

ERC7579に準拠するためには、コントラクトアカウントが以下の実行インターフェースを実装する必要があります。

IERC7579Execution
IERC7579Execution.sol
interface IERC7579Execution {
    /**
     * @dev Executes a transaction on behalf of the account. MAY be payable.
     * @param mode The encoded execution mode of the transaction.
     * @param executionCalldata The encoded execution call data.
     *
     * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337
     * If a mode is requested that is not supported by the Account, it MUST revert
     */
    function execute(bytes32 mode, bytes calldata executionCalldata) external;

    /**
     * @dev Executes a transaction on behalf of the account. MAY be payable.
     *         This function is intended to be called by Executor Modules
     * @param mode The encoded execution mode of the transaction.
     * @param executionCalldata The encoded execution call data.
     *
     * @return returnData An array with the returned data of each executed subcall
     *
     * MUST ensure adequate authorization control: i.e. onlyExecutorModule
     * If a mode is requested that is not supported by the Account, it MUST revert
     */
    function executeFromExecutor(bytes32 mode, bytes calldata executionCalldata)
        external
        returns (bytes[] memory returnData);
}
execute

コントラクトアカウント自身、またはEntryPointコントラクトから処理を実行する関数。 mode(実行モード)と executionCalldata(実行データ)を引数で受け取ります。 payable にすることも可能です。 権限制御が必須で、ERC4337を用いる場合は onlyEntryPointOrSelf (EntryPointコントラクトかコントラクトアカウント自身からのみ実行可能)のような制御を設ける必要があります。 サポートされていない実行モードが指定された場合、処理を失敗させる必要があります。

executeFromExecutor

Executorモジュールがコントラクトアカウントに変わって処理を実行する関数。 mode(実行モード)と executionCalldata(実行データ)を引数で受け取り、実行結果を returnData として返します。 権限制御として onlyExecutorModule (Executorモジュールからのみ実行可能)を実装する必要があります。 サポートされていない実行モードが指定された場合、処理を失敗させる必要があります。

追加関数
js
/**
 * @dev ERC-4337 executeUserOp according to ERC-4337 v0.7
 *         This function is intended to be called by ERC-4337 EntryPoint.sol
 * @param userOp PackedUserOperation struct (see ERC-4337 v0.7+)
 * @param userOpHash The hash of the PackedUserOperation struct
 *
 * MUST ensure adequate authorization control: i.e. onlyEntryPoint
 */
function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
executeUserOp

ERC4337のEntryPointコントラクトからUserOperationを実行する関数。 userOp(PackedUserOperation構造体)とそのハッシュを引数で受け取ります。 onlyEntryPoint による権限制御が必要です。 実装する場合、userOp.callData の先頭4バイト(executeUserOp.selector)を除いた部分 userOp.callData[4:] を実行する必要があります。

userOp.callData の先頭4バイトには関数セレクタが入ります。 関数セレクタとは、関数を識別するための4バイトの識別子です。 関数セレクタば、関数名と引数の型一覧である関数のシグネチャ(transfer(address,uint256))を取得し、UTF-8でエンコードしてKeccak-256ハッシュ化することで導出することができます。 これにより、実行する関数の情報を少ないデータ量で検証できるようになります。

userOp.callData[:3]にはこの関数セレクタの情報が格納されており、userOp.callData[4:]には実行するべきデータが格納されています。

通常は delegatecall を使用することが推奨されます。 これは、msg.sender を維持した状態で処理を行うことができるため、権限の制御などが適切に行えるためです。

js
(bool success, bytes memory innerCallRet) = address(this).delegatecall(userOp.callData[4:]);

実行モード(Execution Mode)の構造

mode は32バイトの値で、以下のように構造化されています。

項目名 サイズ 説明
callType 1バイト 呼び出しの種類(0x00: 単体呼び出し, 0x01: バッチ, 0xfe: staticcall, 0xff: delegatecall)
execType 1バイト 失敗時の動作(0x00: revert でエラーを返す, 0x01: revert せず何らかのエラーハンドリングを組み込んでいる)
unused 4バイト 将来の拡張用に予約
modeSelector 4バイト 拡張モードの識別子
modePayload 22バイト 追加のデータを渡すための領域

実行モードのサポート宣言と制限

アカウントはすべての実行モードに対応する必要はありません。 サポートするモードは supportsExecutionMode 関数で明示する必要があります。 未サポートのモードが指定された場合はエラーを返す必要があります。

実行データのエンコード形式

実行に使用する executionCalldata は、モードによって以下のようにエンコードします。

  • 単体呼び出し(single call) *以下の形式でエンコード。

    js
    abi.encodePacked(target, value, callData)
  • delegatecall

    • 以下の形式でエンコード。
    js
    abi.encodePacked(target, callData)
  • バッチ呼び出し(batch call)

    • 複数の Execution 構造体(address target, uint256 value, bytes callData)を配列にしてabi.encode でエンコード。
    js
    struct Execution {
        address target;
        uint256 value;
        bytes callData;
    }
    
    abi.encode(Execution[])

このように、実行の柔軟性と安全性を両立するために、実行モードごとの仕様とエンコード方法が明確に定められています。

アカウントの設定

ERC7579に準拠するコントラクトアカウントは、以下のアカウント設定用インターフェースを実装する必要があります。 これは、アカウントの識別や対応モード、モジュールの互換性を外部から確認できるようにするインターフェースです。

IERC7579AccountConfig.sol
interface IERC7579AccountConfig {
    /**
     * @dev Returns the account id of the smart account
     * @return accountImplementationId the account id of the smart account
     *
     * MUST return a non-empty string
     * The accountId SHOULD be structured like so:
     *        "vendorname.accountname.semver"
     * The id SHOULD be unique across all smart accounts
     */
    function accountId() external view returns (string memory accountImplementationId);

    /**
     * @dev Function to check if the account supports a certain execution mode (see above)
     * @param encodedMode the encoded mode
     *
     * MUST return true if the account supports the mode and false otherwise
     */
    function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);

    /**
     * @dev Function to check if the account supports a certain module typeId
     * @param moduleTypeId the module type ID according to the ERC-7579 spec
     *
     * MUST return true if the account supports the module type and false otherwise
     */
    function supportsModule(uint256 moduleTypeId) external view returns (bool);
}
accountId
js
function accountId() external view returns (string memory accountImplementationId);

コントラクトアカウントの一意な識別子(account ID)を返す関数。 返される文字列は空であってはならず、フォーマットは "ベンダー名.アカウント名.バージョン" のように構成されることが推奨されます(例:mywallet.safemodule.v1.0.0)。 この識別子は、他のコントラクトアカウントと一意であることが望まれます。

このIDは、外部からそのコントラクトアカウントがどのような実装で構成されているかを識別する際に使われます。

supportsExecutionMode
js
function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);

渡された実行モード(encodedMode)がこのコントラクトアカウントでサポートされているかを確認する関数。

実行モードの詳細は呼び出しの種類やエラーハンドリング方式をエンコードしたものです。 コントラクトアカウントがそのモードをサポートしていれば true、そうでなければ false を返します。 これにより、モジュールや外部アプリケーションが事前に互換性を確認できます。

supportsModule
js
function supportsModule(uint256 moduleTypeId) external view returns (bool);

指定されたモジュールタイプ(moduleTypeId)がコントラクトアカウントで使用可能かを返す関数。

moduleTypeId はERC7579で定義されたモジュール種別(Validator, Executor, Fallback など)を表すIDです。 コントラクトアカウントがそのモジュールタイプを受け入れる設計になっていれば true を返します。

この関数により、モジュールの導入可否を事前に判別することができ、モジュールとアカウント間の安全な組み合わせを確保できます。

モジュールの設定

ERC7579に準拠するコントラクトアカウントは、モジュールの追加・削除・確認を行うためのインターフェースを実装する必要があります。 これにより、各モジュールの種類を識別し、必要なアクセス制御や初期化・後処理を安全に行えるようになります。

モジュールの種類識別について

コントラクトアカウントは、インストールされたモジュールがどのタイプ(バリデータ、エグゼキュータ、フォールバックなど)に属しているかを識別できる必要があります。 例えば、executeFromExecutor 関数はExecutorモジュールだけが呼び出せるべきなので、他のモジュールとは明確に区別されている必要があります。

IERC7579ModuleConfig
IERC7579ModuleConfig.sol
interface IERC7579ModuleConfig {
    event ModuleInstalled(uint256 moduleTypeId, address module);
    event ModuleUninstalled(uint256 moduleTypeId, address module);

    /**
     * @dev Installs a Module of a certain type on the smart account
     * @param moduleTypeId the module type ID according to the ERC-7579 spec
     * @param module the module address
     * @param initData arbitrary data that may be required on the module during `onInstall`
     * initialization.
     *
     * MUST implement authorization control
     * MUST call `onInstall` on the module with the `initData` parameter if provided
     * MUST emit ModuleInstalled event
     * MUST revert if the module is already installed or the initialization on the module failed
     */
    function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;

    /**
     * @dev Uninstalls a Module of a certain type on the smart account
     * @param moduleTypeId the module type ID according the ERC-7579 spec
     * @param module the module address
     * @param deInitData arbitrary data that may be required on the module during `onInstall`
     * initialization.
     *
     * MUST implement authorization control
     * MUST call `onUninstall` on the module with the `deInitData` parameter if provided
     * MUST emit ModuleUninstalled event
     * MUST revert if the module is not installed or the deInitialization on the module failed
     */
    function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;

    /**
     * @dev Returns whether a module is installed on the smart account
     * @param moduleTypeId the module type ID according the ERC-7579 spec
     * @param module the module address
     * @param additionalContext arbitrary data that may be required to determine if the module is installed
     *
     * MUST return true if the module is installed and false otherwise
     */
    function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool);
}
installModule
js
function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;

指定した種類のモジュールをアカウントに追加する関数。

  • moduleTypeId
    • モジュールの種類を識別するID(ERC7579で定義)。
  • module
    • モジュールのコントラクトアドレス。
  • initData
    • モジュール初期化時に渡す任意のデータ。

適切な権限制御が必要です。 module の onInstall(initData) 関数を必ず呼び出す必要があります。 すでにインストールされている場合、あるいは初期化に失敗した場合は revert する必要があります。 成功時には ModuleInstalled イベントを発行する必要がります。

uninstallModule
js
function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;

指定したモジュールをアカウントから取り除く関数。

  • deInitData
    • アンインストール時にモジュールへ渡す任意のデータ(クリーンアップ処理などに使用)

適切な権限制御が必要です。 module の onUninstall(deInitData) を呼び出す必要があります。 モジュールが存在しない場合や、後処理に失敗した場合は revert する必要があります。 成功時には ModuleUninstalled イベントを発行する必要がります。

isModuleInstalled
js
function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool);

指定されたモジュールが現在アカウントにインストールされているかを返す関数。 additionalContext は、必要に応じてインストール確認のために使用できる追加データです。 モジュールがインストールされていれば true、そうでなければ false を返します。

通常のモジュール判定はアドレス単位で行われますが、以下のような複雑な条件がある場合、additionalContext での追加情報が必要になります。

  • モジュールが「特定の機能だけに有効」
  • モジュールが「特定ユーザーに対してだけ有効」
  • モジュールが「特定ブロックタイムやチェーンIDのもとでだけ有効」

このようなケースでは、外部から additionalContext を使って補足情報を渡すことで柔軟な判定が可能になります。

このモジュール設定インターフェースにより、スマートアカウントは柔軟かつ安全に外部モジュールを組み込み、機能を拡張することができます。

Hooks

Hooksは、ERC7579におけるオプション機能として定義されています。 コントラクトアカウントが任意で導入できる拡張機構であり、トランザクションの実行前後に独自のロジックや検証処理を挿入することができます。

Hooksの目的

Hooksは、コントラクトアカウントが単体またはバッチで処理を実行する時に、以下のような追加制御を実現するために使われます。

  • セキュリティチェック(例:トランザクションの出力先制限)
  • ログ記録や監査用のイベント処理
  • 実行条件や事前・事後検証の導入
  • モジュールインストール時の検証処理
実装要件

この拡張機能に対応するスマートアカウントは、以下のように preCheck ・ postCheck 関数をHooks経由で呼び出す必要があります。

  • execute または executeFromExecutor を実行する前に、1つ以上のHooksの preCheck を呼び出す必要があります。
  • 実行が完了した後に、1つ以上のHooksの postCheck を呼び出す必要があります。
  • installModule や uninstallModule を呼び出す前後にも、preCheck と postCheck を呼び出すことが推奨されます。
  • アカウントが独自に定義するその他の「実行系関数(execution)」でも、可能であればフックの使用が推奨されます。

ERC-1271 Forwarding

ERC7579に準拠するコントラクトアカウントは、コントラクトによる署名検証を行う標準インターフェースであるERC1271を実装する必要があります。 この仕様では、その署名検証をバリデータ(Validator)に委任(フォワーディング)する方法が定義されています。

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

ERC-1271とは

ERC1271は、コントラクトが署名の有効性を検証するための標準インターフェースです。 以下の関数が提案されています。

js
function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue);

この関数により、EOAではないコントラクトアカウントでも署名検証が可能になります。

フォワーディングの仕組み

コントラクトアカウントが署名検証を内部で処理せず、外部のバリデータに転送(フォワーディング)する設計を採用する場合、以下のルールに従う必要があります。

  • コントラクトアカウントは、ERC1271の isValidSignature 関数を実装する必要があります。
  • フォワーディングする場合は、バリデータの以下の関数を呼び出します。
js
isValidSignatureWithSender(address sender, bytes32 hash, bytes memory signature)

sender にはコントラクトアカウントの isValidSignature の呼び出し元アドレス(msg.sender)をそのまま渡します。

バリデータ選択に署名データを使う場合の注意

もしコントラクトアカウントが、signature パラメータ内に「どのバリデータを使うか」という選択情報をエンコードしている場合、そのままバリデータに渡すのではなく、不正なデータを除去・sanitizeしてから渡す必要があります。 これは、署名情報を悪用して不正なバリデータに誘導されることを防ぐためです。

戻り値の扱い

コントラクトアカウントの isValidSignature 関数は、フォワーディング先のバリデータが返した戻り値をそのまま返すことが推奨されています。 これにより、署名の有効性に関する判定結果を正しく反映することができます。

Fallback

ERC7579に準拠するコントラクトアカウントでは、任意で fallback 関数を実装し、Fallbackハンドラに処理を転送する仕組みを導入できます。 これは、未定義の関数呼び出しや柔軟な機能拡張を可能にする設計です。

Fallbackの目的

通常のコントラクトでは、定義されていない関数が呼び出された際に fallback 関数が実行されます。 ERC7579ではこの挙動を活用し、Fallback処理を外部の「Fallbackハンドラ(コントラクト)」に転送することで、アカウント機能を柔軟に拡張できるようにしています。

Fallbackハンドラへのルール

コントラクトアカウントがFallbackハンドラを導入している場合、以下の条件を満たす必要があります。

  • Fallback処理は call あるいは staticcall を使ってハンドラを呼び出す必要があります。
  • ERC2771に従い、msg.sender を calldata の末尾に付加してから呼び出す必要があります。
  • 転送先を選択する際は、calldata の先頭に含まれる 関数セレクタ(4バイト)をもとに、適切なハンドラを選択する必要があります。
  • アクセス制御を加えたい場合は、Hooks機構を使うことが推奨されます。

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

Fallbackで追加された機能の扱い

Fallbackクハンドラによってアカウントに新たな機能を追加した場合、それはコントラクトアカウントに実装された機能と同等に扱われる必要があります。

つまり、コントラクトアカウントのインターフェースと含める必要があるという意味です。

例えばERC165のように、インターフェース判定などをFallback経由で提供することも可能です。

ERC165は、特定のコントラクトが特定のインターフェースを実装しているかを確認する仕組みを提案しています。

実装に関する注意点

Fallback経由で view 関数(読み取り専用)を実装することは、将来的な拡張性を高めるため推奨されます。 一方で、コントラクトアカウントのコアロジック(署名検証やトランザクション実行など)をFallbackに依存させることはセキュリティや予測可能性の低下という観点から非推奨です。

ERC165

ERC7579に準拠するコントラクトアカウントは、任意でERC165を実装できます。 ERC165は、コントラクトアカウントが特定のインターフェースに対応しているかどうかを、外部から判定できるようにする規格です。

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

ERC165とは?

ERC165は、以下のような関数を通じて、コントラクトが対応しているインターフェースIDを判定するための仕組みです。

js
function supportsInterface(bytes4 interfaceID) external view returns (bool);

この関数に対してインターフェースID(たとえば IERC721 なら 0x80ac58cd)を渡すと、対応していれば true、非対応なら false を返します。

ERC7579における要件

コントラクトアカウントがERC165を実装する場合、以下の条件を満たす必要があります:

  • 宣言したインターフェースに実際に機能を実装していない場合(revertする関数など)は、対応していないものとして扱わなければなりません。
  • 具体的には、そういったインターフェースIDに対して supportsInterface が false を返す必要があります。

これは、インターフェースに形式的にだけ対応しているように見せかけることを禁止するためのルールです。

Modules

ERC7579では、コントラクトアカウントの機能をモジュールとして外部化し、役割ごとに分類・管理できるように設計されています。 モジュールにはそれぞれタイプIDが割り振られており、コントラクトアカウントや他のモジュールが種類を識別するために使用されます。

モジュールの種類とタイプID

モジュールは以下の4種類に分類され、それぞれ固有のタイプIDを持ちます。

  • バリデータ(Validator)
    • type id: 1
  • エグゼキュータ(Executor)
    • type id: 2
  • フォールバックハンドラ(Fallback)
    • type id: 3
  • フック(Hook)
    • type id: 4

1つのモジュールが複数のタイプを持つことも可能です。

IERC7579Module

全てのモジュールは以下の3つの関数を持つ必要があります。

IERC7579Module.sol
interface IERC7579Module {
     /**
     * @dev This function is called by the smart account during installation of the module
     * @param data arbitrary data that may be required on the module during `onInstall` initialization
     *
     * MUST revert on error (e.g. if module is already enabled)
     */
    function onInstall(bytes calldata data) external;

    /**
     * @dev This function is called by the smart account during uninstallation of the module
     * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization
     *
     * MUST revert on error
     */
    function onUninstall(bytes calldata data) external;

    /**
     * @dev Returns boolean value if module is a certain type
     * @param moduleTypeId the module type ID according the ERC-7579 spec
     *
     * MUST return true if the module is of the given type and false otherwise
     */
    function isModuleType(uint256 moduleTypeId) external view returns(bool);
}
onInstall

コントラクトアカウントにモジュールを追加する関数。 モジュールは data を受け取り初期化処理を行います。 すでにインストールされている場合などは revert する必要があります。

onUninstall

コントラクトアカウントからモジュールを取り除く関数。 data によってステートの後処理が可能です。

isModuleType

特定のモジュールが引数として与えられた moduleTypeId であるかを確認する関数。

データ構造の工夫

1つのモジュールが複数のタイプを持つ場合、onInstall や onUninstall に渡される data に moduleTypeId を含めることで、タイプごとの初期化処理を柔軟に制御できます。

例:

js
// Module.sol
function onInstall(bytes calldata data) external {
    // ...
    (uint256 moduleTypeId, bytes memory otherData) = abi.decode(data, (uint256, bytes));
    // ...
}

バリデータ(Validator)

必ず IERC7579Module と IERC7579Validator を実装する必要があります。

  • type id: 1
IERC7579Validator.sol
interface IERC7579Validator is IERC7579Module {
    /**
     * @dev Validates a UserOperation
     * @param userOp the ERC-4337 PackedUserOperation
     * @param userOpHash the hash of the ERC-4337 PackedUserOperation
     *
     * MUST validate that the signature is a valid signature of the userOpHash
     * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch
     */
    function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256);

    /**
     * @dev Validates a signature using ERC-1271
     * @param sender the address that sent the ERC-1271 request to the smart account
     * @param hash the hash of the ERC-1271 request
     * @param signature the signature of the ERC-1271 request
     *
     * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid
     * MUST NOT modify state
     */
    function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view returns (bytes4);
}
validateUserOp

ERC4337の UserOperation を検証します。 不正な署名の場合は revert せず、定義された戻り値(SIG_VALIDATION_FAILED)を返すことが推奨されます。

isValidSignatureWithSender

ERC1271に基づく署名検証を行う関数。 MAGIC_VALUE を返すことで署名が有効であることを示します。

Executor

IERC7579Moduleのみを実装します。

  • type id: 2

コントラクトアカウントの executeFromExecutor (コントラクトアカウントの代わりに処理を実行する関数)を実行するために使用されます。

フォールバックハンドラ(Fallback Handler)

IERC7579Moduleを実装します。

  • type id: 3

アクセス制御に関する注意

アクセス制御を行う場合、msg.sender を使わず、ERC2771に基づいた _msgSender() を使う必要があります。 これはフォワードされたトランザクションの元の送信者を正しく識別するためです。

Hooks

IERC7579ModuleとIERC7579Hookを実装します。

  • type id: 4
IERC7579Hook.sol
interface IERC7579Hook is IERC7579Module {
    /**
     * @dev Called by the smart account before execution
     * @param msgSender the address that called the smart account
     * @param value the value that was sent to the smart account
     * @param msgData the data that was sent to the smart account
     *
     * MAY return arbitrary data in the `hookData` return value
     */
    function preCheck(address msgSender, uint256 value, bytes calldata msgData) external returns (bytes memory hookData);

    /**
     * @dev Called by the smart account after execution
     * @param hookData the data that was returned by the `preCheck` function
     *
     * MAY validate the `hookData` to validate transaction context of the `preCheck` function
     */
    function postCheck(bytes calldata hookData) external;
}
preCheck

実行前に呼び出され、任意のデータ(hookData)を返す関数。

postCheck

実行後に呼び出され、preCheck で返した hookData に基づいて検証や後処理を行う関数。

補足

最小構成のアプローチ

コントラクトアカウントは新しい概念であり、最適な設計方法についてはまだ発展途上です。 そのため、具体的な実装については言及せず、異なる実装間での互換性を確保するための最小限のインターフェースだけを定義する方針がとられています。

この仕様策定は以下の2つの方針に基づいています。

  • 実際に運用された既存のコントラクトアカウントや、それらの互換レイヤーの開発から得られた知見に基づいている
  • 将来的なアーキテクチャの多様化に対応できるよう、インターフェースは最小かつ柔軟性の高いものにする

拡張機構

標準は最小に保ちつつも、革新的な機能やベンダーごとの拡張も可能にすべきです。

そのため、将来的に必要な追加機能については、本体の標準に影響を与えず、拡張仕様(Extension)として独立したERCとして提案する方針が示されています。 拡張の命名には [FEATURE] Extension for ERC-7579 のような形式を用いることが推奨されます。

実行モードの設計(Execution Mode)

異なる方法でコールデータを実行するため、実行種別ごとに関数を分けるのではなく、実行方法をひとつの bytes32 値で表現する方式が採られています。

この execution mode の構成は以下の通りです。

  • 先頭1バイト(callType)
    • 単体実行、バッチ実行、delegatecall などの呼び出し方法
  • 次の1バイト(execType)
    • 失敗時に revert するか、しないでエラーハンドリングするか
  • 次の4バイト(未使用領域)
    • 将来の標準化のための予約領域
  • 次の4バイト(modeSelector)
    • カスタム実行モード識別子。Solidityのセレクタと同様の衝突回避性を持つ
  • 残り22バイト(modePayload)
    • フックアドレスや設定フラグなど、追加データを自由に埋め込む領域

この構成により、将来的な実行方法の拡張がしやすく、コードも読みやすく、保守や監査がしやすい形式となっています。

モジュールタイプの識別

バリデータとエグゼキュータなど、異なる役割のモジュールを明確に区別しないと、不適切な権限付与によるセキュリティリスクが発生します。

例)バリデーターモジュールが、誤ってエグゼキュータとして扱われることで、コントラクトアカウント上で任意のトランザクションを実行できてしまう。

このようなリスクを防ぐため、モジュールの種別は moduleTypeId により厳密に区別する必要があります。

アカウントIDの意義

コントラクトアカウントには、識別用の accountId を持たせることができます。 このIDは主にフロントエンドやライブラリが、アカウントの種類やバージョンに応じた適切な処理を行うために利用されます。

他の代替案として以下があります。

  • ERC165のようにインターフェースの違いを宣言する方式 → 拡張性に欠け、定義された機能セットに依存する
  • accountId のハッシュを返す方式 → 読みやすさに欠け、人間にとって意味のある識別が難しい

このため、柔軟かつ人間可読性のある accountId 方式が最も実用的とされています。

ERC4337 への依存と今後の方針

現在の仕様では、コントラクトアカウントの検証フローに ERC4337への依存が存在します。 ただし、今後はロールアップや他のL1で ネイティブなAccount Abstractionが登場する可能性があります。 この場合、ERC4337に依存しないコントラクトアカウントが求められます。 そのときは ERC4337関連のインターフェースを拡張仕様として分離し、必要に応じて組み込む方式に移行する必要があります。

現在は、実用的にERC4337上のモジュラーアカウントしか存在しないため、まずはその実装に対応した仕様を標準化し、将来的に柔軟な移行ができる構成としています。

互換性

プロキシ経由でデプロイされたアカウント

すでにプロキシパターンを使ってデプロイされているコントラクトアカウントは、実装ロジックを差し替えることでERC7579に準拠させることが可能です。 これは、プロキシが持つアップグレード機能により、新しい標準に対応した実装を反映できるためです。

非アップグレード型のアカウント

一方、アップグレードできない単体のコントラクトとしてデプロイされているコントラクトアカウントであっても、条件次第でERC7579に準拠することが可能です。 例えば、コントラクトに fallback 関数が存在し、外部からFallbackハンドラを追加できる設計であれば、準拠したアダプターをFallbackハンドラとして追加することで互換性を持たせることができます。

参考実装

IERC7579Account.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.21;

interface IERC7579Account {
    // MUST be emitted when a module is installed
    event ModuleInstalled(uint256 moduleTypeId, address module);

    // MUST be emitted when a module is uninstalled
    event ModuleUninstalled(uint256 moduleTypeId, address module);

    /**
     * @dev Executes a transaction on behalf of the account.
     * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
     * @param executionCalldata The encoded execution call data
     *
     * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337
     * If a mode is requested that is not supported by the Account, it MUST revert
     */
    function execute(bytes32 mode, bytes calldata executionCalldata) external;

    /**
     * @dev Executes a transaction on behalf of the account.
     *         This function is intended to be called by Executor Modules
     * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
     * @param executionCalldata The encoded execution call data
     *
     * MUST ensure adequate authorization control: i.e. onlyExecutorModule
     * If a mode is requested that is not supported by the Account, it MUST revert
     */
    function executeFromExecutor(bytes32 mode, bytes calldata executionCalldata)
        external
        returns (bytes[] memory returnData);

    /**
     * @dev ERC-1271 isValidSignature
     *         This function is intended to be used to validate a smart account signature
     * and may forward the call to a validator module
     * @param hash The hash of the data that is signed
     * @param data The data that is signed
     *
     * MAY forward the call to a validator module
     * The validator module MUST be called with isValidSignatureWithSender(address sender, bytes32 hash, bytes signature)
     * with sender being the msg.sender of this function
     * MUST sanitize the data parameter to before forwarding it to the validator module
     */
    function isValidSignature(bytes32 hash, bytes calldata data) external view returns (bytes4);

    /**
     * @dev Returns the account id of the smart account
     * @return accountImplementationId the account id of the smart account
     *
     * MUST return a non-empty string
     * The accountId SHOULD be structured like so:
     *        "vendorname.accountname.semver"
     * The id SHOULD be unique across all smart accounts
     */
    function accountId() external view returns (string memory accountImplementationId);

    /**
     * @dev Function to check if the account supports a certain execution mode (see above)
     * @param encodedMode the encoded mode
     *
     * MUST return true if the account supports the mode and false otherwise
     */
    function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);

    /**
     * @dev Function to check if the account supports a certain module typeId
     * @param moduleTypeId the module type ID according to the ERC-7579 spec
     *
     * MUST return true if the account supports the module type and false otherwise
     */
    function supportsModule(uint256 moduleTypeId) external view returns (bool);

    /**
     * @dev Installs a Module of a certain type on the smart account
     * @param moduleTypeId the module type ID according to the ERC-7579 spec
     * @param module the module address
     * @param initData arbitrary data that may be required on the module during `onInstall`
     * initialization.
     *
     * MUST implement authorization control
     * MUST call `onInstall` on the module with the `initData` parameter if provided
     * MUST emit ModuleInstalled event
     * MUST revert if the module is already installed or the initialization on the module failed
     */
    function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;

    /**
     * @dev Uninstalls a Module of a certain type on the smart account
     * @param moduleTypeId the module type ID according the ERC-7579 spec
     * @param module the module address
     * @param deInitData arbitrary data that may be required on the module during `onInstall`
     * initialization.
     *
     * MUST implement authorization control
     * MUST call `onUninstall` on the module with the `deInitData` parameter if provided
     * MUST emit ModuleUninstalled event
     * MUST revert if the module is not installed or the deInitialization on the module failed
     */
    function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;

    /**
     * @dev Returns whether a module is installed on the smart account
     * @param moduleTypeId the module type ID according the ERC-7579 spec
     * @param module the module address
     * @param additionalContext arbitrary data that may be required to determine if the module is installed
     *
     * MUST return true if the module is installed and false otherwise
     */
    function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext)
        external
        view
        returns (bool);
}

セキュリティ

delegatecall の危険性

コントラクトアカウントが delegatecall を使用する場合、呼び出し先コントラクトのコードがアカウントのストレージを直接操作するため、非常に注意が必要です。

不正なコントラクトに delegatecall してしまうと、アカウントのストレージやステートが改ざんされるリスクがあります。 呼び出し先が信頼できるかどうかを事前に検証する仕組みが必要です。

モジュールの onInstall / onUninstall に関するリスク

モジュールをインストールまたはアンインストールする時に呼び出される onInstall や onUninstall 関数には、以下のような問題が起こる可能性があります。

  • 再入可能性(reentrancy)攻撃
    • コールバックが発生するため、アカウントの内部ステートが予期しない順序で変更される可能性があります。
  • アンインストール拒否
    • 悪意あるモジュールが onUninstall 内で意図的に revert することで、モジュールの削除を阻害することができます。

これらを防ぐには、再入保護や例外処理の明示的な設計が必要です。

単一モジュール制約のあるタイプでの上書きリスク

例えば、Fallbackハンドラのように1つのモジュールのみが有効であることを前提とするタイプにおいて、新たなモジュールを installModule で追加した場合、既存のモジュールが適切にアンインストールされなければ、意図しない挙動を招く可能性があります。

古いモジュールが残存したステートで再度追加されると、古いステートや副作用がそのまま生きる可能性があります。

Fallbackハンドラにおける認可不足

Fallbackハンドラが不適切な認可制御で設計されている場合、第三者による任意の関数呼び出しが可能となるリスクがあります。 とくに、msg.sender に基づいた制御だけを行っている場合、ERC2771によるフォワード処理の影響で認可が破られる恐れがあります。

HooksによるDoSリスク

Hooksはトランザクションの前後に preCheck や postCheck を実行できますが、以下のリスクがあります。

  • **悪意あるフックが preCheck / postCheck 内で revert すると、アカウント全体の操作が拒否される可能性があります。
  • 信頼できないHooksを追加すると、アカウントが意図的に利用不能(DoS)にされる危険性があります。

自己呼び出しによる構成操作のバイパス

現在、モジュールのインストールなどの構成関数は単一操作を前提に設計されていますが、アカウント自身(address(this))からこれらの関数を呼び出せる設計にしてしまうと複数の構成操作をバッチ的にネストして実行することが可能になります。 その結果、外部からの呼び出しには厳格な制御をしていても、内部からの自己呼び出しによって制限が回避される可能性があります。

引用

zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216), "ERC-7579: Minimal Modular Smart Accounts [DRAFT]," Ethereum Improvement Proposals, no. 7579, December 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7579.

引用

zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216), "ERC-7579: Minimal Modular Smart Accounts [DRAFT]," Ethereum Improvement Proposals, no. 7579, December 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7579.

最後に

今回は「最小限のモジュラースマートアカウントの仕組みを提案しているERC7579」についてまとめてきました! いかがだったでしょうか?

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

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