Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7806: 极简的意图中心 EOA 智能账户

可扩展的意图中心 EOA 智能账户接口设计,支持批量执行、gas 赞助以及更多其他功能。

Authors hellohanchen (@hellohanchen)
Created 2024-11-02
Discussion Link https://ethereum-magicians.org/t/erc-7806-minimal-intent-centric-eoa-smart-account/21565
Requires EIP-7702

摘要

本提案定义了意图中心智能账户的标准接口。它使外部拥有账户 (EOA) 能够将合约代码委托给智能账户实现,从而允许他们签署意图。然后,这些意图可以由求解器(或中继器)代表账户所有者执行,从而简化交互并扩展 EOA 的功能。

动机

账户抽象 (AA) 是区块链行业中一个备受关注的话题,因为它增强了账户的可编程性,从而实现以下功能:

  • 批量执行
  • Gas 赞助
  • 访问控制

ERC-4337 的引入为 AA 建立了一个无需许可的标准,开启了各种强大的功能。然而,ERC-4337 有几个局限性:

  • 复杂性:该标准需要多个相互依赖的组件,包括 Account、EntryPoint、Paymaster、Bundler 和其他插件(ERC-6900ERC-7579。运行 bundler 需要大量的工程专业知识,并会带来运营开销。
  • 兼容性:组件依赖性使得升级变得繁琐,通常需要同时更新多个智能合约。这会在生态系统中造成碎片化。 一个版本更新,也会分割生态系统。
  • 成本:处理 UserOperation 交易会消耗大量的 gas。
  • 信任假设:尽管被设计为无需许可的标准,但 ERC-4337 仍然依赖于中心化实体。例如,Paymaster 通常是中心化的,因为它们必须信任账户所有者会报销 gas 成本,或者管理外部资金来源。同样,bundler 在矿工可提取价值 (MEV) 环境中运行,要求用户信任它们以包含交易。

ERC-7521 引入了一种具有意图中心设计的智能合约账户 (SCA) 解决方案。它允许求解器履行账户所有者的意图,同时保持自定义执行逻辑的灵活性,并确保向前兼容。

随着 SET_CODE_TX_TYPE=0x04 的引入,EOA 现在可以动态设置合约代码,从而赋予它们类似于 SCA 的可编程性。这为开发一种新标准提供了一个机会,该标准将 AA 功能扩展到 EOA,同时解决上述挑战。

通过简化执行、提高效率和增强用户体验,本提案旨在加速采用意图中心账户抽象智能合约。

求解器、中继器、Paymaster 和 Bundler - 合而为一

在以意图为中心的系统中,求解器在实现用户意图方面发挥着至关重要的作用,并获得相应的奖励。本提案引入了一种开放的执行模型,任何求解器都可以参与,从而营造一个有利于用户的竞争环境。

通过集成的 gas 抽象,求解器可以使用原生代币支付 gas 费用,同时从 EOA 账户收到其他代币作为补偿。此外,求解器可以通过将多个意图执行捆绑到单个区块链交易中来进一步优化成本。

每个求解器都可以自由地开发自己的策略来最大化盈利能力。本提案不对求解器如何执行意图施加限制,从而确保在不同的执行场景中的灵活性和适应性。

规范

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

UserIntent 架构

每个意图都是一个打包的数据结构,其中包含有关账户所有者想要执行的操作的足够信息。UserIntent 对象的核心结构如下:

字段 类型 描述
sender address 发起意图的账户地址。
standard address 负责验证和解析 UserIntentIStandard 实现
header bytes UserIntent 关联的元数据,由 standard 解释。为保证灵活性,存储为字节。
instructions bytes[] UserIntent 的执行细节,由 standard 解释。为保证灵活性,存储为 bytes[]
signatures bytes[] 执行所需的可验证签名,由 standard 解释。

字段说明

  • headerbytes header 可以携带有关如何验证意图或如何防止重复消费的信息。例如,header 可以包含一个 uint256 nonce 来检查 nonce 是否已被使用。
  • instructions:这些 bytes instructions 可以只是连接的 (address,value,calldata),也可以是标准化值,例如 (erc20TokenAddress,1000) 意味着 instructions 最多可以使用 1000 个指定的 ERC-20 代币。不需要 EOA 所有者提供所有 instructions,以允许在意图执行期间动态执行其他操作,但 IStandard 设计需要仔细处理这种情况。
  • signaturesbytes signatures 字段可以支持不同的签名方法。不需要 EOA 所有者提供所有 signatures,其中一些可以由求解器、中继器或任何其他人提供。

UserIntent 打包为字节

UserIntent 对象被打包和编码为 bytes calldata userIntent。对于数据结构没有严格的模式要求。每个 IAccountIStandard 实现都可以定义自己的编码和解码方法来处理 bytes 数据。

以下是打包编码格式的示例:

部分 值类型 描述
userIntent[0:20] address sender
userIntent[20:40] address standard
userIntent[40:42] uint16 header 的长度
userIntent[42:44] uint16 instructions 的长度
userIntent[44:46] uint16 signatures 的长度
下一个 headerLength 字节 bytes 实际的 header 数据
下一个 instructionsLength 字节 bytes 实际的 instructions 数据
下一个 signatureLength 字节 bytes 实际的 signatures 数据
剩余字节 bytes 额外数据,例如用于进一步执行的嵌套意图

IStandard 接口

每个标准定义了如何解析和验证 UserIntent。标准的实现必须符合 IStandard 接口:

interface IStandard {
    /**
     * 验证用户的意图
     *
     * @dev 返回验证结果,该类型使用 bytes4 以实现可扩展性
     * @return result 表示验证结果的值
     */
    function validateUserIntent(bytes calldata intent) external view returns (bytes4 result);

    /**
     * 解包用户的意图,建议在解包时验证意图以节省 gas
     *
     * @dev 返回解包后的结果,该类型使用 bytes 以实现可扩展性
     * @return result 解包后的结果状态
     * @return operations 可以由 IAccount 执行的解包后的操作,不需要与 UserIntent.instructions 匹配
     */
    function unpackOperations(bytes calldata intent) external view returns (bytes4 result, bytes[] memory operations);
}

IStandard 接口负责定义和执行 UserIntent 对象的验证逻辑。

它的运行方式类似于 ERC-4337 和 ERC-7521 中的 EntryPoint

bytes4 返回类型的可扩展性允许未来的升级,而无需修改函数签名。

IAccount 接口

在账户方面,IAccount 提供了执行 bytes calldata intent 的接口:

interface IAccount {
    /**
     * 执行用户的意图
     * 
     * @dev 返回执行结果,该类型使用 bytes 以实现可扩展性
     * @return result 表示执行结果的值
     */
    function executeUserIntent(bytes calldata intent) external returns (bytes memory);
}

使用 SET_CODE_TX_TYPE=0x04,EOA 可以将合约代码委托给 IAccount 实现,从而使它们可以充当智能账户。单个账户实现可以在多个 EOA 之间共享,这意味着:

  • 它只需要部署和审计一次。
  • 每个 EOA 所有者负责将他们的账户委托给安全的 IAccount 实现。

建议每个账户利用 IStandard 来验证和解包操作,请查看参考实现中的示例。账户智能合约可以是无状态的,以避免与其他委托合约共享存储空间。

理由

字节的使用

UserIntent 对象定义为结构可以提高可读性,并使其在 Solidity 中更易于使用。例如:

struct UserIntent {
    address sender;
    address standard;
    bytes header;
    bytes[] instructions;
    bytes[] signatures;
}

但是,这种方法有几个缺点:

  • 强制所有 IAccountIStandard 实现都遵循这种特定的结构格式会降低灵活性。
  • 由于 Solidity 的动态数组编码,bytes[] 的使用会增加额外的 gas 成本。

由于 UserIntent 结构中的所有对象都是可选的,并且它们的使用取决于 IStandardIAccount 实现,因此字节格式可确保最大的灵活性,同时保持兼容性。

在 EOA 合约代码中执行

使用 SET_CODE_TX_TYPE=0x04,EOA 获得执行合约代码的能力。直接从 EOA 执行交易提供了几个关键优势:

  • 保留 EOA 控制:执行仍然完全由账户所有者控制。如果需要,EOA 所有者可以通过取消委托合约代码轻松禁用所有智能合约功能。
  • 一致的 msg.sender 行为:由于执行源自 EOA,因此 msg.sender 始终解析为 EOA 地址,从而简化了身份验证和权限检查。
  • 无状态执行:可以将执行逻辑设计为无状态,从而允许 IAccount 实现避免存储持久数据,从而降低存储成本。

如果 EOA 不需要智能合约执行,或者执行意图的成本太高,则所有者仍然可以将该账户用作常规 EOA,而无需进行任何修改。

在标准合约中进行验证

验证逻辑通常依赖于合约状态。例如,加权多所有者签名方案需要跟踪分配给每个签名者的权重。将意图验证完全保留在 IStandard 中具有多个优势:

  • 简化的实现:通过镜像 ERC-4337 中的 EntryPoint 概念,但以更简单的形式,IStandard 仅专注于验证。
  • 更易于审计和维护:由于 IStandard 仅负责验证,因此合约工程师可以更轻松地实施、审计和维护。
  • 模块化验证IStandard 接口本质上是模块化的,从而允许更复杂的验证机制。例如,“复合”标准可以将意图分解为更小的组件,分别验证每个组件,然后组合结果。

Gas 抽象

此设计允许通过允许任何地址代表意图的发送者发起交易来实现无 gas 交易。

  • 发送者可以在意图的 headerinstructions 中指定如何以及支付什么。
  • 可以从发送者的账户中以任何代币进行支付。
  • 交易成本可以通过将代币从发送者的账户转移到 tx.origin(提交交易的地址)来支付。

不强制执行重入保护

本提案不强制执行内置的重入保护机制,例如 nonce。此决定的理由是某些意图本质上被设计为多次执行。

每个标准都应根据其预期用例定义自己的保护规则,而不是全局重入保护机制。鼓励实施者:

向后兼容性

IAccount 标准与 EOA 合约代码执行的引入(SET_CODE_TX_TYPE=0x04)共享相同的向后兼容性注意事项。

参考实现

辅助库

PackedIntent 是一个库,用于从打包编码的意图中解码 (address sender, address standard, uint16 headerLength, uint16 instructionsLength, uint16 signaturesLength)。以下 IAccountIStandrd 实现都遵循 PackedIntent 模式。

/// @title PackedIntent
/// @notice 这是一个将意图的元数据(发送者、标准、长度)打包到字节中的库
/// @dev 打包的意图数据模式定义如下:
/// @dev 1. sender: address, 20 字节
/// @dev 2. standard: address, 20 字节
/// @dev 3. headerLength: uint16, 2 字节
/// @dev 4. instructionLength: uint16, 2 字节
/// @dev 5. signatureLength: uint16, 2 字节
library PackedIntent {
    /// @notice getSenderAndStandard 是一个从意图中获取发送者和标准的函数
    /// @param intent 要从中获取发送者和标准的意图
    /// @return sender 意图的发送者
    /// @return standard 意图的标准
    function getSenderAndStandard(bytes calldata intent) external pure returns (address, address) {
        require(intent.length >= 40, "Intent 太短");
        return (address(bytes20(intent[: 20])), address(bytes20(intent[20 : 40])));
    }

    /// @notice getLengths 是一个从意图中获取长度的函数
    /// @param intent 用于获取长度的意图
    /// @return headerLength header 的长度
    /// @return instructionLength 指令的长度
    /// @return signatureLength 签名的长度
    function getLengths(bytes calldata intent) external pure returns (uint256, uint256, uint256) {
        require(intent.length >= 46, "缺少长度部分");
        return (
        uint256(uint16(bytes2(intent[40 : 42]))),
        uint256(uint16(bytes2(intent[42 : 44]))),
        uint256(uint16(bytes2(intent[44 : 46])))
        );
    }

    /// @notice getSignatureLength 是一个从意图中获取签名长度的函数
    /// @param intent 用于获取签名长度的意图
    /// @return signatureLength 签名的长度
    function getSignatureLength(bytes calldata intent) external pure returns (uint256) {
        require(intent.length >= 46, "缺少长度部分");
        return uint256(uint16(bytes2(intent[44 : 46])));
    }

    /// @notice getIntentLength 是一个从意图中获取意图长度的函数
    /// @param intent 用于获取意图长度的意图
    /// @return result header、instruction 和 signature 长度之和
    function getIntentLength(bytes calldata intent) external pure returns (uint256) {
        require(intent.length >= 46, "缺少长度部分");
        uint256 headerLength = uint256(uint16(bytes2(intent[40 : 42])));
        uint256 instructionLength = uint256(uint16(bytes2(intent[42 : 44])));
        uint256 signatureLength = uint256(uint16(bytes2(intent[44 : 46])));
        return headerLength + instructionLength + signatureLength + 46;
    }

    /// @notice getIntentLengthFromSection 是一个从长度部分获取意图长度的函数
    /// @param lengthSection 要从其中获取意图长度的长度部分
    /// @return result header、instruction 和 signature 长度之和
    function getIntentLengthFromSection(bytes6 lengthSection) external pure returns (uint16 result) {
        assembly {
            let value := lengthSection
            let a := shr(240, value) // 提取前 2 个字节
            let b := and(shr(224, value), 0xFFFF) // 提取接下来的 2 个字节
            let c := and(shr(208, value), 0xFFFF) // 提取最后 2 个字节
            result := add(add(add(a, b), c), 46)
        }
    }
}

中继执行标准

RelayedExecutionStandard 允许中继器执行链上的操作并从意图发送者处获取 ERC-20 代币,从而为发送者实现无 gas 体验。

import {MessageHashUtils}
import {ECDSA}
import {IERC20}
import {IStandard}
import {IAccount}
import {PackedIntent}

/// @title ERC7806Constants
/// @notice 这是一个定义 ERC7806 标准常量的库
library ERC7806Constants {
/// @notice VALIDATION_DENIED 是拒绝意图的魔术值
bytes4 public constant VALIDATION_DENIED = 0x00000000;

/// @notice VALIDATION_APPROVED 是已验证意图的魔术值
bytes4 public constant VALIDATION_APPROVED = 0x00000001;
}

abstract contract HashGatedStandard is IStandard {
    event HashUsed(address sender, uint256 hash);

    mapping(bytes32 => bool) internal _hashes;

    function checkHash(address sender, uint256 hash) external view returns (bool) {
        bytes32 compositeKey = keccak256(abi.encode(sender, hash));
        return _hashes[compositeKey];
    }

    function markHash(uint256 hash) external {
        bytes32 compositeKey = keccak256(abi.encode(msg.sender, hash));
        _hashes[compositeKey] = true;

        emit HashUsed(msg.sender, hash);
    }
}

/*
RelayedExecutionStandard

此标准允许发送者定义一系列执行指令,并要求中继器代表发送者在链上执行。它是哈希和时间门控的,意味着意图只能在时间戳之前执行,并且只能执行一次。

`intent` 的前 20 个字节是发送者地址。
`intent` 的接下来的 20 个字节是标准地址,应该等于此标准的地址。
以下是长度部分,其中包含 3 个 uint16,分别定义 header 长度、instructions 长度和 signature 长度。

header 的长度为 8 字节或 28 字节。
8 字节部分是以纪元秒为单位的时间戳。
可选的 20 字节定义了分配的中继器地址,如果发送者只希望特定的中继器执行。

instructions 包含 2 个主要部分。
前 36 个字节是一个打包编码的 (address, uint128) 对,表示发送者将支付给中继器的 'payment'。它应该是 ERC20 代币。
接下来的 1 字节是一个 uint8,定义要执行的指令数。
这些指令连接在一起,前 2 个字节 (uint16) 定义每个指令的长度,后面是指令体。指令应该是 abi.encode(address, uint256, bytes),可以直接由发送者帐户执行。

签名字段始终为 65 字节长。它包含签名的 bytes.concat(header, instructions)。
*/
contract RelayedExecutionStandard is HashGatedStandard {
    using ECDSA for bytes32;

    string public constant ICS_NUMBER = "ICS1";
    string public constant DESCRIPTION = "定时哈希中继执行标准";
    string public constant VERSION = "0.0.0";
    string public constant AUTHOR = "hellohanchen";

    function validateUserIntent(bytes calldata intent) external view returns (bytes4) {
        (address sender, address standard) = PackedIntent.getSenderAndStandard(intent);
        require(standard == address(this), "不是这个标准");
        (uint256 headerLength, uint256 instructionsLength, uint256 signatureLength) = PackedIntent.getLengths(intent);
        require(headerLength == 28 || headerLength == 8, "无效的 header 长度");
        require(instructionsLength >= 36, "指令太短");
        require(signatureLength == 65, "无效的签名长度");
        // 指令结束
        uint256 instructionsEndIndex = 46 + headerLength + instructionsLength;
        require(instructionsLength + signatureLength == intent.length, "无效的意图长度");

        // 验证签名
        uint256 hash = _validateSignatures(sender, intent, instructionsEndIndex);
        require(!this.checkHash(sender, hash), "哈希已执行");

        // header 包含过期时间戳和分配的中继器(可选)
        require(uint256(uint64(bytes8(intent[46 : 54]))) >= block.timestamp, "意图已过期");
        // assignedRelayerAddress = address(intent[54:74]) [可选]

        // header 部分结束 / instruction 部分开始
        uint256 headerEndIndex = 46 + headerLength;
        // 指令的前 20 个字节是 out token 地址
        address outTokenAddress = address(bytes20(intent[headerEndIndex : headerEndIndex + 20]));
        // out token 数量,使用 uint128 来缩短意图
        uint256 outTokenAmount = uint256(uint128(bytes16(intent[headerEndIndex + 20 : headerEndIndex + 36])));
        if (outTokenAddress != address(0)) {
            (bool success, bytes memory data) = outTokenAddress.staticcall(
                abi.encodeWithSelector(IERC20.balanceOf.selector, sender)
            );
            if (!success || data.length != 32) {
                revert("Not ERC20 token");
            }
            require(abi.decode(data, (uint256)) >= outTokenAmount, "代币余额不足");
        } else {
            require(sender.balance >= outTokenAmount, "eth 余额不足");
        }

        // outToken 指令结束
        uint256 numExecutions = uint256(uint8(bytes1(intent[headerEndIndex + 36 : headerEndIndex + 37])));
        // 指令索引
        uint256 instructionIndex = 0;
        // 第一个指令的开始
        uint256 instructionStart;
        uint256 instructionEnd = headerEndIndex + 37;

        while (instructionIndex < numExecutions) {
            instructionStart = instructionEnd;
            require(instructionStart + 2 <= instructionsEndIndex, "意图太短:指令长度");
            // 此执行指令结束
            instructionEnd = instructionStart + 2 + uint256(uint16(bytes2(intent[instructionStart : instructionStart + 2])));
            require(instructionEnd <= instructionsEndIndex, "意图太短:单个指令");

            instructionIndex += 1;
        }
        require(instructionEnd == instructionsEndIndex, "意图长度不匹配");

        return ERC7806Constants.VALIDATION_APPROVED;
    }

    function unpackOperations(bytes calldata intent) external view returns (bytes4 code, bytes[] memory unpackedInstructions) {
        (address sender, address standard) = PackedIntent.getSenderAndStandard(intent);
        require(standard == address(this), "不是这个标准");
        (uint256 headerLength, uint256 instructionsLength, uint256 signatureLength) = PackedIntent.getLengths(intent);
        require(headerLength == 28 || headerLength == 8, "无效的 header 长度");
        require(instructionsLength >= 36, "指令太短");
        require(signatureLength == 65, "无效的签名长度");
        // 指令结束
        uint256 instructionsEndIndex = 46 + headerLength + instructionsLength;
        require(instructionsLength + signatureLength == intent.length, "无效的意图长度");

        // 获取 header 内容(时间戳、中继器地址 [可选])
        require(uint256(uint64(bytes8(intent[46 : 54]))) >= block.timestamp, "意图已过期");
        if (headerLength == 28) {
            // 分配的中继器
            require(tx.origin == address(bytes20(intent[54 : 74])), "无效的中继器");
        }

        uint256 intentHash = _validateSignatures(sender, intent, instructionsEndIndex);
        require(!this.checkHash(sender, intentHash), "哈希已执行");

        // 指令开始
        uint256 headerEndIndex = headerLength + 46;
        // 总指令 = 标记哈希 + 将代币转移到中继器 + 执行
        // 前 36 个字节定义了支付给中继器的金额
        // 接下来的 1 个字节定义了执行指令的数量
        unpackedInstructions = new bytes[](2 + uint8(bytes1(intent[headerEndIndex + 36 : headerEndIndex + 37])));
        // 第一个指令是标记哈希以防止重入攻击
        unpackedInstructions[0] = abi.encode(
            address(this), 0, abi.encodeWithSelector(this.markHash.selector, intentHash));

        // 指令的前 20 个字节是 out token 地址
        address outTokenAddress = address(bytes20(intent[headerEndIndex : headerEndIndex + 20]));
        // 数量
        uint256 outTokenAmount = uint256(uint128(bytes16(intent[headerEndIndex + 20 : headerEndIndex + 36])));
        // out token 指令
        if (outTokenAddress == address(0)) {
            unpackedInstructions[1] = abi.encode(address(tx.origin), outTokenAmount, "");
        } else {
            unpackedInstructions[1] = abi.encode(
                outTokenAddress,
                uint256(0),
                abi.encodeWithSelector(IERC20.transfer.selector, address(tx.origin), outTokenAmount));
        }

        // 指令索引
        uint256 instructionIndex = 2;
        uint256 instructionEndIndex = headerEndIndex + 37;
        uint256 instructionStartIndex;
        while (instructionIndex < unpackedInstructions.length) {
            // 下一个执行指令的开始
            instructionStartIndex = instructionEndIndex;
            require(instructionStartIndex + 2 <= instructionEndIndex, "意图太短:指令长度");
            // 下一个执行指令结束
            instructionEndIndex = instructionStartIndex + 2 + uint256(uint16(bytes2(intent[instructionStartIndex : instructionStartIndex + 2])));
            require(instructionEndIndex <= instructionEndIndex, "意图太短:单个指令");

            unpackedInstructions[instructionIndex] = intent[instructionStartIndex + 2 : instructionEndIndex];

            instructionIndex += 1;
        }
        require(instructionEndIndex == instructionEndIndex, "意图长度不匹配");

        return (ERC7806Constants.VALIDATION_APPROVED, unpackedInstructions);
    }

    function _validateSignatures(
        address sender, bytes calldata intent, uint256 sigStartIndex
    ) internal view returns (uint256) {
        bytes32 intentHash = keccak256(abi.encode(intent[46 : sigStartIndex], address(this), block.chainid));
        bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(intentHash);
        require(sender == messageHash.recover(intent[sigStartIndex : sigStartIndex + 65]), "无效的发送者签名");

        return uint256(intentHash);
    }

    // -------------
    // 以下方法将在测试后删除
    // -------------
    function sampleIntent(
        address sender, address relayer,
        address outTokenAddress, uint128 outAmount,
        bytes[] memory executions
    ) external view returns (
        bytes memory intent, bytes32 intentHash
    ) {
        bytes memory header = relayer == address(0) ?
        abi.encodePacked(uint64((block.timestamp + 31536000) & 0xFFFFFFFFFFFFFFFF)) :
        abi.encodePacked(uint64((block.timestamp + 31536000) & 0xFFFFFFFFFFFFFFFF), relayer);

        bytes memory instructions = bytes.concat(bytes20(outTokenAddress), bytes16(outAmount), bytes1(uint8(executions.length)));
        for (uint256 i = 0; i < executions.length; i++) {
            uint16 length = uint16(executions[i].length);
            instructions = bytes.concat(instructions, bytes2(length), executions[i]);
        }

        bytes memory toSign = bytes.concat(header, instructions);
        intentHash = keccak256(abi.encode(toSign, address(this), block.chainid));

        intent = bytes.concat(bytes20(sender), bytes20(address(this)), bytes2(uint16(header.length)), bytes2(uint16(instructions.length)), bytes2(uint16(65)), toSign);

        return (intent, intentHash);
    }

    function sampleERC20Execution(
        address token, address receiver, uint256 amount
    ) external pure returns (bytes memory) {
        if (token == address(0)) {
            return abi.encode(receiver, amount, "");
        }

        return abi.encode(token, uint256(0), abi.encodeWithSelector(IERC20.transfer.selector, address(receiver), amount));
    }

    function executeUserIntent(bytes calldata intent) external returns (bytes memory) {
        (address sender,) = PackedIntent.getSenderAndStandard(intent);
        bytes memory executeCallData = abi.encodeWithSelector(IAccount.executeUserIntent.selector, intent);

        (, bytes memory result) = sender.call{value : 0, gas : gasleft()}(executeCallData);
        return result;
    }
}

示例账户

以下 IAccount 实现使用 StandardRegistry 来维护标准的允许列表,并且仅批量执行从 IStandard.unpackOperations 返回的所有操作。

```solidity import {MessageHashUtils} from “@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol”; import {ECDSA} from “@openzeppelin/contracts/utils/cryptography/ECDSA.sol”;

/// @title StandardRegistry /// @notice 这是一个标准的注册表,用于确定帐户是否接受标准 /// @dev EIP-712 用于签名验证 contract StandardRegistry { using ECDSA for bytes32;

/// @notice 注册标准时发出的事件
event StandardRegistered(address indexed signer, address indexed standard);
/// @notice 取消注册标准时发出的事件
event StandardUnregistered(address indexed signer, address indexed standard);

/// @notice 此合约的域分隔符
bytes32 public immutable DOMAIN_SEPARATOR;
/// @notice 此合约的签名数据的类型哈希
bytes32 public immutable SIGNED_DATA_TYPEHASH;

/// @notice nonce 的映射
mapping(bytes32 nonce => bool used) private _nonces;
/// @notice 注册的映射
mapping(bytes32 standard => bool registered) private _registrations;

/// @notice 此合约的构造函数
constructor() {
    DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256(bytes("StandardRegistry")), // 合约名称
            keccak256(bytes("2")), // 版本
            block.chainid, // 链 ID
            address(this) // 合约地址
        )
    );

    SIGNED_DATA_TYPEHASH = keccak256(
        "Permission(bool registering,address standard,uint256 nonce)"
    );
}

/// @notice 允许标准的功能,允许中继器为用户注册或取消注册标准
/// @param registering 注册还是取消注册
/// @param signer 许可的签名者
/// @param standard 要许可的标准
/// @param nonce 许可的 nonce
/// @param signature 许可的签名
function permit(bool registering, address signer, address standard, uint256 nonce, bytes calldata signature) external {
    bytes32 compositeKey = keccak256(abi.encodePacked(signer, nonce));
    require(!_nonces[compositeKey], "无效的 nonce");

    // 验证签名
    bytes32 structHash = keccak256(
        abi.encode(SIGNED_DATA_TYPEHASH, registering, standard, nonce)
    );
    bytes32 digest = MessageHashUtils.toTypedDataHash(DOMAIN_SEPARATOR, structHash);
    require(signer == digest.recover(signature), "无效的签名");

    _process(registering, signer, standard, nonce);
}

/// @notice 直接更新标准注册的功能
/// @param registering 注册还是取消注册
/// @param standard 要更新的标准
/// @param nonce 更新的 nonce
function update(bool registering, address standard, uint256 nonce) external {
    address signer = msg.sender;
    bytes32 compositeKey = keccak256(abi.encodePacked(signer, nonce));
    require(!_nonces[compositeKey], "无效的 nonce");

    _process(registering, signer, standard, nonce);
}

/// @notice 检查是否使用了 nonce 的功能
/// @param signer nonce 的签名者
/// @param nonce 要检查的 nonce
/// @return result 如果使用了 nonce,则为 true
function isNonceUsed(address signer, uint256 nonce) external view returns (bool) {
    bytes32 compositeKey = keccak256(abi.encodePacked(signer, nonce));
    return _nonces[compositeKey];
}

/// @notice 检查是否注册了标准的功能
/// @param signer 标准的签名者
/// @param standard 要检查的标准- **可公开审计**:开放访问合约代码允许安全研究人员识别潜在的漏洞。 - **经过充分审查和共享**:公开讨论和同行评审有助于加强安全假设。 - **安全地防范兼容性风险**:确保不同标准和账户实现之间的兼容性可以防止可能导致漏洞的意外交互。

委托合约存储风险

如果 IAccount 实现维护状态(而不是无状态),它可能会:

  • 干扰共享相同存储的其他委托合约。
  • 如果存储未得到适当保护,则可能被未经授权的用户操纵。

强烈建议无状态执行以防止存储冲突。

版权

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

Citation

Please cite this document as:

hellohanchen (@hellohanchen), "ERC-7806: 极简的意图中心 EOA 智能账户 [DRAFT]," Ethereum Improvement Proposals, no. 7806, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7806.