OpenZeppelin Contracts 的 ERC-4337 担保式 Paymaster 修复:有效预存款(Effective Prefund)如何在扩展组合中正确传播
2026/9/11 1:41:15 网站建设 项目流程

OpenZeppelin Contracts 的 ERC-4337 担保式 Paymaster 修复:有效预存款(Effective Prefund)如何在扩展组合中正确传播

【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts

导读

本篇文章围绕 OpenZeppelin Contracts 仓库中一条 release changeset(.changeset/paymaster-guarantor-effective-prefund.md)展开,剖析其对PaymasterERC20Guarantor(ERC-20 代币付费 + 第三方担保的 ERC-4337 Paymaster 扩展)的一次关键修复:_prefund改为返回由super._prefund传播回来的有效预存款金额,而非调用方传入的输入金额。读完本文,你将理解该修复解决的实际问题——下游扩展实际拉取的代币少于请求量时,postOp 上下文中序列化的金额必须与真实扣款一致,否则会导致退款路径超付或事件记账失真——并掌握从 changeset 回溯到源码实现与测试用例的完整证据链。

一、这条 changeset 是什么:一次 patch 级别的行为修复

在 OpenZeppelin Contracts 仓库中,.changeset/目录存放由 Changesets 工具管理的发布说明文件(.changeset/config.jsonchangelog使用@changesets/changelog-githubaccesspublicbaseBranchmaster)。每条文件在发布时会被合并进 CHANGELOG,并驱动语义化版本号计算。

本文件的 front matter 声明了版本影响:

--- 'openzeppelin-solidity': patch ---

即这是一次patch(补丁)级变更,代表向后兼容的缺陷修复,不引入破坏性 API 变化。正文如下:

PaymasterERC20Guarantor: Return the effectiveprefundAmountpropagated bysuper._prefundinstead of the input amount, so extensions composed below the guarantor that pull less than requested serialize the actual pulled amount into the postOp context.

翻译过来:PaymasterERC20Guarantor._prefund现在返回由super._prefund传播回来的有效prefundAmount,而不再返回输入金额;这样,位于担保人之下、实际拉取金额少于请求量的下游扩展,就能把真实拉取量序列化进 postOp 上下文。

这条变更关联的模块位于 contracts/account/paymaster/extensions/PaymasterERC20Guarantor.sol(v5.7.0)。要理解它,需要先弄清 Paymaster 的"预扣–退款"资金模型。

二、背景:PaymasterERC20 的预扣–退款模型与担保人扩展

2.1 基础资金流:先预扣最大成本,执行后按实际成本退款

