账户抽象:ERC-4337、智能合约钱包与 Paymaster

系统讲解 EOA 与合约账户的本质差异,深入剖析 ERC-4337 EntryPoint 架构与 UserOperation 完整生命周期,涵盖 Account Factory、Paymaster 代付 gas、Bundler 中继角色、社交恢复与 Session Key 等智能合约钱包核心能力。

导语:从助记词到智能合约钱包的用户体验革命

以太坊自诞生以来就有一个根本性的 UX 难题:每个用户必须管理一个由私钥控制的外部拥有账户(EOA),而私钥一旦丢失,资产就永久无法找回。EVM 有两类账户——EOA 和合约账户——但 EOA 承担了几乎所有用户交互,合约账户只能被动响应调用。这造成了双重割裂:用户无法享受合约的可编程性(如社交恢复、多签、gas 代付),而合约失去了作为"主动账户"的完整能力。

ERC-4337 不改变共识层,而是在 mempool 层面引入了一层抽象,让智能合约账户拥有了与 EOA 等价的"发起交易"能力。这就是账户抽象(Account Abstraction)的核心突破。

一句话总结:账户抽象的本质是"让智能合约账户像 EOA 一样发交易"——用户不再需要保管私钥来签名每一笔交易,而是可以把签名逻辑、gas 付费逻辑、权限控制逻辑全部交给智能合约来编程实现。


1. EOA 与合约账户的本质差异

1.1 两种账户的核心区别

┌─────────────────────────────────────────────────────────────┐
│  EOA(外部拥有账户)                                           │
│    - 由私钥(助记词)控制                                      │
│    - 发起交易:必须由私钥签名(ECDSA secp256k1)               │
│    - 不能部署代码                                              │
│    - 每笔交易必须付 gas(ETH)                                 │
│    - 丢失私钥 = 永久丢失资产                                   │
├─────────────────────────────────────────────────────────────┤
│  合约账户(Contract Account)                                   │
│    - 由代码逻辑控制                                            │
│    - 不能主动发起交易,只能被动响应调用                           │
│    - 可以部署任意代码                                          │
│    - gas 由调用者支付                                          │
│    - 可编程:多签、社交恢复、Session Key、gas 代付等             │
└─────────────────────────────────────────────────────────────┘

1.2 以太坊生态的账户现状

EOA 的痛点:
  - 私钥管理:MetaMask 等钱包要求用户自行保管 12/24 词助记词
  - 助记词泄露:钓鱼网站、剪贴板劫持、社工攻击
  - 无恢复机制:没有"忘记密码"选项
  - gas 门槛:新用户必须先买 ETH 才能进行任何操作
  - 体验割裂:跨链、跨 dApp 需要重复签名授权

EIP-2938 / EIP-3074 的尝试:
  - 试图在协议层修改 EVM,让合约账户可以发交易
  - 都需要共识层硬分叉,推进缓慢且存在安全风险

ERC-4337 的突破:
  - 不需要协议层修改
  - 在 mempool 层面引入 "UserOperation" 替代 "Transaction" 的概念
  - 以太坊基金会和 Vitalik 的背书推动快速落地

一句话总结:EOA 是"私钥决定一切"的安全模型,合约账户是"代码决定一切"的可编程模型——ERC-4337 用 UserOperation 层桥接了两者,让合约账户获得了 EOA 的主动性而无需修改共识。


2. ERC-4337 核心架构:EntryPoint、Bundler 与 UserOperation

2.1 整体架构

传统交易流程:                    ERC-4337 流程:

用户(EOA)                       用户(智能合约钱包)
  │                                  │
  │ eth_sendTransaction              │ 创建 UserOperation
  ▼                                  ▼
签名交易(ECDSA)                Bundler(中继/打包者)
  │                                  │
  │ 广播到 P2P 网络                 │ 验证签名、模拟执行
  ▼                                  ▼
矿工/验证者                      EntryPoint 合约(链上入口)
  │                                  │
  │ 验证签名 + nonce + gas          │ validateUserOp(验证)
  ▼                                  │ execute(执行)
EVM 执行                          ▼
                                    智能合约钱包逻辑
                                    │
                                    ▼
                                    Paymaster(可选:代付 gas)

2.2 UserOperation 结构

// ERC-4337 定义的 UserOperation 结构
struct UserOperation {
    address sender;               // 智能合约钱包地址
    uint256 nonce;                // 防重放序列号(在钱包合约中维护)
    bytes initCode;               // 创建钱包的 factory + 初始化参数(首次使用时)
    bytes callData;               // 实际要执行的调用
    uint256 callGasLimit;         // 执行 callData 的 gas 上限
    uint256 verificationGasLimit; // 验证阶段的 gas 上限
    uint256 preVerificationGas;   // bundler 的基础操作 gas
    uint256 maxFeePerGas;         // EIP-1559 最大费用
    uint256 maxPriorityFeePerGas; // EIP-1559 优先费
    bytes paymasterAndData;       // Paymaster 地址 + 签名数据
    bytes signature;              // 用户签名(不一定是 ECDSA)
}

2.3 EntryPoint 合约

EntryPoint 是 ERC-4337 的核心链上合约,所有 UserOperation 最终都通过它进入 EVM:

// 简化的 EntryPoint 入口逻辑
interface IEntryPoint {
    function handleOps(
        UserOperation[] calldata ops,
        address payable beneficiary  // bundler 获得 gas 退款的目标地址
    ) external;
}

// handleOps 的核心流程:
// for each op in ops:
//   1. 如果是首次使用(initCode != ""):创建钱包合约
//   2. 调用 sender.validateUserOp(op, userOpHash, missingAccountFunds)
//      → 钱包验证 signature 是否正确
//      → 钱包验证 nonce 是否合法
//      → 钱包支付预验证 gas(若为 paymaster 则 paymaster 支付)
//   3. 调用 sender.execute(callData) 执行用户操作
//   4. 若使用了 paymaster:调用 paymaster.validatePaymasterUserOp + postOp

一句话总结:EntryPoint 是 ERC-4337 的"中央路由器"——它负责验证所有 UserOperation 的签名与 nonce、协调 wallet 的执行和 paymaster 的付费,让智能合约钱包获得了在 mempool 中与 EOA 同等的"发言权"。


3. UserOperation 完整生命周期

3.1 打包阶段:Bundler 的角色

Bundler 是 ERC-4337 生态中的关键新角色。它承担着传统 mempool 中矿工/验证者的部分职能:

Bundler 的工作流程:

1. 从 UserOperation mempool(alt-mempool)接收 UserOperation
2. 本地模拟验证(static call validateUserOp)
   - 检查签名是否有效
   - 检查 nonce 是否未使用
   - 检查 wallet 是否有足够余额支付 gas
3. gas 估算与排序:按 maxPriorityFeePerGas 排序
4. 打包成 handleOps 交易
   - Bundler 用自己的 EOA 签名并发送到 EntryPoint
   - 多个 UserOperation 批量打包(节省 L1 调用开销)
5. 从 EntryPoint 获得 gas 退款

3.2 签名验证与执行

// 智能合约钱包中的 validateUserOp 示例
contract SimpleAccount is BaseAccount {
    address public owner;
    IEntryPoint private immutable _entryPoint;

    constructor(IEntryPoint anEntryPoint, address anOwner) {
        _entryPoint = anEntryPoint;
        owner = anOwner;
    }

    function entryPoint() public view override returns (IEntryPoint) {
        return _entryPoint;
    }

    // EntryPoint 在验证阶段调用此函数
    function _validateSignature(
        UserOperation calldata userOp,
        bytes32 userOpHash
    ) internal override virtual returns (uint256 validationData) {
        bytes32 hash = userOpHash.toEthSignedMessageHash();
        if (owner != hash.recover(userOp.signature))
            return SIG_VALIDATION_FAILED;
        return 0;
    }

    // 执行用户操作:这里可以是任意调用
    function execute(address dest, uint256 value, bytes calldata func)
        external
    {
        _requireFromEntryPoint();
        (bool success, bytes memory result) = dest.call{value: value}(func);
        if (!success) {
            assembly { revert(add(result, 32), mload(result)) }
        }
    }

    // 仅允许 EntryPoint 调用
    function _requireFromEntryPoint() internal view {
        require(msg.sender == address(entryPoint()), "Not from EntryPoint");
    }
}

