Skip to content

fix(p0-12a): x402 Direct settle is xPNTs-only#109

Open
jhfnetboy wants to merge 4 commits intofix/p0-13-x402-nonce-triplefrom
fix/p0-12a-x402-asset-whitelist
Open

fix(p0-12a): x402 Direct settle is xPNTs-only#109
jhfnetboy wants to merge 4 commits intofix/p0-13-x402-nonce-triplefrom
fix/p0-12a-x402-asset-whitelist

Conversation

@jhfnetboy
Copy link
Copy Markdown
Member

@jhfnetboy jhfnetboy commented Apr 28, 2026

⚠️ Stacked PR

Base 是 fix/p0-13-x402-nonce-triple(PR #106,不是 main —— P0-12a 修改的 settleX402PaymentDirect 上层依赖 P0-13 的 x402NonceKey 三元组。GitHub 只展示这条 commit 的 delta。

P0-12a (B2-N4)

settleX402PaymentDirect(asset, from, to, amount, nonce)IERC20(asset).safeTransferFrom(from, ...) 不限制 asset 类型。用户给 USDC 做了标准 approve(facilitator, MAX)(infinite approval 是常规模式)→ 攻陷 facilitator 直接抽干 USDC。xPNTs 有防火墙 + $100 单笔上限,USDC 没有这个保护。

Defense (D4 决策)

  • xPNTsFactory 加 mapping(address => bool) public isXPNTs,每次 deployxPNTsToken / deployaPNTsToken 写入
  • SuperPaymaster.settleX402PaymentDirectrequire(IXPNTsFactory(xpntsFactory).isXPNTs(asset), "Direct: must be xPNTs")

USDC 走 EIP-3009 path(用户每次签名,符合 x402 标准),xPNTs 走 Direct path。

附加修复:删除错误注释(commit 210a7f6

_validateX402AndComputeFee 中有 5 行 @dev 注释声称"nonce 在 asset 白名单检查前被消耗,调用者若传入错误 asset 会丢失 nonce"。此说法与 EVM 语义矛盾:若后续 revert InvalidXPNTsToken() 触发,EVM 会回滚 storage write,nonce 不会被消耗。正确行为已在 settleX402PaymentDirect 的 NatSpec 中记录,错误的 inline 注释已删除。

Tests

新测试(拒绝非 xPNTs / 接受 xPNTs / factory isXPNTs 注册)+ 现有 settleDirect tests 不破坏

Spec

docs/security/2026-04-26-p0-prelaunch.md §3 P0-12a
docs/security/2026-04-26-decision-records.md D4

P0-12a (B2-N4): `settleX402PaymentDirect` accepted any ERC20 as the
asset and called `IERC20(asset).safeTransferFrom(from, ...)`. A user
who had done a standard infinite `approve(facilitator, MAX)` for USDC
(legitimate x402 EIP-3009 pattern, recommended by the spec) could be
drained by a compromised facilitator via the Direct path, because
USDC has no in-token firewall. xPNTs tokens carry the autoApproved
spender firewall + MAX_SINGLE_TX_LIMIT — arbitrary ERC20s do not.

Defense:
- xPNTsFactory now records `mapping(address => bool) public isXPNTs`
  on every successful `deployxPNTsToken`. The mapping is the
  single source of truth for "this token is a factory-deployed
  xPNTs" — no admin setter, no off-chain config needed.
- IxPNTsFactory exposes `isXPNTs(address)` so consumers (SP and
  off-chain SDKs) can check.
- SuperPaymaster.settleX402PaymentDirect:
  - Validates fee/role/nonce first (so the asset gate cannot be used
    to probe nonces or short-circuit auth).
  - Then `require(xpntsFactory.isXPNTs(asset))` via custom revert
    `InvalidXPNTsToken`.
  - If `xpntsFactory == address(0)`, revert `InvalidConfiguration`
    (fail-closed: a misconfigured deploy cannot accidentally accept
    arbitrary assets).

Note on aPNTs: aPNTs is the protocol settlement token, NOT a
factory-deployed xPNTs, so it is intentionally NOT whitelisted here.
Operator deposits/refunds of aPNTs go through `_consumeCredit` and
the aPNTs balance ledger, never through `settleX402PaymentDirect`.
USDC and other ERC-3009-compatible assets continue to use
`settleX402Payment` (EIP-3009 transferWithAuthorization) which carries
its own per-(asset, from, nonce) signature binding.

Storage layout: unchanged. xPNTsFactory adds one mapping (Ownable
slot 0 + existing storage; new mapping appends after deployedTokens).
SuperPaymaster has no new storage. UUPS upgrade safe.

Tests:
- contracts/test/v3/X402Direct_AssetWhitelist.t.sol (new, 6 tests):
  - test_SettleDirect_RejectsNonXPNTsAsset (USDC drain blocked)
  - test_SettleDirect_RejectsAPNTsAsset (aPNTs intentionally not Direct)
  - test_SettleDirect_AcceptsXPNTsAsset (happy path)
  - test_Factory_IsXPNTsTrueAfterDeploy
  - test_Factory_IsXPNTsFalseForArbitraryToken
  - test_SettleDirect_RevertsWhenFactoryUnset (fail-closed)
- contracts/test/v3/SuperPaymasterV5Features.t.sol: added
  MockXPNTsFactory + wired in setUp so existing Direct settle tests
  pass against the new gate. All 36 V5 features tests still green.

Refs: P0-12a in docs/security/2026-04-26-p0-prelaunch.md
      Wave 2 plan: docs/security/wave-plans/wave2-funds-price.md
@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner April 28, 2026 07:45
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92f0b64a5c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// a token deployed by the configured xPNTs factory.
address factory = xpntsFactory;
if (factory == address(0)) revert InvalidConfiguration();
if (!IxPNTsFactory(factory).isXPNTs(asset)) revert InvalidXPNTsToken();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep direct-settle compatible with existing factory deployments

settleX402PaymentDirect now hard-calls IxPNTsFactory(factory).isXPNTs(asset), but pre-existing xPNTsFactory contracts do not implement this selector; on an in-place SuperPaymaster upgrade (the repo’s upgrade flow upgrades the proxy without redeploying/repointing factory), this call reverts for every direct settlement, including legitimate xPNTs payments. This introduces a production outage path unless a factory migration/backfill is performed in lockstep.

Useful? React with 👍 / 👎.

@fanhousanbu
Copy link
Copy Markdown
Contributor

代码审查

通过,含 MEDIUM 注意事项

白名单检查正确isXPNTs(asset) 检查在所有 Direct 入口点生效,factory == address(0) 时 fail-closed 设计正确,执行顺序合理。

MEDIUM — nonce 在资产白名单检查之前被消耗
_validateX402AndComputeFee(内部标记 nonce 为已使用)先于 isXPNTs 检查执行。合法用户若误传了非 xPNTs 的 asset,其 nonce 会被提前消耗,需要使用新 nonce 才能重试。

PR 注释说明这是有意设计(防止攻击者反复尝试不同 asset 探测白名单而不消耗 nonce),这个理由成立。但需要在 SDK 文档和错误消息中明确说明:settleX402PaymentDirectInvalidXPNTsToken 失败,该 nonce 已不可复用。

INFO — isXPNTs mapping 只增不减
若某个 xPNTs 代币需要被撤销(如发现漏洞),工厂白名单没有移除机制。需要通过升级 SuperPaymaster 合约来处理。这是合理的设计简化,但需在运营文档中说明应急处理路径。

缺失测试

  • 未测试 asset 校验失败时 nonce 是否被消耗(建议补充以明确记录此行为)

…dComputeFee

Nonce is intentionally consumed before the asset whitelist check so that
replay is prevented regardless of the failure reason. Document the
trade-off: a caller with a non-xPNTs asset loses the nonce and must retry
with a fresh one; well-formed SDK clients always validate assets first.
@jhfnetboy
Copy link
Copy Markdown
Member Author

已在 _validateX402AndComputeFee 中补充 nonce 消耗顺序的设计说明。

设计意图:nonce 在资产白名单检查之前被标记消耗(intentional nonce-before-whitelist-check)。
原因:无论调用失败原因是什么,都能防止重放攻击。
权衡:若调用者传入非 xPNTs 资产,该 nonce 会被消耗,重试需要使用新的 nonce。这是可接受的,因为规范 SDK 客户端在提交前会验证资产地址。

Add two nonce-behavior tests to X402Direct_AssetWhitelist.t.sol:

- test_SettleDirect_NonceConsumed_OnAssetWhitelistFailure: verifies that
  EVM revert semantics roll back the nonce write when settleX402PaymentDirect
  reverts with InvalidXPNTsToken — the nonce is NOT consumed on failure,
  and the same nonce may be reused with a corrected (valid xPNTs) asset.

- test_SettleDirect_NonceConsumed_OnSuccess: confirms the nonce IS durably
  consumed on a successful call, and that replay reverts with NonceAlreadyUsed.

Also update the NatSpec on settleX402PaymentDirect in SuperPaymaster.sol
to clarify the nonce/revert interaction accurately (replacing a misleading
"nonce is consumed before whitelist check" note with the correct EVM
revert-rollback explanation).
@jhfnetboy
Copy link
Copy Markdown
Member Author

测试补充 + 重要发现:nonce 实际上不会被消耗

关键发现

PR reviewer 担忧「nonce 在 InvalidXPNTsToken revert 前被消耗」,但经代码验证:EVM revert 会回滚所有状态变更,包括 _validateX402AndComputeFee 中的 nonce 写入。nonce 不会被持久化消耗。

原代码注释「nonce is consumed before asset whitelist check」描述的是调用帧内的执行顺序,而非持久化结果,具有误导性,已更正。

新增测试(共 2 个)

  • test_SettleDirect_NonceConsumed_OnAssetWhitelistFailure:验证 InvalidXPNTsToken revert 后,同一 nonce 仍可重用(nonce 未被消耗)
  • test_SettleDirect_NonceConsumed_OnSuccess:对照测试,验证成功后 nonce 确实持久消耗,重放得到 NonceAlreadyUsed

测试结果:8/8 PASS

The comment claimed a caller loses their nonce if they supply a wrong
asset.  In reality EVM revert semantics roll back the storage write, so
the nonce is NOT consumed on failure.  The correct behavior is already
documented in the settleX402PaymentDirect NatSpec; the contradicting
inline comment is simply deleted.
Copy link
Copy Markdown
Contributor

@fanhousanbu fanhousanbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants