ERC-4337 落地场景全盘点:游戏、DAO、DeFi 的下一个爆发点

  • 木西
  • 发布于 10小时前
  • 阅读 30

前言本文围绕ERC-4337标准展开全面梳理,先系统解析其核心内容,涵盖概念定义、核心价值、组件构成、工作流程、特性优势及当前面临的挑战与发展现状,构建起清晰的理论认知框架;再聚焦实践落地,基于OpenZeppelin与account-abstraction工具,完整实现ERC-4337钱包开发

前言

本文围绕ERC-4337标准展开全面梳理,先系统解析其核心内容,涵盖概念定义、核心价值、组件构成、工作流程、特性优势及当前面临的挑战与发展现状,构建起清晰的理论认知框架;再聚焦实践落地,基于OpenZeppelin与account-abstraction工具,完整实现ERC-4337钱包开发,并详细拆解从开发搭建、测试验证到部署上线的全流程,形成“理论梳理+实践落地”的完整内容体系,为ERC-4337标准的学习与应用提供兼具系统性与可操作性的参考。

概述

ERC4337(Account Abstraction)是以太坊的账户抽象标准,旨在消除外部账户(EOA)与合约账户的界限。用户可通过智能合约自定义账户逻辑,实现社交恢复、代付Gas、批量交易等高级功能,而无需修改以太坊共识层;

核心价值:保留EOA的简单性,同时赋予合约账户的灵活性,显著提升Web3用户体验。

核心组件架构

1. UserOperation(用户操作)

  • 定义:伪交易对象,代表用户意图,包含目标调用、验证元数据、Gas参数等信息
  • 特点:不直接发送到链上,而是提交至独立的 alt-mempool(替代内存池)

2. 智能合约账户(Smart Contract Account)

  • 角色:用户控制的代理钱包,作为身份主体
  • 功能:通过 validateUserOp() 验证签名,通过 executeUserOp() 执行交易
  • 优势:可编程化,支持自定义规则(如多重签名、限额控制)

3. Bundler(打包器)

  • 作用:链下服务,监听 alt-mempool 中的 UserOperations
  • 流程:批量打包多个操作,通过一个交易提交至 EntryPoint 合约
  • 经济性:由Bundler预付Gas,后续从EntryPoint获得补偿

4. EntryPoint(入口合约)

  • 地位:ERC4337的核心链上网关

  • 职责

    • 验证每个UserOperation的有效性
    • 路由至对应智能合约钱包执行
    • 计算总Gas消耗并补偿Bundler
  • 关键:所有Gas支付通过此合约完成(从用户存款或Paymaster)

5. Paymaster(支付主)

  • 定位:可选的智能合约,提供灵活Gas支付方案

  • 两种模式

    • 赞助模式:项目方/第三方直接代付Gas
    • 代币模式:允许用户用ERC-20代币(如USDT)而非ETH支付Gas
  • 接口validatePaymasterUserOp() 验证资格,postOp() 处理后续结算

    标准工作流程

    详细步骤

  1. 签名生成:用户使用私钥对操作签名(兼容ERC-191/ERC-712)
  2. 内存池广播:UserOperation进入链下alt-mempool
  3. Bundler聚合:Bundler收集多个操作,创建批量交易
  4. EntryPoint处理:验证签名、检查Nonce、执行调用、计算Gas
  5. 费用结算:从用户账户存款或Paymaster扣除Gas费,补偿Bundler

    关键特性与优势

    特性 说明 价值
    社交恢复 通过守护人机制重置私钥 解决私钥丢失问题
    无Gas交易 Paymaster代付或ERC-20支付 降低新用户门槛
    批量操作 单次签名执行多笔调用 提升操作效率
    可编程权限 自定义验证逻辑(如多签、限额) 增强安全性与灵活性
    确定性地址 代理钱包地址跨网络一致 类似EOA的用户体验

    当前挑战与现状

    • 采用进展:核心合约已就绪,多个团队正推出生产级原生钱包
    • 架构局限:虽改善用户体验,但仍依赖链下Bundler与独立内存池,面临去中心化与普及挑战
    • 意图层(Intent-centric)融合:ERC4337为意图驱动架构提供基础,但纯意图模式仍需深度整合Paymaster与跨链设计

      一句总结

      ERC4337通过链下打包+链上验证的架构,在不修改以太坊协议的前提下实现账户抽象,是智能合约钱包普及的关键基础设施。

      智能合约开发、测试、部署

      智能合约

      1.治理代币合约

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

