fix(p0-13): x402 nonce per-(asset, from, nonce) triple key#106
fix(p0-13): x402 nonce per-(asset, from, nonce) triple key#106
Conversation
P0-13 (B3-N3 + B2-N8): x402SettlementNonces was keyed by `nonce` alone in a global namespace. An anonymous attacker watching the mempool could pre-burn a victim's nonce by submitting a dummy settlement with the same nonce on a different (asset, from) pair, forcing the legitimate settlement to revert as "nonce used" — an anonymous DoS primitive. Defense: - New `x402NonceKey(asset, from, nonce) pure` public helper (matches SDK-side encoding). - _validateX402AndComputeFee now keys on the triple, isolating each payer's nonce space per asset. - Storage layout unchanged (mapping(bytes32 => bool) preserved); only the value placed into that bytes32 changes — UUPS upgrade safe. Tests (contracts/test/v3/SuperPaymasterV5Features.t.sol): - test_Nonce_PerAssetIsolation - test_Nonce_PerPayerIsolation - test_Nonce_TripleReplayBlocked - test_Nonce_PublicKeyMatchesStorage Wave 2 plan: docs/security/wave-plans/wave2-funds-price.md Spec: docs/security/2026-04-26-p0-prelaunch.md (security/audit-2026-04-25) Refs: P0-13 in 2026-04-26-p0-prelaunch.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 735c534fcc
ℹ️ 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".
| bytes32 key = x402NonceKey(asset, from, nonce); | ||
| if (x402SettlementNonces[key]) revert NonceAlreadyUsed(); | ||
| x402SettlementNonces[key] = true; |
There was a problem hiding this comment.
Preserve legacy nonce keys during upgrade
Switching replay protection to x402NonceKey(asset, from, nonce) without checking the legacy x402SettlementNonces[nonce] value re-opens all nonces that were already consumed before this upgrade. After upgrading from the prior version, a previously settled settleX402PaymentDirect tuple can be executed again because the old write used the raw nonce key, while this code only reads the new hash key; unlike the EIP-3009 path, the direct path has no token-level nonce guard to stop that replay.
Useful? React with 👍 / 👎.
代码审查通过,含 HIGH 注意事项 Triple key 设计正确:使用 HIGH — MEDIUM — 缺少跨路径(EIP-3009 vs Direct)nonce 共享验证测试 INFO — 旧单 nonce key 在 storage 中留存 |
…ding Replace abi.encodePacked with abi.encode in both settleX402Payment and settleX402PaymentDirect when computing settlementId. This aligns with x402NonceKey() which already uses abi.encode, ensuring consistent encoding across the x402 settlement flow and eliminating any future hash-collision risk if variable-length types are added to the tuple. Closes #106
修复说明问题: 修复:将两处 // Before:
settlementId = keccak256(abi.encodePacked(from, to, asset, amount, nonce));
// After:
settlementId = keccak256(abi.encode(from, to, asset, amount, nonce));注意:虽然 已更新 NatSpec:两个函数的 测试: |
Pre-P0-13 code keyed x402SettlementNonces by the raw bytes32 nonce alone. After upgrading to the triple-key scheme, those old slots are never checked, so a pre-upgrade settled tuple can be re-executed immediately post-upgrade. Fix: _validateX402AndComputeFee now checks `x402SettlementNonces[nonce]` (the legacy raw-key slot) BEFORE the new triple key, reverting with NonceAlreadyUsed if the slot is occupied — covering both settlement paths. Tests added (SuperPaymasterV5Features.t.sol): - test_Nonce_CrossPath_EIP3009ThenDirectBlocked: EIP-3009 consumed nonce rejected by Direct path for same (asset, from, nonce) triple. - test_Nonce_CrossPath_DirectThenEIP3009Blocked: Direct consumed nonce rejected by EIP-3009 path. - test_Nonce_LegacyRawNonceReplayBlocked: legacy raw-key slot set via stdstore; both paths must revert, proving the upgrade guard works. 306 forge tests, 0 failures.
PR #106 补丁:遗留 nonce 重放防护 + 跨路径测试Issue 1(P1):升级后遗留 raw-nonce 可被重放根因分析: 修复方案: // Guard against replay of settlements made BEFORE the P0-13 upgrade.
// Pre-V5.4 the mapping was keyed by the raw nonce bytes32 value alone.
if (x402SettlementNonces[nonce]) revert NonceAlreadyUsed();
bytes32 key = x402NonceKey(asset, from, nonce);
if (x402SettlementNonces[key]) revert NonceAlreadyUsed();
x402SettlementNonces[key] = true;两条 settle 路径均通过 Issue 2(Medium):新增跨路径 nonce 共享验证测试新增 3 个测试(
说明:两条路径都经过同一个 测试结果所有现有测试通过,无回归。 |
_applyAgentSponsorship had no observable on-chain signal when a discount was applied. The new event enables monitoring of agent subsidy usage. Hitchhike on P0-13.
P0-13 (B3-N3 + B2-N8)
`x402SettlementNonces` 把 nonce 放在全局命名空间。匿名攻击者观察 mempool 后用相同 nonce 在不同 (asset, from) 上下文先提交一笔 dummy settle → 合法 settle revert "nonce used" → 匿名 DoS。
Defense
合并到 main 后,SDK 必须重新生成 ABI。`@aastar/core/actions/x402.ts` 现有的单参数 `x402SettlementNonces({nonce})` 查询会永远返回 false(旧 key 没人写了)—— 见 `docs/integration/sdk-x402-integration.md` (PR #98) §3 ABI 同步章节。
Tests
4 新测试(per-asset 隔离 / per-payer 隔离 / 三元组 replay 拒绝 / public helper 与 storage 一致)+ 32 现有 = 36/36 passed
Spec
`docs/security/2026-04-26-p0-prelaunch.md` §3 P0-13
P2 顺风车追加(B3-N10)
_applyAgentSponsorship()应用折扣时没有链上事件,监控无法感知实际折扣使用情况。追加:在函数返回折扣值前 emit,参数包含 operator(运营商)、agent(折扣接受者)、bestBPS(实际折扣力度)。纯可观测性追加,无逻辑变更。414 tests pass.