はじめに

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

今回は、EIP7702を利用したバッチトランザクションの仕組みを提案しているERC7821についてまとめていきます!

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

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

概要

ERC7821は、デリゲーションのための最小限のバッチ実行インターフェースを提案しています。 デリゲーションとは、あるコントラクトが他のコントラクトに対して実行ロジックを提供する仕組みです。 このインターフェースを導入することで、コントラクト間でのバッチ実行を標準化して効率的に処理できるようになります。

動機

EIP7702の登場により、EOAでもアトミックなバッチ実行が可能になりました。 これまでEOAでは、通常1回のトランザクションにつき1つの処理しか行えませんでしたが、EIP7702の仕組みを活用することで複数の処理を1つのトランザクションでまとめて実行できます。

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

EIP7702によって、今後さまざまなプロバイダーが異なるデリゲーションを提供することが予想されます。 異なる実装が乱立すると、それぞれのデリゲーションの使い方が異なり、互換性の問題が発生する可能性があります。 そこで、統一された実行インターフェースを用意することで、互換性を確保して異なるデリゲーション間のスムーズな連携を可能にします。

また、EIP7702のデリゲーションは慎重に行う必要があります。 なぜなら、デリゲーションの切り替えを頻繁に行うとセキュリティリスクが高まり、トランザクション実行のたびにガス代が発生してコストが増大するためです。 そこで、デリゲーションの切り替えを最小限に抑えるために、統一された実行インターフェースが役立ちます。

EIP5792との関係

ERC7821は、EIP5792で定義された wallet_sendCalls APIとも関連しています。 EIP5792は、ウォレットが外部からバッチトランザクションを送信できる仕組みを提供する規格です。 ERC7821では、EOAがアトミックバッチ実行機能を持っているかどうかを判別し、それに対応した calldata(コントラクトへのリクエストデータ)を作成できるようにします。

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

アトミックバッチ実行の利点

バッチ実行を利用すると、トランザクションの待ち時間やコストを大幅に削減できます。 従来の方法では、複数の処理を個別に送信する必要があり、そのたびにガス代がかかる上に処理が完了するまでの時間も長くなっていました。 しかし、EIP7702のデリゲーションを活用すれば、複数の処理を1つのトランザクションにまとめて実行できるため、処理速度が向上してコストも節約できます。

仕様

IERC7821.sol
/// @dev Interface for minimal batch executor.
interface IERC7821 {
    /// @dev Call struct for the `execute` function.
    struct Call {
        address target; // Replaced with `address(this)` if `address(0)`.
        uint256 value; // Amount of native currency (i.e. Ether) to send.
        bytes data; // Calldata to send with the call.
    }

    /// @dev Executes the calls in `executionData`.
    /// Reverts and bubbles up error if any call fails.
    ///
    /// `executionData` encoding (single batch):
    /// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`.
    /// - Else, `executionData` is `abi.encode(calls, opData)`.
    ///   See: https://eips.ethereum.org/EIPS/eip-7579
    ///
    /// `executionData` encoding (batch of batches):
    /// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]`
    ///   is an `executionData` for a single batch.
    ///
    /// Supported modes:
    /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
    /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
    /// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
    ///
    /// For the "batch of batches" mode, each batch will be recursively passed into
    /// `execute` internally with mode `0x01000000000078210001...`.
    /// Useful for passing in batches signed by different signers.
    ///
    /// Authorization checks:
    /// - If `opData` is empty, the implementation SHOULD require that
    ///   `msg.sender == address(this)`.
    /// - If `opData` is not empty, the implementation SHOULD use the signature
    ///   encoded in `opData` to determine if the caller can perform the execution.
    /// - If `msg.sender` is an authorized entry point, then `execute` MAY accept
    ///   calls from the entry point, and MAY use `opData` for specialized logic.
    ///
    /// `opData` may be used to store additional data for authentication,
    /// paymaster data, gas limits, etc.
    ///
    /// For calldata compression efficiency, if a Call.target is `address(0)`,
    /// it will be replaced with `address(this)`.
    function execute(bytes32 mode, bytes calldata executionData)
        external
        payable;