3.3 完整生命周期时序

UserOperation 全链路:

用户前端(如 Rainbow / Particle Network)
  │
  │ 1. 构建 UserOperation(包含 sender、callData、signature 等)
  ▼
Bundler RPC(如 Alchemy ERC-4337 Bundler / Pimlico / Biconomy)
  │
  │ 2. 模拟验证(eth_estimateUserOperationGas)
  │ 3. 调整 gas limit
  │ 4. 发送到 alt-mempool
  ▼
Bundler 节点
  │
  │ 5. 排序多个 UserOperation
  │ 6. 用 EOA 调用 EntryPoint.handleOps(ops, beneficiaryAddress)
  ▼
L1/L2 区块
  │
  │ 7. EntryPoint 逐个验证 + 执行
  │ 8. EntryPoint 支付 gas 给 beneficiaryAddress(Bundler)
  ▼
智能合约钱包
  │
  │ 9. validateUserOp → _validateSignature
  │ 10. execute → 调用目标合约
  ▼
dApp 合约执行

一句话总结:UserOperation 的生命周期是"用户签名 → Bundler 模拟验证 → EntryPoint 链上验证执行"三步走——Bundler 充当了 EOA 和智能合约账户之间的翻译官。


4. Account Factory:钱包即代码

4.1 为什么需要 Factory

ERC-4337 允许 UserOperation 的 initCode 字段包含创建新钱包合约的指令。这意味着用户在第一次使用某个 dApp 时,可以不预先部署钱包——Bundler 会在第一次 UserOperation 中自动创建它。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import "@openzeppelin/contracts/utils/Create2.sol";
import "./SimpleAccount.sol";

// Account Factory:使用 CREATE2 确定性创建用户钱包
contract SimpleAccountFactory {
    IEntryPoint public immutable entryPoint;

    constructor(IEntryPoint _entryPoint) {
        entryPoint = _entryPoint;
    }

    // 创建新钱包(首次使用时由 Bundler 调用)
    function createAccount(address owner, uint256 salt)
        public
        returns (SimpleAccount ret)
    {
        address addr = getAddress(owner, salt);
        uint256 codeSize = addr.code.length;
        if (codeSize > 0) {
            return SimpleAccount(payable(addr)); // 已存在
        }
        ret = new SimpleAccount{salt: bytes32(salt)}(entryPoint, owner);
    }

    // 预先计算钱包地址(CREATE2 确定性)
    function getAddress(address owner, uint256 salt)
        public
        view
        returns (address)
    {
        return Create2.computeAddress(
            bytes32(salt),
            keccak256(abi.encodePacked(
                type(SimpleAccount).creationCode,
                abi.encode(entryPoint, owner)
            ))
        );
    }
}

4.2 initCode 执行流程

首次使用钱包时:
  UserOperation.initCode = abi.encodePacked(
      factoryAddress,
      factory.createAccount.selector,
      abi.encode(owner, salt)
  )

EntryPoint 处理:
  if initCode.length > 0:
      - 调用 initCode(创建钱包合约)
      - 要求 sender == 创建出的合约地址(防止伪造)
  然后正常执行 validateUserOp + execute

一句话总结:Account Factory 用 CREATE2 让钱包地址在用户创建之前就确定——用户可以在没有 ETH 的情况下"预注册"地址,第一次交互时由 Bundler 代为部署,彻底消除了新用户的 gas 启动门槛。


5. Paymaster:代付 gas 的无限可能

5.1 Paymaster 的核心机制

Paymaster 是 ERC-4337 中最令人兴奋的组件之一。它允许第三方(dApp 赞助商、稳定币发行方、或其他账户)为用户支付 gas 费用。

