Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7679: UserOperation 构建器

构建 UserOperations 而无需耦合账户特定的逻辑。

Authors Derek Chiang (@derekchiang), Garvit Khatri (@plusminushalf), Fil Makarov (@filmakarov), Kristof Gazso (@kristofgazso), Derek Rein (@arein), Tomas Rocchi (@tomiir), bumblefudge (@bumblefudge)
Created 2024-04-05
Discussion Link https://ethereum-magicians.org/t/erc-7679-smart-account-interfaces/19547
Requires EIP-4337

摘要

不同的 ERC-4337 智能账户实现以不同的方式编码其签名、nonce 和 calldata。这使得 DApp、钱包和智能账户工具难以与智能账户集成,除非与账户特定的 SDK 集成,这会引入供应商锁定并损害智能账户的采用。

我们提出了一种标准方式,让智能账户实现将其账户特定的编码逻辑放在链上。这可以通过实现接受原始签名、nonce 或 calldata(以及上下文)作为输入的方法来实现,并以正确的格式输出它们,以便智能账户可以在验证和执行 User Operation 时使用它们。

动机

目前,要为智能账户构建一个 ERC-4337 UserOperation(简称 UserOp),需要详细了解智能账户实现的工作方式,因为每个实现都可以自由地以不同的方式编码其 nonce、calldata 和签名。

作为一个简单的例子,一个账户可能使用名为 executeFoo 的执行函数,而另一个账户可能使用名为 executeBar 的执行函数。即使它们执行相同的调用,这也会导致两个账户之间的 calldata 不同。

因此,想要为给定的智能账户发送 UserOp 的人需要:

  • 弄清楚该账户正在使用哪个智能账户实现。
  • 根据智能账户实现正确编码签名/nonce/calldata,或使用知道如何执行此操作的账户特定的 SDK。

实际上,这意味着今天大多数 DApp、钱包和 AA 工具都与特定的智能账户实现绑定,从而导致碎片化和供应商锁定。

规范

本文档中的关键词“必须 (MUST)”,“禁止 (MUST NOT)”,“需要 (REQUIRED)”,“应当 (SHALL)”,“不应 (SHALL NOT)”,“应该 (SHOULD)”,“不应该 (SHOULD NOT)”,“推荐 (RECOMMENDED)”,“可以 (MAY)”,和“可选 (OPTIONAL)”按照 RFC 2119 中的描述进行解释。

UserOp 构建器

为了符合此标准,智能账户实现必须提供一个“UserOp 构建器”合约,该合约实现 IUserOperationBuilder 接口,如下定义:

struct Execution {
    address target;
    uint256 value;
    bytes callData;
}

interface IUserOperationBuilder {
    /**
     * @dev 返回账户实现支持的 ERC-4337 EntryPoint。
     */
    function entryPoint() external view returns (address);
    
    /**
     * @dev 返回 UserOp 要使用的 nonce,给定上下文。
     * @param smartAccount 是 UserOp 发送者的地址。
     * @param context 是 UserOp 构建器正确计算 UserOp
     * 请求字段所需的数据。
     */
    function getNonce(
        address smartAccount,
        bytes calldata context
    ) external view returns (uint256);
	
    /**
     * @dev 返回 UserOp 的 calldata,给定上下文和执行。
     * @param smartAccount 是 UserOp 发送者的地址。
     * @param executions 是 (destination, value, callData) 元组,
     * UserOp 想要执行。它是一个数组,因此 UserOp 可以
     * 批量执行。
     * @param context 是 UserOp 构建器正确计算 UserOp
     * 请求字段所需的数据。
     */
    function getCallData(
        address smartAccount,
        Execution[] calldata executions,
        bytes calldata context
    ) external view returns (bytes memory);
    
