2025.06.25

ERC-7579 をソースコードから読み解く

こんにちは。次世代システム研究室のT.M です。

注意

本稿は2025/6/24 時点の情報です。情報が更新されていることがありますので、ご注意ください。特にERC-7579 は現在もまだドラフトであり、仕様が変更になる可能性が大いにあります。

はじめに

Ethereum におけるスマートアカウント技術は、従来の EOA(Externally Owned Account)に比べて、署名管理やバッチ処理、ガスの抽象化などを実現する大きな進歩です。

すでに ERC-4337 によって Account Abstraction(AA)の実用化は始まっており、その概要と活用方法については以前の記事をご覧ください。

本記事では、さらに進化した ERC-7579 に注目し、その設計思想やモジュール構造、OpenZeppelin によるサンプル実装を実際のコードとともに詳しく読み解いていきます。

 

ERC-7579 とは?

ERC-7579 は、モジュールベースのスマートアカウントを構築するための規格です。各機能を独立したモジュールとして扱えるようにし、柔軟かつ拡張可能な設計を実現しています。

主なモジュールタイプとそのユースケース例:

  • Validator: トランザクションの署名検証を行います。たとえば、EOA 署名を使った基本的な認証や、複数の署名者によるマルチシグ、重み付きマルチシグ、あるいは署名者の明示的同意が必要な場合などに使用されます。
  • Executor: トランザクションの実行処理を担当します。シンプルな即時実行のほか、タイムロック(遅延実行)によるセキュアな操作スケジュールなどが可能です。
  • Hook: トランザクションの実行前後に任意の処理を挿入可能です。たとえば、アクセス制御や実行ログの記録、ガス使用量の測定などが実現できます。
  • Fallback: 定義されていないコールを処理します。たとえば、特定の dApp に動的対応したり、既存プロトコルとの互換性維持に活用されます。

これにより、ユーザーや開発者はアカウントの振る舞いを自由にカスタマイズできます。

OpenZeppelin Community では以下のモジュールが実装されています。

  • ERC7579Validator.sol: Validator モジュールの共通基盤。
  • ERC7579Signature.sol: EOA のような外部署名に対応した Validator。ERC-7913 に準拠。
  • ERC7579Multisig.sol: 複数署名者によるトランザクション承認を行うモジュール。
  • ERC7579MultisigConfirmation.sol: 新しい署名者を追加する際に、その署名者の明示的な署名同意を要求。
  • ERC7579MultisigWeighted.sol: 署名者に重みを持たせて閾値に到達したときのみトランザクションを承認。
  • ERC7579Executor.sol: 即時にトランザクションを実行できる Executor の基本実装。
  • ERC7579DelayedExecutor.sol: トランザクションをスケジュールし、一定の遅延後にのみ実行可能にする Executor。

ERC-4337 との比較

観点 ERC-4337 ERC-7579
構造 モノリシック モジュールベース
カスタマイズ性 低い 高い(動的に差し替え可能)
拡張性 限定的 任意の機能をモジュールとして追加可能
メンテナンス性 困難 モジュール単位で対応可能
セキュリティ 複雑性が高まると危険 役割分離により監査が容易

OpenZeppelin 実装から見る ERC-7579

ここからは OpenZeppelin の draft-AccountERC7579 実装をベースに、主要関数をコード付きで読み解きます。

installModule()

function installModule(
    uint256 moduleTypeId,
    address module,
    bytes calldata initData
) public virtual onlyEntryPointOrSelf {
    _installModule(moduleTypeId, module, initData);
}

function _installModule(uint256 moduleTypeId, address module, bytes memory initData) internal virtual {
    require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
    require(
        IERC7579Module(module).isModuleType(moduleTypeId),
        ERC7579Utils.ERC7579MismatchedModuleTypeId(moduleTypeId, module)
    );

    if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
        require(_validators.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
    } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
        require(_executors.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
    } else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
        bytes4 selector;
        (selector, initData) = _decodeFallbackData(initData);
        require(
            _fallbacks[selector] == address(0),
            ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module)
        );
        _fallbacks[selector] = module;
    }

    IERC7579Module(module).onInstall(initData);
    emit ModuleInstalled(moduleTypeId, module);
}

