Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7821: 最小批量执行器接口

用于委托的最小批量执行器接口

Authors Vectorized (@Vectorized), Jake Moxey (@jxom), Hadrien Croubois (@Amxx)
Created 2024-11-21
Discussion Link https://ethereum-magicians.org/t/erc-7821-minimal-batch-executor-interface/21776
Requires EIP-7579

摘要

本提案定义了一个用于委托的最小批量执行器接口。委托是一个智能合约,它实现了其他智能合约可以委托给它的逻辑。这允许以标准化的方式准备原子批量执行。

动机

随着 EIP-7702 的出现,外部所有账户 (EOA) 可以执行原子批量执行。

我们预计将会有多个主要供应商提供多个 EIP-7702 委托。执行接口的标准将实现更好的互操作性。EIP-7702 委托是一个有风险的过程,应该谨慎进行——不应在用户每次切换网站时执行。此外,EIP-7702 委托是需要 gas 费用的交易,这使得频繁切换委托变得不经济。标准化的执行接口将减少切换委托的需求。

该标准补充了 EIP-5792 中的 wallet_sendCalls API。它支持检测 EOA 上的原子批量执行能力,并为 EOA 上的原子批量执行准备 calldata。

使用原子批量执行可以减少总延迟和总交易成本,因此对于 EOA 来说,它比顺序交易发送更可取。

因此,本提案的最大动机是,它经过精心设计,以实现最大的简单性、可扩展性、性能和兼容性。

规范

本文档中的关键词“必须”、“不得”、“需要”、“应”、“不应”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

概述

最小批量执行器接口定义如下:

/// @dev 最小批量执行器的接口。
interface IERC7821 {
    /// @dev `execute` 函数的调用结构体。
    struct Call {
        address to; // 如果是 `address(0)`,则替换为 `address(this)`。
        uint256 value; // 要发送的原生货币(即以太币)的数量。
        bytes data; // 要随调用一起发送的 Calldata。
    }

    /// @dev 执行 `executionData` 中的调用。
    /// 如果任何调用失败,则恢复并冒泡错误。
    ///
    /// 如果 `address(0)`,MAY 将 `Call.to` 替换为 `address(this)`。
    ///
    /// `executionData` 编码(单个批次):
    /// - 如果 `opData` 为空,则 `executionData` 仅仅是 `abi.encode(calls)`。
    /// - 否则,`executionData` 是 `abi.encode(calls, opData)`。
    ///   参见:https://eips.ethereum.org/EIPS/eip-7579
    ///
    /// `executionData` 编码(批次集合):
    /// - `executionData` 是 `abi.encode(bytes[])`,其中 `bytes[]` 中的每个元素
    ///   都是单个批次的 `executionData`。
    ///
    /// 支持的模式:
    /// - `0x01000000000000000000...`: 单个批次。不支持可选的 `opData`。
    /// - `0x01000000000078210001...`: 单个批次。支持可选的 `opData`。
    /// - `0x01000000000078210002...`: 批次集合。该模式是可选的。
    ///
    /// 对于“批次集合”模式,每个批次将以 `0x01000000000078210001...` 模式在内部递归地传递到
    /// `execute` 中。
    /// 适用于传入由不同签名者签名的批次。
    ///
    /// 授权检查:
    /// - 如果 `opData` 为空,则实现应要求 `msg.sender == address(this)`。
    /// - 如果 `opData` 不为空,则实现应使用在 `opData` 中编码的签名来确定
    ///   调用者是否可以执行该操作。
    /// - 如果 `msg.sender` 是授权的入口点,则 `execute` 可以接受来自入口点的调用,并且可以使用
    ///   `opData` 进行专门的逻辑。
    ///
    /// `opData` 可以用于存储用于身份验证、
    /// 付款人数据、gas 限制等额外数据。
    ///
    /// 为了 calldata 压缩效率,如果 Call.to 是 `address(0)`,
    /// 它将被替换为 `address(this)`。
    function execute(bytes32 mode, bytes calldata executionData)
        external
        payable;

    /// @dev 提供用于执行模式支持检测。
    /// 只返回真值:
    /// - `0x01000000000000000000...`: 单个批次。不支持可选的 `opData`。
    /// - `0x01000000000078210001...`: 单个批次。支持可选的 `opData`。
    /// - `0x01000000000078210002...`: 批次集合。该模式是可选的。
    function supportsExecutionMode(bytes32 mode) external view returns (bool);
}

OPTIONAL 支持批量模式的批量处理。如果不支持,合约必须在 supportsExecutionMode 中返回 false,对于任何以 0x01000000000078210002 开头的模式。

建议

为了支持 EOA 上具有委托的批准 + 交换工作流程,前端应:

  1. 查询 supportsExecutionMode(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000)),确保它返回 true。

  2. 执行 execute(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000), abi.encode(calls))

理由

我们的目标是实现最大程度的极简主义,以使标准尽可能的左倾。简单是采用的关键。我们的北极星是尽快让每个去中心化交易所都支持 EOA 与委托的批准 + 交换工作流程。

executesupportsExecutionMode

我们选择在 ERC-7579 中使用 executesupportsExecutionMode 函数,以实现与现有智能账户生态系统的更好兼容性。

虽然极简主义是目标,但在追求更好采用的过程中必须做出一些妥协。

为了极简主义,此标准不需要在 ERC-7579 中实现 executeFromExecutor 函数。

executionDataopData 的可选编码

opData 字节参数可以选择性地包含在 executionData 中,可以通过执行 abi.encode(calls)abi.encode(calls, opData) 来实现。

address(0) 替换为 address(this)

用于 calldata 压缩优化。

可选的批次集合模式