Paymaster 的两种模式:

模式一:Gasless(赞助商代付)
  Paymaster 由 dApp 运营
  用户发起交易 → Paymaster 支付 ETH gas
  dApp 通过其他方式向用户收费(如订阅、交易手续费)

模式二:ERC-20 Token 付费
  用户用 USDC/USDT 等 ERC-20 token 支付 gas
  Paymaster 接收 token,代为支付 ETH gas
  Paymaster 通过 DEX 将 token 兑换为 ETH 补充押金

5.2 Paymaster 合约实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@account-abstraction/contracts/interfaces/IPaymaster.sol";
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenPaymaster is IPaymaster {
    IEntryPoint public entryPoint;
    IERC20 public token;
    address public owner;
    uint256 public tokenToEthRate; // 动态更新

    constructor(IEntryPoint _entryPoint, IERC20 _token) {
        entryPoint = _entryPoint;
        token = _token;
        owner = msg.sender;
    }

    // EntryPoint 在验证阶段调用
    function validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external override returns (bytes memory context, uint256 validationData) {
        require(msg.sender == address(entryPoint), "Not EntryPoint");

        // 从 paymasterAndData 解析用户签名的 token 授权
        // 格式: address(paymaster) + tokenAmount + signature
        (uint256 tokenAmount, bytes memory signature) =
            abi.decode(userOp.paymasterAndData[20:], (uint256, bytes));

        // 验证用户确实允许 Paymaster 扣除这些 token
        require(
            verifyTokenApproval(userOp.sender, tokenAmount, signature),
            "Invalid approval"
        );

        // 计算需要的 token 数量
        uint256 requiredTokens = (maxCost * tokenToEthRate) / 1e18;
        require(tokenAmount >= requiredTokens, "Insufficient token amount");

        // 返回上下文,供 postOp 使用
        context = abi.encode(userOp.sender, tokenAmount);
        return (context, 0);
    }

    // EntryPoint 在执行后调用
    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost
    ) external override {
        require(msg.sender == address(entryPoint), "Not EntryPoint");

        (address sender, uint256 maxTokenAmount) = abi.decode(context, (address, uint256));

        uint256 actualTokenCost = (actualGasCost * tokenToEthRate) / 1e18;
        uint256 refund = maxTokenAmount - actualTokenCost;

        // 扣除用户 token
        token.transferFrom(sender, address(this), actualTokenCost);

        // 如果有余额,退还给用户
        if (refund > 0) {
            token.transfer(sender, refund);
        }

        // 如果 mode == opReverted,可以选择不收费或部分收费
    }

    function verifyTokenApproval(
        address sender,
        uint256 amount,
        bytes memory signature
    ) internal pure returns (bool) {
        // 验证签名逻辑...
        return true;
    }

    function updateRate(uint256 newRate) external {
        require(msg.sender == owner, "Not owner");
        tokenToEthRate = newRate;
    }

    // 存入 ETH 到 EntryPoint 作为质押
    function deposit() external payable {
        entryPoint.depositTo{value: msg.value}(address(this));
    }
}

5.3 Paymaster 的安全考量

EntryPoint 对 Paymaster 的保护机制:

1. 质押要求:
   Paymaster 必须向 EntryPoint 存入 ETH 质押
   validatePaymasterUserOp 失败会扣除质押作为惩罚

2. postOp 保证执行:
   即使用户操作 revert,EntryPoint 也保证调用 postOp
   这让 Paymaster 有机会收回 token(或至少记账)

3. gas limit 分离:
   validation 阶段和执行阶段有独立的 gas limit
   防止恶意 Paymaster 消耗无限 gas

一句话总结:Paymaster 是 ERC-4337 的"商业模式创新层"——它让用户可以用稳定币付 gas、让 dApp 可以补贴用户 gas、让社交网络可以代付注册费——所有这些都无需改动以太坊协议,只需写一套 Paymaster 合约。


6. 社交恢复与 Session Key

6.1 社交恢复机制

传统钱包丢失私钥即丢失资产。智能合约钱包可以实现社交恢复:

// 社交恢复钱包的核心逻辑
contract SocialRecoveryWallet is SimpleAccount {
    address[] public guardians;           // 监护人列表
    mapping(address => bool) public isGuardian;
    uint256 public threshold;             // 恢复所需监护人数量

    // 当前恢复请求
    address public pendingNewOwner;
    uint256 public recoveryRound;
    mapping(uint256 => mapping(address => bool)) public hasGuardianConfirmed;

    modifier onlyGuardian() {
        require(isGuardian[msg.sender], "Not guardian");
        _;
    }

    // 监护人发起恢复
    function initiateRecovery(address newOwner) external onlyGuardian {
        require(newOwner != address(0), "Invalid owner");
        recoveryRound++;
        pendingNewOwner = newOwner;
        hasGuardianConfirmed[recoveryRound][msg.sender] = true;
    }

    // 监护人确认恢复
    function confirmRecovery() external onlyGuardian {
        require(pendingNewOwner != address(0), "No pending recovery");
        require(!hasGuardianConfirmed[recoveryRound][msg.sender], "Already confirmed");
        hasGuardianConfirmed[recoveryRound][msg.sender] = true;

        // 执行恢复
        if (_countConfirmations() >= threshold) {
            owner = pendingNewOwner;
            pendingNewOwner = address(0);
        }
    }

    // 取消恢复请求(由原 owner 调用)
    function cancelRecovery() external {
        require(msg.sender == owner || msg.sender == address(this), "Not authorized");
        pendingNewOwner = address(0);
    }

    function _countConfirmations() internal view returns (uint256 count) {
        for (uint256 i = 0; i < guardians.length; i++) {
            if (hasGuardianConfirmed[recoveryRound][guardians[i]]) {
                count++;
            }
        }
    }
}

6.2 Session Key:降低交互签名频次

每次 dApp 交互都需要用户钱包签名,这对于高频交互场景(如游戏、DeFi 交易)是灾难性的。Session Key 提供了一种临时授权机制:

// Session Key 管理合约
contract SessionKeyManager {
    struct Session {
        address sessionKey;     // 临时密钥地址
        uint256 expiresAt;      // 过期时间
        address[] allowedTargets; // 允许调用的合约白名单
        uint256 maxSpend;       // 最大消费限额
        uint256 spent;          // 已消费金额
        bool active;
    }

    mapping(address => Session) public sessions;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    // 用户签名授权创建 Session
    function createSession(
        address sessionKey,
        uint256 duration,
        address[] calldata allowedTargets,
        uint256 maxSpend,
        bytes calldata ownerSignature
    ) external {
        require(verifyOwnerSignature(
            keccak256(abi.encodePacked(sessionKey, duration, maxSpend)),
            ownerSignature
        ), "Invalid signature");

        sessions[sessionKey] = Session({
            sessionKey: sessionKey,
            expiresAt: block.timestamp + duration,
            allowedTargets: allowedTargets,
            maxSpend: maxSpend,
            spent: 0,
            active: true
        });
    }

    // dApp 使用 Session Key 代替用户签名发起交易
    function executeWithSession(
        address sessionKey,
        address target,
        bytes calldata callData,
        uint256 value
    ) external {
        Session storage session = sessions[sessionKey];
        require(session.active, "Session inactive");
        require(block.timestamp < session.expiresAt, "Session expired");
        require(_isAllowedTarget(session, target), "Target not allowed");
        require(session.spent + value <= session.maxSpend, "Spend limit exceeded");

        session.spent += value;
        (bool success, ) = target.call{value: value}(callData);
        require(success, "Call failed");
    }

    function revokeSession(address sessionKey) external onlyOwner {
        sessions[sessionKey].active = false;
    }

    function _isAllowedTarget(Session memory session, address target)
        internal
        pure
        returns (bool)
    {
        for (uint256 i = 0; i < session.allowedTargets.length; i++) {
            if (session.allowedTargets[i] == target) return true;
        }
        return false;
    }

    function verifyOwnerSignature(bytes32 hash, bytes memory sig)
        internal
        view
        returns (bool)
    {
        // ECDSA 验证逻辑...
        return true;
    }
}