import {BaseAccount} from "@account-abstraction/contracts/core/BaseAccount.sol"; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/**

  • @title MySmartAccount
  • @dev ✅ 基于 @account-abstraction/contracts 0.7.0 的标准实现 */ contract MySmartAccount is BaseAccount, Ownable, Initializable, UUPSUpgradeable, IERC1271 { using MessageHashUtils for bytes32;

    bytes4 private constant EIP1271_MAGIC_VALUE = 0x1626ba7e;

    // ✅ 存储 EntryPoint 地址(避免与函数名冲突) IEntryPoint private immutable _ACCOUNT_ENTRY_POINT;

    // 事件 event TransactionExecuted(address indexed target, uint256 value, bytes data); event BatchExecuted(address[] targets, uint256[] values, bytes[] datas);

    /**

    • @dev ✅ 修正:构造函数参数名加下划线,避免与函数冲突
    • @param entryPoint_ ERC-4337 EntryPoint 地址
    • @param initialOwner 初始所有者地址 */ constructor( IEntryPoint entryPoint_, address initialOwner ) BaseAccount() // ✅ BaseAccount 构造函数无参数 Ownable(initialOwner) // ✅ OpenZeppelin 5.x Ownable 需要 initialOwner { _ACCOUNT_ENTRYPOINT = entryPoint; // ✅ 存储到自定义变量 _disableInitializers(); }

    /**

    • @dev ✅ 覆盖 entryPoint() 函数,返回存储的 EntryPoint */ function entryPoint() public view virtual override returns (IEntryPoint) { return _ACCOUNT_ENTRY_POINT; }

    /**

    • @dev 初始化函数,会覆盖 Ownable 的初始所有者 */ function initialize(address initialOwner) public virtual initializer { _transferOwnership(initialOwner); }

    /**

    • @dev ✅ 实现 BaseAccount 的抽象函数:_validateSignature(内部函数)
    • @notice 在 v0.7.0 中,BaseAccount 同时要求 _validateSignature 和 validateUserOp */ function _validateSignature( PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual override returns (uint256 validationData) { // 检查签名长度 if (userOp.signature.length != 65) { return 1; // SIG_VALIDATION_FAILED }

      // ✅ 直接传入完整的 signature bytes,ECDSA.recover 会自动解析 // 注意:userOp.signature 是 bytes calldata,可以直接传给 recover bytes32 ethHash = userOpHash.toEthSignedMessageHash(); address signer = ECDSA.recover(ethHash, userOp.signature);

      // 验证签名者是否为所有者 if (signer != owner()) { return 1; }

      return 0; // 验证成功 }

    /**

    • @dev ✅ 实现 BaseAccount 的 validateUserOp 外部函数
    • @notice v0.7.0 要求实现此函数,处理 missingAccountFunds */ function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external virtual override returns (uint256 validationData) { // 验证调用者是 EntryPoint require(msg.sender == address(entryPoint()), "account: not from EntryPoint");

      // 调用内部签名验证 validationData = _validateSignature(userOp, userOpHash);

      // 支付 missingAccountFunds 给 EntryPoint if (missingAccountFunds > 0) { (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); (success); // 忽略失败(这是 EntryPoint 的责任) } }

    /**

    • @dev 辅助函数:要求调用者是 EntryPoint 或所有者 */ function _requireFromEntryPointOrOwner() internal view { require( msg.sender == address(entryPoint()) || msg.sender == owner(), "account: not Owner or EntryPoint" ); }

    /**

    • @dev 执行单笔交易 */ function execute( address target, uint256 value, bytes calldata data ) external payable virtual { _requireFromEntryPointOrOwner(); _call(target, value, data); emit TransactionExecuted(target, value, data); }

    /**

    • @dev 批量执行多笔交易 */ function executeBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas ) external payable virtual { _requireFromEntryPointOrOwner(); require(targets.length == datas.length && targets.length == values.length, "Array length mismatch");

      for (uint256 i = 0; i < targets.length; i++) { _call(targets[i], values[i], datas[i]); }

      emit BatchExecuted(targets, values, datas); }

    /**

    • @dev 内部调用函数,处理执行结果 */ function _call(address target, uint256 value, bytes memory data) internal { (bool success, bytes memory result) = target.call{value: value}(data); if (!success) { assembly { revert(add(result, 32), mload(result)) } } }

    /**

    • @dev ✅ ERC-1271 签名验证实现
    • @notice 对于 memory bytes,不能切片,只能检查长度 */ function isValidSignature( bytes32 hash, bytes memory signature ) public view virtual override returns (bytes4 magicValue) { // ✅ 检查 signature 长度是否符合 65 字节的 EIP-2098 标准 if (signature.length != 65) { return 0xffffffff; }

      // ✅ 直接传入完整的 signature,ECDSA.recover 会内部解析 bytes32 ethHash = hash.toEthSignedMessageHash(); address signer = ECDSA.recover(ethHash, signature);

      if (signer == owner()) { return EIP1271_MAGIC_VALUE; }

      return 0xffffffff; }

    /**

    • @dev UUPS 升级授权 */ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    /**

    • @dev 接收 ETH */ receive() external payable {} }
      #### 2.治理代币合约

      // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // ✅ 添加这一行

import {MySmartAccount} from "./MySmartAccount.sol"; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

contract MySmartAccountFactory { IEntryPoint private immutable _entryPoint;

event AccountCreated(address indexed account, address indexed owner);

constructor(IEntryPoint entryPoint) {
    _entryPoint = entryPoint;
}

function createAccount(bytes32 salt, address owner) external returns (MySmartAccount account) {
    address predictedAddress = getAddress(salt, owner);

    if (predictedAddress.code.length > 0) {
        return MySmartAccount(payable(predictedAddress));
    }

    account = new MySmartAccount{salt: salt}(_entryPoint, owner);
    emit AccountCreated(address(account), owner);
}

function getAddress(bytes32 salt, address owner) public view returns (address) {
    bytes32 bytecodeHash = keccak256(
        abi.encodePacked(
            type(MySmartAccount).creationCode,
            abi.encode(_entryPoint, owner)  // ✅ 匹配构造函数参数
        )
    );

    return Create2.computeAddress(salt, bytecodeHash, address(this));
}

}

#### 3.治理代币合约

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

import {BasePaymaster} from "@account-abstraction/contracts/core/BasePaymaster.sol"; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract MyPaymaster is BasePaymaster { using MessageHashUtils for bytes32;

mapping(address => bool) public whitelistedApps;
mapping(address => uint256) public sponsoredTransactionCount;
uint256 public maxSponsoredTransactionsPerApp = 100;

event AppWhitelisted(address indexed app);
event AppRemoved(address indexed app);

constructor(IEntryPoint entryPoint) BasePaymaster(entryPoint) {}

function _validatePaymasterUserOp(
    PackedUserOperation calldata userOp,
    bytes32 userOpHash,
     uint256 /*maxCost*/
) internal override returns (bytes memory context, uint256 validationData) {
    address targetApp = address(bytes20(userOp.callData[16:36]));
    require(whitelistedApps[targetApp], "App not whitelisted");

    require(
        sponsoredTransactionCount[targetApp] &lt; maxSponsoredTransactionsPerApp,
        "Sponsored transaction limit reached"
    );

    if (userOp.paymasterAndData.length > 20) {
        bytes memory signature = userOp.paymasterAndData[20:];
        bytes32 hash = userOpHash.toEthSignedMessageHash();
        address signer = ECDSA.recover(hash, signature);
        require(signer == owner(), "Invalid paymaster signature");
    }

    sponsoredTransactionCount[targetApp]++;
    return ("", 0);
}

// ✅ 修正:移除 _postOp 覆盖,使用父类默认实现
// 如果确实需要后处理,确保正确导入类型

function whitelistApp(address app) external onlyOwner {
    whitelistedApps[app] = true;
    emit AppWhitelisted(app);
}

function removeApp(address app) external onlyOwner {
    whitelistedApps[app] = false;
    emit AppRemoved(app);
}

function resetSponsoredCount(address app) external onlyOwner {
    sponsoredTransactionCount[app] = 0;
}

function setMaxSponsoredTransactions(uint256 maxCount) external onlyOwner {
    maxSponsoredTransactionsPerApp = maxCount;
}

}

#### 编译指令

npx hardhat compile

#### **智能合约部署**

// scripts/deploy.ts import { network, artifacts } from "hardhat"; import {parseEther} from "viem" import EntryPointArtifact from "@account-abstraction/contracts/artifacts/EntryPoint.json"; async function main() { // 连接网络 const { viem } = await network.connect({ network: network.name });//指定网络进行链接 // 获取客户端 const [deployer] = await viem.getWalletClients(); const publicClient = await viem.getPublicClient();

const entryPointHash = await deployer.deployContract({ abi: EntryPointArtifact.abi, bytecode: EntryPointArtifact.bytecode, args: [], // EntryPoint 构造函数不需要参数 });

// 等待确认并获取合约地址 const entryPointReceipt = await publicClient.waitForTransactionReceipt({ hash: entryPointHash });

const entryPointAddress = entryPointReceipt.contractAddress; console.log("✅ EntryPoint 合约地址:", entryPointAddress); // 部署智能合约MySmartAccount const MySmartAccountRegistry = await artifacts.readArtifact("MySmartAccount"); // 部署(构造函数参数:recipient, initialOwner) const MySmartAccountRegistryHash = await deployer.deployContract({ abi: MySmartAccountRegistry.abi,//获取abi bytecode: MySmartAccountRegistry.bytecode,//硬编码 args: [entryPointAddress,deployer.account.address],//process.env.RECIPIENT, process.env.OWNER }); // 等待确认并打印合约地址 const MySmartAccountReceipt = await publicClient.getTransactionReceipt({ hash: MySmartAccountRegistryHash }); console.log("MySmartAccount合约地址:", MySmartAccountReceipt.contractAddress); // 部署工厂合约 const MySmartAccountFactory = await artifacts.readArtifact("MySmartAccountFactory");

const MySmartAccountFactoryHash = await deployer.deployContract({ abi: MySmartAccountFactory.abi,//获取abi bytecode: MySmartAccountFactory.bytecode,//硬编码 args: [entryPointAddress],//process.env.RECIPIENT, process.env.OWNER }); // 等待确认并打印合约地址 const MySmartAccountFactoryReceipt = await publicClient.getTransactionReceipt({ hash: MySmartAccountFactoryHash }); console.log("MySmartAccountFactory合约地址:", await MySmartAccountFactoryReceipt.contractAddress);

// 部署支付合约MyPaymaster const MyPaymaster = await artifacts.readArtifact("MyPaymaster"); const MyPaymasterHash = await deployer.deployContract({ // ← 使用 deployContract abi: MyPaymaster.abi, bytecode: MyPaymaster.bytecode, args: [entryPointAddress], });

const MyPaymasterReceipt = await publicClient.waitForTransactionReceipt({ hash: MyPaymasterHash }); console.log("MyPaymaster合约地址:", MyPaymasterReceipt.contractAddress);

}

main().catch((error) => { console.error(error); process.exit(1); });

#### 特殊说明:关于本地部署EntryPoint合约问题
`
本地开发直接在安装包中获取编译后的@account-abstraction/contracts/artifacts/EntryPoint.json 进行部署,如果是测试网或主网中直接使用在线的合约地址即可
`
#### 部署指令

npx hardhat run ./scripts/xxx.ts


#### **智能合约测试**

// test/ERC4337Wallet.ts import assert from "node:assert/strict"; import { describe, it, beforeEach } from "node:test"; import { parseEther, toHex, hashMessage } from "viem"; import { network } from "hardhat"; import EntryPointArtifact from "@account-abstraction/contracts/artifacts/EntryPoint.json";

describe("ERC4337钱包核心功能测试", async function () { let viem: any; let publicClient: any; let deployer: any, owner: any, user1: any; let entryPointAddress: string; let mySmartAccountFactory: any; let myPaymaster: any;

beforeEach(async function () { const networkData = await network.connect(); viem = networkData.viem;

[deployer, owner, user1] = await viem.getWalletClients();
publicClient = await viem.getPublicClient();

// 部署 EntryPoint
console.log("🚀 部署 EntryPoint...");
const entryPointHash = await deployer.deployContract({
  abi: EntryPointArtifact.abi,
  bytecode: EntryPointArtifact.bytecode,
  args: [],
});

const entryPointReceipt = await publicClient.waitForTransactionReceipt({ 
  hash: entryPointHash 
});
entryPointAddress = entryPointReceipt.contractAddress!;
console.log("✅ EntryPoint:", entryPointAddress);

// 部署工厂和 Paymaster
mySmartAccountFactory = await viem.deployContract("MySmartAccountFactory", [entryPointAddress]);
console.log("✅ Factory:", mySmartAccountFactory.address);

myPaymaster = await viem.deployContract("MyPaymaster", [entryPointAddress]);
// await myPaymaster.write.deposit();
console.log("✅ Paymaster:", myPaymaster.address);

});

it("应创建智能账户并验证所有权", async function () { const salt = toHex("test-salt", { size: 32 });

// 1. 创建账户
const createTx = await mySmartAccountFactory.write.createAccount([
  salt,
  owner.account.address
]);
await publicClient.waitForTransactionReceipt({ hash: createTx });

// 2. 获取新创建的账户地址(关键!)
const accountAddress = await mySmartAccountFactory.read.getAddress([
  salt,
  owner.account.address
]);

// 3. 验证地址是合约
const code = await publicClient.getCode({ address: accountAddress });
assert.ok(code && code.length > 2, "账户未成功部署");

// 4. 使用新账户地址创建实例
const mySmartAccount = await viem.getContractAt("MySmartAccount", accountAddress);

// 5. 验证所有权(这会成功,因为账户已初始化)
const actualOwner = await mySmartAccount.read.owner();
assert.equal(actualOwner.toLowerCase(), owner.account.address.toLowerCase());

console.log("✅ 账户地址:", accountAddress);
console.log("✅ 所有者:", actualOwner);

});

it("应验证 ERC-1271 签名", async function () { const walletClient = (await viem.getWalletClients())[0]; const salt = toHex("test-salt-1271", { size: 32 });

const createTx = await mySmartAccountFactory.write.createAccount([
  salt,
  owner.account.address
]);
await publicClient.waitForTransactionReceipt({ hash: createTx });

const accountAddress = await mySmartAccountFactory.read.getAddress([
  salt,
  owner.account.address
]);
const mySmartAccount = await viem.getContractAt("MySmartAccount", accountAddress);

const message = "Hello ERC-1271";
const messageHash = hashMessage({ raw:message });
const signature = await walletClient.signMessage({
  account: owner.account,
  message
});

const result = await mySmartAccount.read.isValidSignature([messageHash, signature]);
console.log("✅ 验证结果:", result);
// assert.equal(result, "0x1626ba7e");

});

it("应允许所有者执行交易", async function () { const salt = toHex("test-salt-exec", { size: 32 });

const createTx = await mySmartAccountFactory.write.createAccount([
  salt,
  owner.account.address
]);
await publicClient.waitForTransactionReceipt({ hash: createTx });

const accountAddress = await mySmartAccountFactory.read.getAddress([
  salt,
  owner.account.address
]);
const mySmartAccount = await viem.getContractAt("MySmartAccount", accountAddress);

// 存入 ETH
await deployer.sendTransaction({
  to: accountAddress,
  value: parseEther("1")
});

// 执行转账
const executeTx = await mySmartAccount.write.execute([
  user1.account.address,
  parseEther("0.5"),
  "0x"
], { account: owner.account });

const receipt = await publicClient.waitForTransactionReceipt({ hash: executeTx });
assert.equal(receipt.status, "success");

});

it("应管理 Paymaster 白名单", async function () { const app = user1.account.address;

// 初始不在白名单
const isWhitelistedBefore = await myPaymaster.read.whitelistedApps([app]);
assert.equal(isWhitelistedBefore, false);

// 添加白名单
await myPaymaster.write.whitelistApp([app], { account: deployer.account });
const isWhitelistedAfter = await myPaymaster.read.whitelistedApps([app]);
assert.equal(isWhitelistedAfter, true);
console.log("✅ 已添加白名单:", app);

}); });

#### **测试指令**

npx hardhat test ./test/xxx.ts


# 总结
以上内容完成了对 ERC-4337 标准的全面知识梳理,同时涵盖了其相关智能合约的完整实现流程。从核心知识解析到合约开发、测试验证,再到部署上线,每个环节均提供了详细的操作指引与逻辑说明,形成了 “理论梳理 + 实践落地” 的完整闭环,为开发者掌握 ERC-4337 标准应用与合约开发提供了清晰且可落地的参考框架
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。