    /**
     * @dev 返回正确编码的签名,给定一个 UserOp,
     * 该 UserOp 已被正确填写,除了签名字段。
     * @param smartAccount 是 UserOp 发送者的地址。
     * @param userOperation 是 UserOp。UserOp 的每个字段都应该
     * 有效,除了签名字段。“PackedUserOperation”
     * 结构体的定义与 ERC-4337 中相同。
     * @param context 是 UserOp 构建器正确计算 UserOp
     * 请求字段所需的数据。
     */
    function formatSignature(
        address smartAccount,
        PackedUserOperation calldata userOperation,
        bytes calldata context
    ) external view returns (bytes memory signature);
}

使用 UserOp 构建器

要使用 UserOp 构建器构建 UserOp,构建方应按如下步骤进行:

  1. 从账户所有者处获取 UserOpBuilder 的地址和 context。从构建方的角度来看,context 是一个不透明的字节数组。 UserOpBuilder 实现可能需要 context 才能正确确定 UserOp 字段。有关更多信息,请参见 Rationale
  2. 执行一个 multicall(批量 eth_call),使用 context 和 executions 调用 getNoncegetCallData。构建方现在将获得 nonce 和 calldata。
  3. 使用先前获得的数据填写 UserOp。Gas 值可以随机设置或设置得很低。此 userOp 将用于获取 gas 估算的虚拟签名。对 userOp 的哈希进行签名。(有关什么是虚拟签名,请参见 Rationale。 有关虚拟签名安全性的详细信息,请参见 Security Considerations)。
  4. 使用 UserOp 和 context 调用(通过 eth_callformatSignature 以获得具有正确格式的虚拟签名的 UserOp。现在,此 userOp 可以用于 gas 估算。
  5. 在 UserOp 中,将现有的 gas 值更改为从适当的 gas 估算获得的值。除了 signature 字段外,此 UserOp 必须有效。对 UserOp 的哈希进行签名,并将签名放入 UserOp.signature 字段中。
  6. 使用 UserOp 和 context 调用(通过 eth_callformatSignature 以获得完全有效的 UserOp。
    1. 请注意,UserOp 比 noncecallDatasignature 具有更多的字段,但是构建方如何获得其他字段不在本文档的范围内,因为只有这三个字段在很大程度上取决于智能账户实现。

此时,构建方具有一个完全有效的 UserOp,然后他们可以将其提交给 bundler 或对其进行任何处理。

在账户尚未部署时使用 UserOp 构建器

为了向构建方提供准确的数据,在大多数情况下,UserOpBuilder 必须调用该账户。 如果该账户尚未部署,这意味着构建方希望为此账户发送第一个 UserOp,则构建方可以按如下方式修改上述流程:

  • 除了 UserOpBuilder 地址和 context 之外,构建方还获得如 ERC-4337 中定义的 factoryfactoryData
  • 在调用 UserOp 构建器上的一个 view 函数时,构建方可以使用 eth_call 来部署 CounterfactualCall 合约,该合约将部署该账户并调用 UserOpBuilder(请参见下文)。
  • 在填写 UserOp 时,构建方包括 factoryfactoryData

CounterfactualCall 合约应:

  • 使用构建方提供的 factoryfactoryData 部署该账户。
  • 如果部署未成功,则回退。
  • 如果该账户已成功部署,则调用 UserOpBuilder 并将 UserOpBuilder 返回的数据返回给构建方。

有关 CounterfactualCall 合约的更多详细信息,请参见参考实现部分。

理由

上下文

context 是一个字节数组,它编码 UserOp 构建器为了正确确定 nonce、calldata 和签名所需的所有数据。据推测,context 是由账户所有者在钱包软件的帮助下构建的。

在这里,我们概述了 context 的一种可能用法:委托。假设账户所有者希望委托一个由构建方执行的交易。账户所有者可以将构建方的公钥的签名编码在 context 内。我们将账户所有者的这个签名称为 authorization

然后,当构建方填写 UserOp 时,它将使用其自己的私钥生成的签名来填充 signature 字段。当它在 UserOp 构建器上调用 getSignature 时,UserOp 构建器将从 context 中提取 authorization,并将其与构建方的签名连接起来。据推测,智能账户的实现方式是从签名中恢复构建方的公钥,并检查该公钥实际上是否已由 authorization 签署。如果检查成功,则智能账户将执行 UserOp,从而允许构建方代表用户执行 UserOp。

虚拟签名

“虚拟签名”是指在发送给 bundler 以估计 gas (通过 eth_estimateUserOperationGas)的 UserOp 中使用的签名。需要虚拟签名,因为在 bundler 估计 gas 时,有效的签名尚未存在,因为有效的签名本身取决于 UserOp 的 gas 值,从而形成循环依赖。为了打破循环依赖,使用了虚拟签名。

但是,虚拟签名不仅仅是任何智能账户都可以使用的固定值。必须构造虚拟签名,使其导致 UserOp 使用与真实签名大约相同的 gas。因此,虚拟签名会根据智能账户用于验证 UserOp 的特定验证逻辑而变化,使其依赖于智能账户的实现。

向后兼容性

此 ERC 旨在与截至 EntryPoint 0.7 的所有 ERC-4337 智能账户向后兼容。

对于针对 EntryPoint 0.6 部署的智能账户,需要修改 IUserOperationBuilder 接口,以便将 PackedUserOperation 结构体替换为 EntryPoint 0.6 中的相应结构体。

参考实现

Counterfactual call 合约

反事实调用合约的灵感来自 ERC-6492,它设计了一种针对预部署(反事实)合约执行 isValidSignature(请参见 ERC-1271)的机制。

contract CounterfactualCall {
    
    error CounterfactualDeployFailed(bytes error);

    constructor(
        address smartAccount,
        address create2Factory, 
        bytes memory factoryData,
        address userOpBuilder, 
        bytes memory userOpBuilderCalldata
    ) { 
        if (address(smartAccount).code.length == 0) {
            (bool success, bytes memory ret) = create2Factory.call(factoryData);
            if (!success || address(smartAccount).code.length == 0) revert CounterfactualDeployFailed(ret);
        }

        assembly {
            let success := call(gas(), userOpBuilder, 0, add(userOpBuilderCalldata, 0x20), mload(userOpBuilderCalldata), 0, 0)
            let ptr := mload(0x40)
            returndatacopy(ptr, 0, returndatasize())
            if iszero(success) {
                revert(ptr, returndatasize())
            }
            return(ptr, returndatasize())
        }
    }
    
}

这是使用 ethers 和 viem 库调用此合约的示例:

// ethers
const nonce = await provider.call({
  data: ethers.utils.concat([
    counterfactualCallBytecode,
    (
      new ethers.utils.AbiCoder()).encode(['address','address', 'bytes', 'address','bytes'], 
      [smartAccount, userOpBuilder, getNonceCallData, factory, factoryData]
    )
  ])
})

// viem
const nonce = await client.call({
  data: encodeDeployData({
    abi: parseAbi(['constructor(address, address, bytes, address, bytes)']),
    args: [smartAccount, userOpBuilder, getNonceCalldata, factory, factoryData],
    bytecode: counterfactualCallBytecode,
  })
})

安全考虑

虚拟签名安全性

由于格式正确的虚拟签名将被公开披露,因此从理论上讲,它可能会被中间人拦截和使用。但是,这样做的风险和潜在危害非常低,因为在提交最终 UserOp 之后,虚拟签名将实际上无法使用(因为两个 UserOp 都使用相同的 nonce)。但是,为了缓解即使是这个小问题,建议填写 UserOp(对 UserOp 的哈希进行签名以获得未格式化的虚拟签名)(上述步骤 3)时,gas 值应非常低。

版权

版权及相关权利均通过 CC0 放弃。

Citation

Please cite this document as:

Derek Chiang (@derekchiang), Garvit Khatri (@plusminushalf), Fil Makarov (@filmakarov), Kristof Gazso (@kristofgazso), Derek Rein (@arein), Tomas Rocchi (@tomiir), bumblefudge (@bumblefudge), "ERC-7679: UserOperation 构建器 [DRAFT]," Ethereum Improvement Proposals, no. 7679, April 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7679.