    /// @dev Provided for execution mode support detection.
    /// Only returns true for:
    /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
    /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
    /// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
    function supportsExecutionMode(bytes32 mode) external view returns (bool);
}

ERC7821のインターフェース IERC7821 は、コントラクトが複数のトランザクションをまとめて実行するための仕組みを提供します。

Call

このインターフェースでは、Call 構造体を用いて実行する内容を指定します。

  • target
    • 呼び出し先のアドレスを指定します。
    • address(0) が指定された場合は、自動的に address(this) に置き換えられて実行元のコントラクト自体が対象となります。
  • value
    • ネイティブトークンの送信額を指定します。
  • data
    • 実行するコールデータ(コントラクトの関数呼び出しに必要なデータ)を格納します。

これにより、複数のトランザクションを1つの処理としてまとめて実行することが可能になります。

execute

バッチ実行を実行するためのメインの関数。 この関数に executionData を渡すことで、複数の処理を一度に実行できます。

executionData のエンコード方法は2種類あります。

  • 単一のバッチ処理(1回のバッチ実行)

opData(オプションのデータ)がない場合は abi.encode(calls) という形式でエンコードします。
opData がある場合は abi.encode(calls, opData) という形式でエンコードします。

  • 複数のバッチ処理をまとめて実行する場合(「バッチのバッチ」モード)

executionData は abi.encode(bytes[]) という形式でエンコードされ、bytes[] の各要素が個別のバッチの executionData になります。
それぞれのバッチは、内部で再帰的に execute に渡され、処理されます。

実行モードの種類

以下の3つのモードが定義されています。

  • 0x01000000000000000000...
    • 単一のバッチを処理するモードです。
    • opData は使えません。
  • 0x01000000000078210001...
    • 単一のバッチを処理するモードですが、opData の使用が可能です。
  • 0x01000000000078210002...
    • 複数のバッチをまとめて処理するモードです。
    • このモードはオプションであり、実装によってはサポートしない場合もあります。

認可の仕組み

このインターフェースでは、opData の有無によって、異なる方法で実行権限をチェックすることが求められます。

  • opData がない場合、msg.sender が address(this) であることを要求するべき。
    • コントラクト自身が内部的に execute を呼び出す場合にのみ実行を許可するためのチェックです。
  • opData がある場合、opData に含まれる署名データを検証し、実行権限を持つかどうかを判断するべき。
  • msg.sender が信頼されたエントリーポイント(外部からのアクセスを許可する特定のアドレス)の場合、そのエントリーポイント経由での実行を許可することも可能です。

opData には、認証情報のほか、Paymaster(ガス代を負担する仕組み)に関するデータや、ガス制限に関する情報などを含めることができます。

supportsExecutionMode

特定の実行モードがサポートされているかどうかを判定する関数。 この関数が true を返す場合、そのモードは実行可能であることを示します。

この関数は、以下の3つのモードに対応しているかを確認するために使われます。

  • 0x01000000000000000000...(単一バッチ、opData なし)
  • 0x01000000000078210001...(単一バッチ、opData あり)
  • 0x01000000000078210002...(複数バッチの処理)

「バッチのバッチ」モード(0x01000000000078210002...)のサポートはオプションであり、もしサポートしない場合は false を返す必要があります。

フロントエンドでの推奨事項

このインターフェースを利用するウォレットやアプリケーションのフロントエンドは、まず supportsExecutionMode(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000)) を呼び出し、その返り値が true であることを確認するべきです。

その後、execute(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000), abi.encode(calls)) を実行することで、バッチトランザクションを送信できます。

これにより、EOAを使った「承認(approve)」と「スワップ(swap)」の一連の操作を、デリゲーションを活用しながら効率的に実行することができます。

補足