一句话总结:社交恢复把"丢了钥匙就丢所有"变成了"丢失主密钥后,由可信赖的朋友投票恢复";Session Key 把"每次操作都弹窗签名"变成了"一次性授权,多次免密执行"——两者都是智能合约钱包超越传统 EOA 的核心卖点。


7. EntryPoint 安全与审计要点

7.1 EntryPoint 的安全模型

EntryPoint 的设计目标:"即使 wallet 和 paymaster 都有漏洞,用户的资金也不应被直接盗取"

安全保障:
  1. validate 和 execute 分离:
     validate 不能修改状态(或修改必须回滚)
     只有 execute 能执行用户指定的操作

  2. 模拟调用(simulation):
     Bundler 先本地模拟 validateUserOp
     如果模拟失败,Bundler 直接拒绝,不消耗链上 gas

  3. gas 限制严格分离:
     verificationGasLimit 不能超过 30 分钟区块限制
     callGasLimit 独立计算

  4. 重入保护:
     EntryPoint 使用 nonce 机制和重入锁防止重复执行

7.2 智能合约钱包的安全审计清单

审计项检查内容
签名验证是否正确验证 userOpHash 的签名格式(需加 Ethereum Signed Message 前缀)
nonce 管理nonce 是否严格递增,防止重放攻击
调用授权execute 是否仅允许 EntryPoint 调用
gas 限制validateUserOp 是否遵守 gas 上限
社交恢复监护人添加/移除是否有 owner 授权;恢复完成后是否正确重置状态
Session Key过期时间、消费限额、目标白名单是否正确校验
PaymasterpostOp 是否在所有情况下(包括 revert)都能正确执行

一句话总结:EntryPoint 的设计哲学是"不信任钱包、不信任 bundler"——通过 validate/execute 分离、模拟验证和严格的 gas 限制,把安全边界拉到了合约级别,让即使恶意的 Bundler 也无法盗取用户资金。


8. 总结

  1. 账户抽象的本质:让智能合约账户获得 EOA 的主动发交易能力
  2. ERC-4337 架构:UserOperation → Bundler → EntryPoint → Wallet + Paymaster
  3. Account Factory:CREATE2 确定性地址 + initCode 自动部署,消除新用户门槛
  4. Paymaster:第三方代付 gas,商业模式创新空间巨大
  5. 社交恢复:链上可编程的"忘记密码"机制
  6. Session Key:一次性授权、多次免密执行,消除高频交互的签名疲劳
  7. 安全核心:EntryPoint 的 validate/execute 分离 + 模拟验证 + gas 限制

相关阅读


延伸阅读


以下是一个完整的 ERC-4337 智能合约钱包 + Paymaster + Account Factory 的 Solidity 实现,可直接在测试网部署体验账户抽象的全部流程。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@account-abstraction/contracts/interfaces/IAccount.sol";
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import "@account-abstraction/contracts/interfaces/IPaymaster.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/Create2.sol";