opData 可用于为单个批次提供身份验证数据。拥有批次集合模式将使单个交易能够提交由不同签名者签名的批次,而无需授权入口点。此模式是可选的,因为使用授权的入口点仍然可以实现相同的功能。它包含在内是为了开发人员的体验。

向后兼容性

没有向后兼容性问题。

参考实现

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

/// @notice 最小批量执行器混合合约。
abstract contract ERC7821 {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          结构体                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev `execute` 函数的调用结构体。
    struct Call {
        address to; // 如果是 `address(0)`,则替换为 `address(this)`。
        uint256 value; // 要发送的原生货币(即以太币)的数量。
        bytes data; // 要随调用一起发送的 Calldata。
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                           错误                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev 不支持执行模式。
    error UnsupportedExecutionMode();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                    执行操作                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev 执行 `executionData` 中的调用。
    /// 如果任何调用失败,则恢复并冒泡错误。
    ///
    /// `executionData` 编码(单个批次):
    /// - 如果 `opData` 为空,则 `executionData` 仅仅是 `abi.encode(calls)`。
    /// - 否则,`executionData` 是 `abi.encode(calls, opData)`。
    ///   参见:https://eips.ethereum.org/EIPS/eip-7579
    ///
    /// `executionData` 编码(批次集合):
    /// - `executionData` 是 `abi.encode(bytes[])`,其中 `bytes[]` 中的每个元素
    ///   都是单个批次的 `executionData`。
    ///
    /// 支持的模式:
    /// - `0x01000000000000000000...`: 单个批次。不支持可选的 `opData`。
    /// - `0x01000000000078210001...`: 单个批次。支持可选的 `opData`。
    /// - `0x01000000000078210002...`: 批次集合。该模式是可选的。
    ///
    /// 对于“批次集合”模式,每个批次将以 `0x01000000000078210001...` 模式在内部递归地传递到
    /// `execute` 中。
    /// 适用于传入由不同签名者签名的批次。
    ///
    /// 授权检查:
    /// - 如果 `opData` 为空,则实现应要求 `msg.sender == address(this)`。
    /// - 如果 `opData` 不为空,则实现应使用在 `opData` 中编码的签名来确定
    ///   调用者是否可以执行该操作。
    /// - 如果 `msg.sender` 是授权的入口点,则 `execute` 可以接受来自入口点的调用,并且可以使用
    ///   `opData` 进行专门的逻辑。
    ///
    /// `opData` 可以用于存储用于身份验证、
    /// 付款人数据、gas 限制等额外数据。
    ///
    /// 为了 calldata 压缩效率,如果 Call.to 是 `address(0)`,
    /// 它将被替换为 `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 提供用于执行模式支持检测。
    /// 只返回真值:
    /// - `0x01000000000000000000...`: 单个批次。不支持可选的 `opData`。
    /// - `0x01000000000078210001...`: 单个批次。支持可选的 `opData`。
    /// - `0x01000000000078210002...`: 批次集合。该模式是可选的。
    function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) {
        return _executionModeId(mode) != 0;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                      内部帮助函数                      */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev 0:无效模式,1:不支持 `opData`,2:支持 `opData`,3:批次集合。
    function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) {
        // 仅支持原子批量执行。
        // 有关编码方案,请参见:https://eips.ethereum.org/EIPS/eip-7579
        // 字节布局:
        // - [0]      ( 1 字节 )  `0x01` 用于批量调用。
        // - [1]      ( 1 字节 )  `0x00` 用于在任何失败时恢复。
        // - [2..5]   ( 4 字节)  由 ERC7579 保留用于未来的标准化。
        // - [6..9]   ( 4 字节)  `0x00000000` 或 `0x78210001` 或 `0x78210002`。
        // - [10..31] (22 字节)  未使用。免费使用。
        uint256 m = (uint256(mode) >> (22 * 8)) & 0xffff00000000ffffffff;
        if (m == 0x01000000000078210002) id = 3;
        if (m == 0x01000000000078210001) id = 2;
        if (m == 0x01000000000000000000) id = 1;
    }

    /// @dev 执行调用并返回结果。
    /// 如果任何调用失败,则恢复并冒泡错误。
    function _execute(Call[] memory calls, bytes memory opData)
        internal
        virtual
    {
        // 非常基本的身份验证,仅允许此合约由自身调用。
        // 覆盖此函数以使用 `opData` 执行更复杂的身份验证。
        if (opData.length == uint256(0)) {
            require(msg.sender == address(this));
            // 当您覆盖此函数时,请记住返回 `_execute(calls)`。
            return _execute(calls);
        }
        revert(); // 在您的覆盖中,将此替换为对 `opData` 进行操作的逻辑。
    }

    /// @dev 执行调用。
    /// 如果任何调用失败,则恢复并冒泡错误。
    function _execute(Call[] memory calls) internal virtual {
        for (uint256 i; i < calls.length; ++i) {
            Call memory c = calls[i];
            address to = c.to == address(0) ? address(this) : c.to;
            _execute(to, c.value, c.data);
        }
    }

    /// @dev 执行调用。
    /// 如果调用失败,则恢复并冒泡错误。
    function _execute(address to, uint256 value, bytes memory data)
        internal
        virtual
    {
        (bool success, bytes memory result) = to.call{value: value}(data);
        if (success) return;
        /// @solidity memory-safe-assembly
        assembly {
            // 如果调用恢复,则冒泡恢复。
            revert(add(result, 0x20), mload(result))
        }
    }
}

安全考虑

execute 的访问控制

实现应确保 execute 具有适当的访问控制。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Vectorized (@Vectorized), Jake Moxey (@jxom), Hadrien Croubois (@Amxx), "ERC-7821: 最小批量执行器接口 [DRAFT]," Ethereum Improvement Proposals, no. 7821, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7821.