导语:从助记词到智能合约钱包的用户体验革命
以太坊自诞生以来就有一个根本性的 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 | 过期时间、消费限额、目标白名单是否正确校验 |
| Paymaster | postOp 是否在所有情况下(包括 revert)都能正确执行 |
一句话总结:EntryPoint 的设计哲学是"不信任钱包、不信任 bundler"——通过 validate/execute 分离、模拟验证和严格的 gas 限制,把安全边界拉到了合约级别,让即使恶意的 Bundler 也无法盗取用户资金。
8. 总结
- 账户抽象的本质:让智能合约账户获得 EOA 的主动发交易能力
- ERC-4337 架构:UserOperation → Bundler → EntryPoint → Wallet + Paymaster
- Account Factory:CREATE2 确定性地址 + initCode 自动部署,消除新用户门槛
- Paymaster:第三方代付 gas,商业模式创新空间巨大
- 社交恢复:链上可编程的"忘记密码"机制
- Session Key:一次性授权、多次免密执行,消除高频交互的签名疲劳
- 安全核心: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);
}
}
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。