前言本文围绕ERC-4337标准展开全面梳理,先系统解析其核心内容,涵盖概念定义、核心价值、组件构成、工作流程、特性优势及当前面临的挑战与发展现状,构建起清晰的理论认知框架;再聚焦实践落地,基于OpenZeppelin与account-abstraction工具,完整实现ERC-4337钱包开发
本文围绕ERC-4337标准展开全面梳理,先系统解析其核心内容,涵盖概念定义、核心价值、组件构成、工作流程、特性优势及当前面临的挑战与发展现状,构建起清晰的理论认知框架;再聚焦实践落地,基于OpenZeppelin与account-abstraction工具,完整实现ERC-4337钱包开发,并详细拆解从开发搭建、测试验证到部署上线的全流程,形成“理论梳理+实践落地”的完整内容体系,为ERC-4337标准的学习与应用提供兼具系统性与可操作性的参考。
概述
ERC4337(Account Abstraction)是以太坊的账户抽象标准,旨在消除外部账户(EOA)与合约账户的界限。用户可通过智能合约自定义账户逻辑,实现社交恢复、代付Gas、批量交易等高级功能,而无需修改以太坊共识层;
核心价值:保留EOA的简单性,同时赋予合约账户的灵活性,显著提升Web3用户体验。
alt-mempool(替代内存池)validateUserOp() 验证签名,通过 executeUserOp() 执行交易alt-mempool 中的 UserOperationsEntryPoint 合约地位:ERC4337的核心链上网关
职责:
关键:所有Gas支付通过此合约完成(从用户存款或Paymaster)
定位:可选的智能合约,提供灵活Gas支付方案
两种模式:
接口:validatePaymasterUserOp() 验证资格,postOp() 处理后续结算
详细步骤
alt-mempool| 特性 | 说明 | 价值 |
|---|---|---|
| 社交恢复 | 通过守护人机制重置私钥 | 解决私钥丢失问题 |
| 无Gas交易 | Paymaster代付或ERC-20支付 | 降低新用户门槛 |
| 批量操作 | 单次签名执行多笔调用 | 提升操作效率 |
| 可编程权限 | 自定义验证逻辑(如多签、限额) | 增强安全性与灵活性 |
| 确定性地址 | 代理钱包地址跨网络一致 | 类似EOA的用户体验 |
ERC4337通过链下打包+链上验证的架构,在不修改以太坊协议的前提下实现账户抽象,是智能合约钱包普及的关键基础设施。
// 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";
/**
@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);
/**
/**
/**
/**
@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; // 验证成功 }
/**
@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 批量执行多笔交易 */ 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); }
/**
/**
@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; }
/**
/**
#### 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] < 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 标准应用与合约开发提供了清晰且可落地的参考框架 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!