// ═══════════════════════════════════════════════════════════════
// 1. SimpleAccount:基础智能合约钱包
// ═══════════════════════════════════════════════════════════════
contract SimpleAccount is IAccount {
    using ECDSA for bytes32;

    IEntryPoint public immutable entryPoint;
    address public owner;
    uint256 public nonce;

    modifier onlyEntryPoint() {
        require(msg.sender == address(entryPoint), "Not EntryPoint");
        _;
    }

    constructor(IEntryPoint _entryPoint, address _owner) {
        entryPoint = _entryPoint;
        owner = _owner;
    }

    // EntryPoint 调用的验证入口
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external override onlyEntryPoint returns (uint256 validationData) {
        // nonce 检查
        require(userOp.nonce == nonce, "Invalid nonce");
        nonce++;

        // ECDSA 签名验证
        bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", userOpHash));
        require(hash.recover(userOp.signature) == owner, "Invalid signature");

        // 支付预验证 gas
        if (missingAccountFunds > 0) {
            (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
            (success); // 忽略失败,EntryPoint 会处理
        }

        return 0; // SIG_VALIDATION_SUCCESS
    }

    // 执行任意调用(仅 EntryPoint 可调用)
    function execute(address dest, uint256 value, bytes calldata func) external onlyEntryPoint {
        (bool success, bytes memory result) = dest.call{value: value}(func);
        if (!success) {
            assembly { revert(add(result, 32), mload(result)) }
        }
    }

    // 批量执行
    function executeBatch(
        address[] calldata dests,
        uint256[] calldata values,
        bytes[] calldata funcs
    ) external onlyEntryPoint {
        require(dests.length == values.length && values.length == funcs.length, "Length mismatch");
        for (uint256 i = 0; i < dests.length; i++) {
            (bool success, bytes memory result) = dests[i].call{value: values[i]}(funcs[i]);
            if (!success) {
                assembly { revert(add(result, 32), mload(result)) }
            }
        }
    }

    receive() external payable {}
}

// ═══════════════════════════════════════════════════════════════
// 2. SimpleAccountFactory:确定性创建钱包
// ═══════════════════════════════════════════════════════════════
contract SimpleAccountFactory {
    IEntryPoint public immutable entryPoint;

    constructor(IEntryPoint _entryPoint) {
        entryPoint = _entryPoint;
    }

    function createAccount(address owner, uint256 salt) external returns (SimpleAccount) {
        address addr = getAddress(owner, salt);
        if (addr.code.length > 0) {
            return SimpleAccount(payable(addr));
        }
        return new SimpleAccount{salt: bytes32(salt)}(entryPoint, owner);
    }

    function getAddress(address owner, uint256 salt) public view returns (address) {
        return Create2.computeAddress(
            bytes32(salt),
            keccak256(abi.encodePacked(type(SimpleAccount).creationCode, abi.encode(entryPoint, owner)))
        );
    }
}

// ═══════════════════════════════════════════════════════════════
// 3. VerifyingPaymaster: sponsors 代付 gas
// ═══════════════════════════════════════════════════════════════
contract VerifyingPaymaster is IPaymaster {
    IEntryPoint public immutable entryPoint;
    address public verifyingSigner;  // 由 paymaster 运营方控制
    mapping(address => uint256) public senderNonce;

    constructor(IEntryPoint _entryPoint, address _verifyingSigner) {
        entryPoint = _entryPoint;
        verifyingSigner = _verifyingSigner;
    }

    function validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 /* userOpHash */,
        uint256 /* maxCost */
    ) external override returns (bytes memory context, uint256 validationData) {
        require(msg.sender == address(entryPoint), "Not EntryPoint");

        // paymasterAndData = address(paymaster) + validUntil + signature
        (uint48 validUntil, bytes memory signature) = abi.decode(
            userOp.paymasterAndData[20:],
            (uint48, bytes)
        );

        require(block.timestamp <= validUntil, "Expired");

        // 构造被签名的 hash
        bytes32 hash = keccak256(abi.encodePacked(
            userOp.sender,
            userOp.nonce,
            validUntil,
            block.chainid
        ));

        require(ECDSA.recover(hash, signature) == verifyingSigner, "Invalid paymaster sig");

        return (abi.encode(userOp.sender), 0);
    }

    function postOp(
        PostOpMode /* mode */,
        bytes calldata context,
        uint256 /* actualGasCost */
    ) external override {
        require(msg.sender == address(entryPoint), "Not EntryPoint");
        (address sender) = abi.decode(context, (address));
        senderNonce[sender]++;
    }

    function deposit() external payable {
        entryPoint.depositTo{value: msg.value}(address(this));
    }

    function withdrawTo(address payable to, uint256 amount) external {
        entryPoint.withdrawTo(to, amount);
    }
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. DePIN 去中心化物理基础设施网络
  2. DeFi 衍生品:期权、永续合约与合成资产
  3. 智能合约形式化验证:Certora 与 K 框架