PaymasterERC20(contracts/account/paymaster/extensions/PaymasterERC20.sol)让用户用 ERC-20 代币支付 gas,遵循两阶段模型:

  1. 验证阶段(_validatePaymasterUserOp:按maxCost与 postOp 估算开销预扣最大可能成本(_prefund执行transferFrom);
  2. 执行后阶段(_postOp_refund:按 EntryPoint 报告的actualGasCost结算实际成本,把prefundAmount - actualAmount退回。

基础_prefund的实现(PaymasterERC20.sol L161-L170)恰好拉取请求的金额

function _prefund( PackedUserOperation calldata /* userOp */, bytes32 /* userOpHash */, IERC20 token, uint256 /* tokenPerNative */, address prefunder_, uint256 prefundAmount_ ) internal virtual returns (bool success, address prefunder, uint256 prefundAmount, bytes memory prefundContext) { return (token.trySafeTransferFrom(prefunder_, address(this), prefundAmount_), prefunder_, prefundAmount_, ""); }

注意它的返回约定(注释明确说明):prefundAmount是"effective prefund actually pulled"(实际拉取的有效金额)。基础实现中请求量与拉取量一致,但在扩展组合中二者可能分叉——这正是本次修复的切入点。

2.2 担保人(Guarantor)扩展:第三方代付 gas

PaymasterERC20GuarantorPaymasterERC20的扩展,允许第三方(担保人)替用户预付 gas,典型场景是空投领取

  • 担保人预先支付最大可能 gas 成本;
  • 用户在操作执行中领到空投代币;
  • 用户从空投所得中偿还担保人;
  • 若用户无法偿还,担保人吸收成本。

担保人通过_fetchGuarantor(userOp)识别(返回address(0)表示不使用担保功能)。当存在担保人时,_prefund会:

  1. 把预扣金额膨胀_guaranteedPostOpCost()(默认 15_000 gas,按maxFeePerGas折算成代币)对应的成本,覆盖担保人在_refund中的额外 postOp 工作(向用户trySafeTransferFrom+ 向担保人trySafeTransfer);
  2. 将担保人设为 prefunder;
  3. prefundContext尾部追加userOp.sender,供退款阶段识别发起者。

修复后的_prefund(PaymasterERC20Guarantor.sol L47-L84):

function _prefund( PackedUserOperation calldata userOp, bytes32 userOpHash, IERC20 token, uint256 tokenPrice, address prefunder_, uint256 prefundAmount_ ) internal virtual override returns (bool success, address prefunder, uint256 prefundAmount, bytes memory prefundContext) { address guarantor = _fetchGuarantor(userOp); bool isGuaranteed = guarantor != address(0); if (isGuaranteed) { // _erc20Cost 可能返回 type(uint256).max 作为溢出哨兵;saturatingAdd 保留它, // 让坏值到达 trySafeTransferFrom 时失败,而不是在此处 revert。 uint256 guaranteedPostOpCost = _erc20Cost(_guaranteedPostOpCost() * userOp.maxFeePerGas(), tokenPrice); prefundAmount_ = prefundAmount_.saturatingAdd(guaranteedPostOpCost); prefunder_ = guarantor; } (success, prefunder, prefundAmount, prefundContext) = super._prefund( userOp, userOpHash, token, tokenPrice, prefunder_, prefundAmount_ ); if (prefunder == guarantor) { emit UserOperationGuaranteed(userOpHash, prefunder, prefundAmount); } return (success, prefunder, prefundAmount, abi.encodePacked(prefundContext, userOp.sender)); }

三、修复的核心:返回"有效金额"而非"输入金额"

3.1 问题所在:扩展组合中拉取量可能少于请求量

PaymasterERC20家族是典型的可组合扩展:开发者可以在PaymasterERC20Guarantor之上再叠加其他扩展(继承顺序如Mock → PaymasterERC20Guarantor → PaymasterERC20ReducingMock → PaymasterERC20)。这些位于担保人之下super方向)的扩展,可能在_prefund中实际拉取的金额少于传入的请求量

仓库中的PaymasterERC20ReducingMock(contracts/mocks/account/paymaster/PaymasterERC20Mock.sol L194-L234)就是这样一个模拟:它在调用super._prefund时把金额减 1,模拟一个"固定减免(fixed-credit)"策略——合法地以低于请求量的价格结算。

修复前,PaymasterERC20Guarantor._prefund的返回值把prefundAmount原样设为输入值prefundAmount_。一旦下游扩展实际只拉取了prefundAmount_ - 1,担保人返回给上层调用者的却是完整的请求量。这一失真的金额随后被:

  • _validatePaymasterUserOp(PaymasterERC20.sol L122-L145)序列化进 context:abi.encodePacked(userOpHash, token, tokenPerNative, prefundAmount, prefunder, penaltyGas, prefundContext)
  • 一路传递到_postOp(PaymasterERC20.sol L181-L217),解码出prefundAmount作为退款基数。

结果就是:postOp 上下文记录的预存款比 Paymaster 账户里真实进入的代币多,退款路径可能按虚高的基数执行(例如向担保人退回超过实际持有的金额),或者UserOperationSponsored事件记录的tokenAmount与真实结算不符。

3.2 修复内容:传播super._prefund的返回值

修复只需一行关键改动:将返回语句中的金额改为super._prefund传播回来的值——即上面代码中的(success, prefunder, prefundAmount, prefundContext) = super._prefund(...)之后,直接返回这个prefundAmount(它已经是下游扩展层层修正后的有效值),而不是构造返回值时重新使用输入变量prefundAmount_

由于PaymasterERC20._prefund的契约本身就是"返回实际拉取的有效金额",这个传播保证了整个组合链上的金额始终是真实进入 Paymaster 的代币量,与prefundContext一起被序列化进 postOp context,供_refund精确结算。

从源码结构看,这一修复同时保持了与PaymasterERC20._prefund文档契约的一致性(PaymasterERC20.sol L149-L160:"Extensions may inflate the amount ... and must return the effective value"),修复让担保人扩展真正履行了"返回有效值"的约定。

四、对称修复:_refund同样传播有效结算金额

值得注意,本次 changeset 只提到_prefund,但仓库中_refund已经实现了对称的传播逻辑(PaymasterERC20Guarantor.sol L102-L146),二者共同保证整条资金链路金额不失真:

  • 非担保分支prefunder == userOp.sender):调用super._refund后,返回returnedEffectiveAmount(下游扩展实际结算的金额),保证UserOperationSponsored.tokenAmount与真实结算一致;
  • 担保分支prefunder != userOp.sender):把actualAmount膨胀上_guaranteedPostOpCost() * actualUserOpFeePerGas的代币成本,先尝试从用户(从prefundContext尾部读出的userOp.sendertrySafeTransferFrom拉取;成功则把actualAmount归零,让super把全部prefundAmount退给担保人;失败则保留该金额,由担保人吸收(注意:担保人吸收的是膨胀后的担保成本,而非基础成本)。

Math.ternary(prefunder != userOpSender, effectiveAmount, returnedEffectiveAmount)确保了返回值语义精确:只有非担保分支才传播下游扩展的真实收费,担保分支返回的是事件里记录的担保人膨胀金额。

五、测试验证:证据链完整闭环

仓库测试 test/account/paymaster/PaymasterERC20Guarantor.test.js 为本次修复提供了直接验证,其中propagates effective amounts from downstream extensions测试组(L432-L516)专门构造了组合:

Mock → PaymasterERC20Guarantor → PaymasterERC20ReducingMock → PaymasterERC20

5.1_prefund必须返回 super 实际拉取的金额

测试'_prefund returns the amount pulled by super, not the input'(L454-L481):

  • 请求requested = 100n,reducing 扩展实际拉取effective = 99n
  • 断言$_prefund的返回值是(true, this.other.address, 99n, ...),即有效值
  • 并核对代币余额:other归零、Paymaster 恰好持有99n

测试注释明确指出:没有该修复时返回值会是requested(100),比真实进入 Paymaster 的多一枚代币,这个虚高值会被序列化进 postOp context——与 changeset 描述的问题完全对应。

5.2_refund对非担保操作传播实际收费

测试'_refund returns the amount charged by super for non-guaranteed operations'(L483-L515)验证对称路径:输入actualAmount = 40n,reducing 扩展实际结算39n,断言返回值是(true, 39n),且 Paymaster 余额恰好等于有效结算额。否则UserOperationSponsored.tokenAmount会与真实结算不一致。

5.3 真实场景测试

同一文件还覆盖了端到端流程:

  • 用户成功偿还担保人(L165-L235):操作中先给用户铸币再授权 Paymaster,用户偿还后担保人余额不变(factor 0),并断言UserOperationGuaranteed/UserOperationSponsored事件与代币、ETH 余额变动一致;
  • 用户未能偿还(L237-L299):用户拿不到资产,担保人余额减少(factor -1),Paymaster 吸收担保成本;
  • 冷存储担保人(L301-L354):余额与授权全部是"冷"状态,验证trySafeTransferFrom路径;
  • 无效担保人签名(L357-L371):EntryPoint 以AA34 signature error拒绝;
  • 担保人余额/授权不足(L399-L429):同样以AA34 signature error拒绝,印证_prefund中"不 revert、返回失败"的设计(失败以SIG_VALIDATION_FAILED形式体现,避免在验证阶段 revert 损害 Paymaster 信誉——见 PaymasterERC20.sol L157-L159 的 NOTE)。

六、理解这条修复对使用者的意义

6.1 对 Paymaster 开发者的影响

  • 升级成本:patch 级变更,无需改动接口;但若你已基于PaymasterERC20Guarantor组合了"拉取量少于请求量"的下游扩展(如折扣、减免策略),此修复会让链上记账与真实资金流严格对齐,属于正确性修复
  • 组合顺序:担保人必须位于"会削减金额的扩展"之上(super方向),有效金额才能被正确传播回 context;
  • 担保成本覆盖_guaranteedPostOpCost()默认 15_000 gas,与PaymasterERC20._postOpCost()(默认 30_000)类似,gas 更重的代币应覆盖为更高值,否则会持续低估并损耗 Paymaster 存款。

6.2 资金流全局回顾

修复后完整链路为:

  1. _validatePaymasterUserOp计算maxTokenCost(含 postOp 成本与_postOpGasPenalty);
  2. PaymasterERC20Guarantor._prefund膨胀金额并调用super._prefund
  3. 下游扩展削减实际拉取量,super链逐层传播有效金额
  4. 担保人把有效金额与prefundContext(尾部附userOp.sender)一并返回;
  5. _validatePaymasterUserOp将其序列化进 context;
  6. _postOp解码并调用_refund,按有效prefundAmount精确退款、按有效actualAmount发出UserOperationSponsored

正是 changeset 中的这一行改动,堵住了第 3~4 步之间金额失真的漏洞,让整个组合链上的预存款始终"如实入账"。

七、结论

这条 patch changeset 是 OpenZeppelin Contracts 中"小改动、深影响"的典型:表面只改了一处返回值,实质修复了可组合扩展架构下资金记账失真的正确性问题。透过它可以看到PaymasterERC20家族设计的两个关键契约——_prefund返回有效拉取量、_refund返回有效结算量——以及它们在测试中的严格验证。相关实现与验证可继续参阅:

  • 变更说明:.changeset/paymaster-guarantor-effective-prefund.md
  • 担保人扩展实现:contracts/account/paymaster/extensions/PaymasterERC20Guarantor.sol
  • 基础 ERC-20 Paymaster:contracts/account/paymaster/extensions/PaymasterERC20.sol
  • 基础 Paymaster(存款/质押/EntryPoint 约束):contracts/account/paymaster/Paymaster.sol
  • 组合扩展测试桩(Reducing/GurantorReducing Mock):contracts/mocks/account/paymaster/PaymasterERC20Mock.sol
  • 端到端与单元测试:test/account/paymaster/PaymasterERC20Guarantor.test.js

【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询