Appearance
L4-1: Smart Wallets + Paymaster(智能钱包 + Gas 代付)
1. 问题
传统以太坊账户(EOA,外部拥有账户)由单个私钥控制。这种模型有根本性的局限:(1) 私钥丢失 = 资金永久丢失,无恢复机制;(2) 每次操作都需要 ETH 支付 Gas,新用户面临"先有鸡还是先有蛋"的难题;(3) 无法实现复杂的权限策略——如日常消费限额、时间锁、或多人共签;(4) 用户体验笨拙——每笔交易都需要确认和等待。
ERC-4337(账户抽象,Account Abstraction)提出了一套不修改共识层的解决方案:引入智能合约钱包(代替 EOA)、Bundler(打包交易)、以及 Paymaster(代付 Gas)。用户操作不再是传统的以太坊交易,而是"用户操作"(UserOperation)——一个包含操作意图的数据结构,由智能钱包签名,由 Bundler 打包提交。
这个挑战构建了完整 ERC-4337 栈的核心组件:一个智能钱包合约(SmartWallet.sol)和一个代币支付 Gas 的 Paymaster(TokenPaymaster.sol)。用户可以用 ERC-20 代币而不是 ETH 来支付 Gas 费,同时享受智能钱包带来的所有优势。
2. 原因
ERC-4337 已经被超过 3000 万个智能账户采用,是以太坊账户抽象的实际标准。Vitalik Buterin 多次强调,账户抽象是实现主流 Web3 采用的必要路径——它消除了新用户入门的技术摩擦。2026 年,EntryPoint v0.7 已在所有主要 EVM 链上以相同地址 (0x0000000071727De22E5E9d8BAf0edAc6f37da032) 通过 CREATE2 部署,成为以太坊基础设施的核心组件。
理解 ERC-4337 对整个 Web3 生态系统至关重要:
- 前端开发:需要理解如何构建 UserOperation 并用智能钱包签名
- 合约开发:需要理解
validateUserOp和execute的接口,以及 EntryPoint 的生命周期 - 基础设施开发:Bundler 和 Paymaster 的构建是新兴的基础设施领域
- 安全工程:智能钱包引入了不同于 EOA 的攻击面,需要不同的安全考量
不同于 EIP-7702(通过在 EOA 中委托代码到智能合约来实现账户抽象——一种"EOA 升级"方案),ERC-4337 是完全的智能合约钱包方案,不依赖 EOA 的存在。两者是互补的技术路径:EIP-7702 给现有 EOA 用户提供升级路径,ERC-4337 面向原生智能钱包的新用户。
3. 方案
完整的 ERC-4337 架构包含四个协作的组件。这个挑战实现了其中两个核心合约:
3.1 架构总览
用户 (可能无 ETH)
|
├─→ 签名 UserOperation
| {sender, nonce, callData, paymasterAndData, signature, ...}
|
▼
Bundler (监听 alt mempool 中的 UserOperation)
|
├─→ simulateValidation() — 模拟验证阶段
├─→ 打包多个 UserOperation 成 bundle
├─→ handleOps([userOp1, userOp2, ...]) — 提交到 EntryPoint
|
▼
EntryPoint (0x0000000071727De22E5E9d8BAf0edAc6f37da032)
|
├─→ 验证阶段:
│ Wallet._validateSignature(userOp, userOpHash)
│ Paymaster._validatePaymasterUserOp(userOp, userOpHash, maxCost)
|
├─→ 执行阶段:
│ Wallet.execute(to, value, data) 或 executeBatch(...)
| 如果验证阶段成功,Bundler 垫付 Gas
|
└─→ 后执行阶段:
Paymaster._postOp(mode, context, actualGasCost)
从用户扣取 ERC-20 代币补偿 Bundler
3.2 SmartWallet(智能钱包合约)
SmartWallet.sol 是 ERC-4337 兼容的智能合约钱包,实现两个核心接口:
签名验证(_validateSignature):由 EntryPoint 在验证阶段调用。验证逻辑:
- 从
userOp.signature(65 字节 ECDSA 签名)中恢复签名者地址 - 检查恢复的地址是否匹配
owner - 检查
userOp.nonce是否匹配钱包当前的nonce(防止重放) - 返回 0 表示验证成功,返回 1 表示失败(SIG_VALIDATION_FAILED)
签名恢复的实现支持标准的 v 值规范化(如果 v < 27 则加 27),同时兼容 EIP-155 重放保护的 v 值编码(chainId * 2 + 35)。
交易执行(execute / executeBatch):
execute():执行单笔调用。只能由 EntryPoint 调用(onlyEntryPoint修饰符)。成功执行后递增 nonce。executeBatch():原子化批量执行。所有调用必须成功,否则全部回滚。传入的三个数组(to、values、datas)必须长度一致。
安全门控:所有执行函数使用 onlyEntryPoint 修饰符,确保只有 EntryPoint 可以触发执行。这是 ERC-4337 的核心安全模型——用户操作必须通过完整的验证流水线(包括 Gas 支付、签名验证和 nonce 检查)才能执行。
接收 ETH:receive() 函数允许钱包直接接收 ETH 转账(如从其他用户或合约转入资金)。
3.3 TokenPaymaster(代币 Gas 代付合约)
TokenPaymaster.sol 实现了允许用户用 ERC-20 代币支付 Gas 的 Paymaster。核心机制:
验证(_validatePaymasterUserOp):在 EntryPoint 的验证阶段被调用,接收三个参数:
userOp:完整的用户操作,paymasterAndData字段包含编码的maxTokenCost(用户授权支付的最大代币数量)userOpHash:用户操作的哈希(虽然在此实现中未被使用,但完整实现中用于额外的签名验证)maxCost:EntryPoint 估算的此操作最大 Gas 成本
验证检查:
- 解析
paymasterAndData中的maxTokenCost(从第 20 字节开始取 32 字节的 uint256) - 根据固定汇率
TOKENS_PER_ETH = 2000计算tokenCost = maxCost * 2000 - 验证用户是否批准了足够的代币额度(
token.allowance(userOp.sender, address(this))) - 验证 Paymaster 在 EntryPoint 中是否有足够的 ETH 存款(
entryPoint.balanceOf(address(this))) - 返回 context(编码的
(sender, tokenCost)),供_postOp使用
后执行(_postOp):在用户操作执行后由 EntryPoint 调用:
- 只在操作成功时(
mode == PostOpMode.opSucceeded)扣费 - 根据实际消耗的 Gas 计算最终代币费用:
actualTokenCost = actualGasCost * TOKENS_PER_ETH - 不超过预授权的
tokenCost上限 - 从用户账户
transferFrom代币到 Paymaster 合约
存款管理:Paymaster 需要在 EntryPoint 中存入 ETH 来支付 Bundler 的 Gas:
deposit():任何人都可以存入 ETH 到 EntryPoint(标注为 Paymaster 的存款)withdraw():只有 owner 可以将 ETH 从 EntryPoint 提取回 owner 地址
汇率模型:当前使用硬编码汇率 TOKENS_PER_ETH = 2000。这是一个简化——生产 Paymaster 需要集成 Chainlink 或其他预言机来获取实时 ETH/代币汇率。
参考源文件:src/level4/SmartWallet.sol、src/level4/TokenPaymaster.sol
3.4 Bundler 逻辑(概念层)
Bundler 是 ERC-4337 中的关键基础设施组件,负责:
- 监听 alt mempool:收集 UserOperation(不同于传统交易 mempool)
- 模拟验证:调用
EntryPoint.simulateValidation()检查每个 UserOperation 是否会成功 - Gas 排序:按 Gas 价格(
maxPriorityFeePerGas)对 UserOperation 降序排列(优先费高的优先) - 打包提交:调用
EntryPoint.handleOps()将所有 UserOperation 批量提交 - Gas 补偿:Bundler 垫付所有 Gas,然后从 Paymaster 或钱包的 EntryPoint 存款中获得补偿
3.5 UserOperation 数据结构
struct UserOperation {
address sender; // 智能钱包地址
uint256 nonce; // 防重放计数器
bytes initCode; // 工厂部署代码(新钱包首次操作)
bytes callData; // 要执行的 calldata
uint256 callGasLimit; // 执行阶段的 Gas 限制
uint256 verificationGasLimit;// 验证阶段的 Gas 限制
uint256 preVerificationGas; // Bundler 补偿
uint256 maxFeePerGas; // EIP-1559 最大费用
uint256 maxPriorityFeePerGas;// EIP-1559 优先费
bytes paymasterAndData; // paymaster 地址(20B) + 自定义数据
bytes signature; // 钱包签名的 userOpHash
}
4. 遭遇的陷阱
- 验证和执行阶段的 Gas 分离:ERC-4337 将验证阶段(签名检查、Paymaster 验证)与执行阶段分开,如果验证阶段消耗过多 Gas 导致
verificationGasLimit超出,整个操作直接失败——且 Bundler 得不到 Gas 补偿 - Paymaster 的 ETH 存款不足:如果在
_validatePaymasterUserOp中存款检查失败,Bundler 会被拒绝补偿——但_validatePaymasterUserOp本身消耗的 Gas 也无法收回 - 代币汇率波动:
TokenPaymaster.sol使用硬编码汇率TOKENS_PER_ETH = 2000,如果 ETH/代币价格大幅波动,用户可能支付过多或过少的代币 - paymasterAndData 解码:
_decodeMaxTokenCost()使用内联汇编从 calldata 偏移 20 字节处读取 uint256,如果paymasterAndData格式不正确(长度不足 52 字节),会读取到垃圾数据或越界数据 - nonce 同步问题:Bundler 提交多个 UserOperation 时,如果某个 UserOperation 的 nonce 与其他操作不连续,整个
handleOps调用可能失败 - 智能钱包无恢复机制:当前实现只有单一 owner,如果 owner 私钥丢失,钱包中的资金将永久锁定——没有社交恢复或时间锁机制
- EntryPoint 地址硬编码:如果 EntryPoint 升级(从 v0.6 到 v0.7),钱包和 Paymaster 需要重新部署以指向新的 EntryPoint
5. 陷阱的原因
验证阶段 Gas 管理是 ERC-4337 最特殊的挑战。在传统交易中,Gas 是一个连续的消耗过程。在 ERC-4337 中,验证阶段的 Gas 按 verificationGasLimit 独立计价:Bundler 在执行任何操作前先支付验证 Gas,如果验证失败,Bundler 不仅得不到补偿,还要自己承担验证阶段的 Gas 成本。这创造了一个激励机制问题:Bundler 不愿意接受验证逻辑复杂(高 Gas)的 UserOperation。
更具体地说,_validatePaymasterUserOp 中的 token.allowance() 和 entryPoint.balanceOf() 调用都是外部合约调用,消耗的 Gas 比简单的存储读取多得多。如果 Paymaster 的验证逻辑过于复杂,Bundler 可能因为潜在的亏损而拒绝打包这些 UserOperation。这迫使 Paymaster 开发者在验证效率和功能完整性之间做出权衡。
Paymaster 存款不足是一个连锁故障:如果存款在 _validatePaymasterUserOp 中检查通过,但在实际执行时(可能几秒到几分钟后)因为其他 UserOperation 消耗了存款而变得不足,Bundler 在 _postOp 阶段将无法获得补偿。ERC-4337 通过要求 Paymaster 在 EntryPoint 中持有足够的存款来解决这个问题——存款在验证阶段被"锁定",不会被其他 UserOperation 消耗。
硬编码汇率的危险在于套利:如果 TOKENS_PER_ETH = 2000 但市场价格是 2500,用户可以用低于市场价的代币支付 Gas,Paymaster 承担损失。如果市场价格是 1500,用户需要多付代币,可能导致用户体验恶化。生产 Paymaster 必须使用实时预言机价格。
6. 如何解决陷阱
验证 Gas 优化:
- 在
_validatePaymasterUserOp中尽量减少外部调用——考虑缓存entryPoint.balanceOf()的结果 - 使用
SLOAD而非CALL来检查白名单等信息 - 对于复杂验证逻辑,将部分检查移到执行阶段(由钱包合约自行承担)
- 为 Bundler 设置盈利预期模型——模拟验证后计算补偿是否覆盖成本
汇率预言机集成(生产代码示例):
solidity
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract TokenPaymaster {
AggregatorV3Interface public tokenPriceFeed; // Token/ETH price
function getTokenCost(uint256 ethCost) public view returns (uint256) {
(, int256 price,,,) = tokenPriceFeed.latestRoundData();
// price 是 token/ETH(缩放 1e8),计算需要多少 token 来支付 ethCost
return (ethCost * uint256(price)) / 1e8;
}
}
nonce 管理改进:使用 ERC-4337 的 key-based nonce 管理(EntryPoint v0.7 支持二维 nonce:(key, sequence))。这允许多个独立的 nonce 序列,支持并发 UserOperation:
钱包恢复机制:在 SmartWallet 中添加 owner 转移功能(通过 EntryPoint 或 owner 自身调用):
solidity
function transferOwnership(address newOwner) external {
require(msg.sender == owner || msg.sender == entryPoint, "Not authorized");
owner = newOwner;
}
更高级的实现可以添加社交恢复模块,允许多个 guardian 通过投票来替换 owner。
存款监控:构建自动化存款补充脚本——当 Paymaster 在 EntryPoint 中的余额低于安全阈值时自动调用 deposit()。
javascript
async function monitorPaymasterDeposit(paymaster, entryPoint, minBalance) {
const balance = await entryPoint.balanceOf(paymaster.address);
if (balance < minBalance) {
await paymaster.deposit({ value: topUpAmount });
}
}
7. 技术要点
| 技术点 | 说明 |
|---|---|
| ERC-4337 | 账户抽象标准,UserOperation 替代传统交易 |
| EntryPoint | 全局单例合约,所有 EVM 链同一地址 (CREATE2) |
| UserOperation | 包含 sender/nonce/callData/signature/paymasterAndData |
| 签名验证 (_validateSignature) | 由 EntryPoint 在验证阶段调用,返回 0=成功 1=失败 |
| 批量执行 (executeBatch) | 原子化批量调用,全成功或全回滚 |
| Paymaster | 代付 Gas,用户可以用 ERC-20 代币支付 |
| 验证→执行→后执行 | 三阶段流水线:validate → execute → postOp |
| 验证阶段 Gas | verificationGasLimit 独立计价,失败则 Bundler 承担 |
| paymasterAndData | 20 字节 Paymaster 地址 + 自定义数据(如 maxTokenCost) |
| EntryPoint 存款 | Paymaster 在 EntryPoint 中存入 ETH,用于补偿 Bundler |
| PostOpMode | opSucceeded/opReverted/postOpReverted,控制后执行扣费 |
| ECDSA 签名恢复 | v 值规范化(v<27 则 +27),ecrecover 预编译 |