目的と設計思想

ERC7821の目的は、できる限りシンプルな仕様を採用して広く普及しやすい形にすることです。 できる限りシンプルな構成にすることで導入しやすくする方針を取っています。 特に、分散型取引所(DEX)でEOAによる「承認(approve)+スワップ(swap)」ワークフローを迅速にサポートできるようにすることを目指しています。

execute と supportsExecutionMode の採用理由

ERC7821では、execute と supportsExecutionMode という関数を採用しています。 これは、ERC7579との互換性を高めるために採用しています。

また、最小限の仕様を維持するために、ERC165やERC7579にある executeFromExecutor 関数の実装は必須としていません。

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

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

opData のエンコードをオプションとする理由

opData(操作データ)のエンコード方式は、開発者が自由に選べるようになっています。 executionData に opData を含めるかどうかは、abi.encode(calls) もしくは abi.encode(calls, opData) のどちらかの方法で決められます。

この設計のメリットは、開発者が必要な場合にのみ opData を利用できる点です。 opData には認証情報やPaymaster(トランザクションのガス代を負担する仕組み)に関するデータを含めることができますが必須ではないです。

address(0) を address(this) に置き換える理由

ERC7821では、Call 構造体の target(呼び出し先アドレス)に address(0) が指定された場合、自動的に address(this) に置き換える仕様になっています。 これは、calldataの圧縮効率を向上させるためです。

コントラクトのトランザクションでは、calldataのサイズが小さいほどガス代が節約できます。 address(0) を address(this) に置き換えることでコントラクト自身を呼び出す時のデータサイズを小さくし、ガスコストの削減が期待できます。

「バッチのバッチ」モードをオプションにした理由

「バッチのバッチ」モードは、1つのトランザクションで複数のバッチを処理できるオプション機能です。 これにより、異なる署名者が署名したバッチをまとめて送信できるようになります。 例えば、異なるアカウントが個別に署名したトランザクションを、1つの execute 呼び出しで一括処理することが可能になります。

参考実装

ERC7821.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.4;