この関数はモジュール(Validator, Executor, Fallback)をアカウントにインストールします。権限チェック(onlyEntryPointOrSelf)のもと、内部関数 _installModule に処理を委譲します。この内部関数では、モジュールタイプに応じて各種データ構造に登録します。Fallback モジュールの場合は、関数セレクター単位で管理されます。

uninstallModule()

function uninstallModule(
    uint256 moduleTypeId,
    address module,
    bytes calldata deInitData
) public virtual onlyEntryPointOrSelf {
    _uninstallModule(moduleTypeId, module, deInitData);
}

function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual {
    require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));

    if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
        require(_validators.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
    } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
        require(_executors.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
    } else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
        bytes4 selector;
        (selector, deInitData) = _decodeFallbackData(deInitData);
        require(
            _fallbackHandler(selector) == module && module != address(0),
            ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
        );
        delete _fallbacks[selector];
    }

    IERC7579Module(module).onUninstall(deInitData);
    emit ModuleUninstalled(moduleTypeId, module);
}

モジュールのアンインストールを行います。

_validateUserOp()

function _validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) internal virtual override returns (uint256) {
    address module = _extractUserOpValidator(userOp);
    if (isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())) {
        return IERC7579Validator(module).validateUserOp(userOp, _signableUserOpHash(userOp, userOpHash));
    }
    return super._validateUserOp(userOp, userOpHash);
}

function _extractUserOpValidator(PackedUserOperation calldata userOp) internal pure virtual returns (address) {
    return address(bytes32(userOp.nonce).extract_32_20(0));
}

_extractUserOpValidator()によりUserOperation の nonce から Validator モジュールを特定し、検証をモジュールに委任します。インストールされていない場合は親実装にフォールバックします。

executeFromExecutor()

function executeFromExecutor(
    bytes32 mode,
    bytes calldata executionCalldata
)
    public
    payable
    virtual
    onlyModule(MODULE_TYPE_EXECUTOR, Calldata.emptyBytes())
    returns (bytes[] memory returnData)
{
    return _execute(Mode.wrap(mode), executionCalldata);
}

modifier onlyModule(uint256 moduleTypeId, bytes calldata additionalContext) {
    _checkModule(moduleTypeId, msg.sender, additionalContext);
    _;
}

function _checkModule(
    uint256 moduleTypeId,
    address module,
    bytes calldata additionalContext
) internal view virtual {
    require(
        isModuleInstalled(moduleTypeId, module, additionalContext),
        ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
    );
}

function isModuleInstalled(
    uint256 moduleTypeId,
    address module,
    bytes calldata additionalContext
) public view virtual returns (bool) {
    if (moduleTypeId == MODULE_TYPE_VALIDATOR) return _validators.contains(module);
    if (moduleTypeId == MODULE_TYPE_EXECUTOR) return _executors.contains(module);
    if (moduleTypeId == MODULE_TYPE_FALLBACK) return _fallbacks[bytes4(additionalContext[0:4])] == module;
    return false;
}

Executor モジュールから_execute() を実行をします。onlyModule() を用いてインストール済みのモジュールかをチェックします。

_fallback()

function _fallback() internal virtual returns (bytes memory) {
    address handler = _fallbackHandler(msg.sig);
    require(handler != address(0), "missing handler");

    (bool success, bytes memory returndata) = handler.call{value: msg.value}(
        abi.encodePacked(msg.data, msg.sender)
    );

    if (success) return returndata;
    assembly { revert(add(returndata, 0x20), mload(returndata)) }
}

定義されていない関数が呼び出された場合に実行されるフォールバック処理です。対応するハンドラに対して msg.data + msg.sender を引き渡します。

まとめ

ERC-7579 は、スマートアカウントのカスタマイズ性・保守性・セキュリティを高次元でバランスさせる革新的な規格です。今後、より多くのモジュールライブラリやテンプレートが登場することで、スマートアカウントの構築がさらに簡単かつ安全になるでしょう。

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。

 

参考

https://erc7579.com/

https://github.com/OpenZeppelin/openzeppelin-contracts/

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事