/// @notice Minimal batch executor mixin.
abstract contract ERC7821 {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STRUCTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Call struct for the `execute` function.
    struct Call {
        address target; // Replaced with `address(this)` if `address(0)`.
        uint256 value; // Amount of native currency (i.e. Ether) to send.
        bytes data; // Calldata to send with the call.
    }

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

    /// @dev The execution mode is not supported.
    error UnsupportedExecutionMode();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                    EXECUTION OPERATIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Executes the calls in `executionData`.
    /// Reverts and bubbles up error if any call fails.
    ///
    /// `executionData` encoding (single batch):
    /// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`.
    /// - Else, `executionData` is `abi.encode(calls, opData)`.
    ///   See: https://eips.ethereum.org/EIPS/eip-7579
    ///
    /// `executionData` encoding (batch of batches):
    /// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]`
    ///   is an `executionData` for a single batch.
    ///
    /// Supported modes:
    /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
    /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
    /// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
    ///
    /// For the "batch of batches" mode, each batch will be recursively passed into
    /// `execute` internally with mode `0x01000000000078210001...`.
    /// Useful for passing in batches signed by different signers.
    ///
    /// Authorization checks:
    /// - If `opData` is empty, the implementation SHOULD require that
    ///   `msg.sender == address(this)`.
    /// - If `opData` is not empty, the implementation SHOULD use the signature
    ///   encoded in `opData` to determine if the caller can perform the execution.
    /// - If `msg.sender` is an authorized entry point, then `execute` MAY accept
    ///   calls from the entry point, and MAY use `opData` for specialized logic.
    ///
    /// `opData` may be used to store additional data for authentication,
    /// paymaster data, gas limits, etc.
    ///
    /// For calldata compression efficiency, if a Call.target is `address(0)`,
    /// it will be replaced with `address(this)`.
    function execute(bytes32 mode, bytes memory executionData)
        public
        payable
        virtual
    {
        uint256 id = _executionModeId(mode);
        if (id == 3) {
            mode ^= bytes32(uint256(3 << (22 * 8)));
            bytes[] memory batches = abi.decode(executionData, (bytes[]));
            for (uint256 i; i < batches.length; ++i) {
                execute(mode, batches[i]);
            }
            return;
        }
        if (id == uint256(0)) revert UnsupportedExecutionMode();
        bool tryWithOpData;
        /// @solidity memory-safe-assembly
        assembly {
            let t := gt(mload(add(executionData, 0x20)), 0x3f)
            let executionDataLength := mload(executionData)
            tryWithOpData := and(eq(id, 2), and(gt(executionDataLength, 0x3f), t))
        }
        Call[] memory calls;
        bytes memory opData;
        if (tryWithOpData) {
            (calls, opData) = abi.decode(executionData, (Call[], bytes));
        } else {
            calls = abi.decode(executionData, (Call[]));
        }
        _execute(calls, opData);
    }

    /// @dev Provided for execution mode support detection.
    /// Only returns true for:
    /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
    /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
    /// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
    function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) {
        return _executionModeId(mode) != 0;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                      INTERNAL HELPERS                      */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support, 3: batch of batches.
    function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) {
        // Only supports atomic batched executions.
        // For the encoding scheme, see: https://eips.ethereum.org/EIPS/eip-7579
        // Bytes Layout:
        // - [0]      ( 1 byte )  `0x01` for batch call.
        // - [1]      ( 1 byte )  `0x00` for revert on any failure.
        // - [2..5]   ( 4 bytes)  Reserved by ERC7579 for future standardization.
        // - [6..9]   ( 4 bytes)  `0x00000000` or `0x78210001` or `0x78210002`.
        // - [10..31] (22 bytes)  Unused. Free for use.
        uint256 m = (uint256(mode) >> (22 * 8)) & 0xffff00000000ffffffff;
        if (m == 0x01000000000078210002) id = 3;
        if (m == 0x01000000000078210001) id = 2;
        if (m == 0x01000000000000000000) id = 1;
    }

    /// @dev Executes the calls and returns the results.
    /// Reverts and bubbles up error if any call fails.
    function _execute(Call[] memory calls, bytes memory opData)
        internal
        virtual
    {
        // Very basic auth to only allow this contract to be called by itself.
        // Override this function to perform more complex auth with `opData`.
        if (opData.length == uint256(0)) {
            require(msg.sender == address(this));
            // Remember to return `_execute(calls)` when you override this function.
            return _execute(calls);
        }
        revert(); // In your override, replace this with logic to operate on `opData`.
    }

    /// @dev Executes the calls.
    /// Reverts and bubbles up error if any call fails.
    function _execute(Call[] memory calls) internal virtual {
        for (uint256 i; i < calls.length; ++i) {
            Call memory c = calls[i];
            address target = c.target == address(0) ? address(this) : c.target;
            _execute(target, c.value, c.data);
        }
    }

    /// @dev Executes the call.
    /// Reverts and bubbles up error if the call fails.
    function _execute(address target, uint256 value, bytes memory data)
        internal
        virtual
    {
        (bool success, bytes memory result) = target.call{value: value}(data);
        if (success) return;
        /// @solidity memory-safe-assembly
        assembly {
            // Bubble up the revert if the call reverts.
            revert(add(result, 0x20), mload(result))
        }
    }
}

セキュリティ

実行に対するアクセス制御

適切なアクセス制御を実装する必要があります。

引用

Vectorized (@Vectorized), Jake Moxey (@jxom), Hadrien Croubois (@Amxx), "ERC-7821: Minimal Batch Executor Interface [DRAFT]," Ethereum Improvement Proposals, no. 7821, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7821.

最後に

今回は「EIP7702を利用したバッチトランザクションの仕組みを提案している**ERC7821*」についてまとめてきました! いかがだったでしょうか?

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

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