Skip to content

fix(p0-13): x402 nonce per-(asset, from, nonce) triple key#106

Open
jhfnetboy wants to merge 4 commits intomainfrom
fix/p0-13-x402-nonce-triple
Open

fix(p0-13): x402 nonce per-(asset, from, nonce) triple key#106
jhfnetboy wants to merge 4 commits intomainfrom
fix/p0-13-x402-nonce-triple

Conversation

@jhfnetboy
Copy link
Copy Markdown
Member

@jhfnetboy jhfnetboy commented Apr 28, 2026

P0-13 (B3-N3 + B2-N8)

`x402SettlementNonces` 把 nonce 放在全局命名空间。匿名攻击者观察 mempool 后用相同 nonce 在不同 (asset, from) 上下文先提交一笔 dummy settle → 合法 settle revert "nonce used" → 匿名 DoS。

Defense

  • 新 `x402NonceKey(asset, from, nonce) public pure` helper(SDK 镜像编码)
  • `_validateX402AndComputeFee` 用三元组 hash 作为 key,每个 (asset, from) 隔离 nonce 空间
  • Storage 布局不变(仍是 `mapping(bytes32 => bool)`),仅写入的 bytes32 含义变 —— UUPS 升级安全

⚠️ SDK 影响

合并到 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() 应用折扣时没有链上事件,监控无法感知实际折扣使用情况。追加:

event AgentSponsorshipApplied(address indexed operator, address indexed user, uint256 bps);

在函数返回折扣值前 emit,参数包含 operator(运营商)、agent(折扣接受者)、bestBPS(实际折扣力度)。纯可观测性追加,无逻辑变更。414 tests pass.

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
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: 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".

Comment on lines +1151 to +1153
bytes32 key = x402NonceKey(asset, from, nonce);
if (x402SettlementNonces[key]) revert NonceAlreadyUsed();
x402SettlementNonces[key] = true;
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 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 👍 / 👎.

@fanhousanbu
Copy link
Copy Markdown
Contributor

代码审查

通过,含 HIGH 注意事项

Triple key 设计正确:使用 abi.encode(非 abi.encodePacked)防止哈希碰撞,两条入口路径(EIP-3009 和 Direct)均已更新,public helper x402NonceKey 设计可供 SDK 镜像编码。

HIGH — settlementId 计算方式与 nonce key 不对称
settlementId = keccak256(abi.encodePacked(from, to, asset, amount, nonce)) 使用了 abi.encodePacked,而 nonce key 使用 abi.encode。此外 settlementId 不包含 validAfter/validBefore,导致两笔参数相同(不同时间窗口)的交易产生相同 ID,可能导致链下去重误判。建议在后续 PR 中统一为 abi.encode,或在文档中明确说明 settlementId 不保证唯一性。

MEDIUM — 缺少跨路径(EIP-3009 vs Direct)nonce 共享验证测试
两条路径共享同一个 x402SettlementNonces mapping,但没有测试验证:EIP-3009 路径消费 nonce 后,相同 triple 在 Direct 路径被拒绝(反之亦然)。建议补充此测试。

INFO — 旧单 nonce key 在 storage 中留存
升级后旧 key(裸 nonce)仍在 mapping 中保留为 true,永远不会被新代码查询,属于可接受的存储孤岛,但在链上数据分析时需注意。

…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
@jhfnetboy
Copy link
Copy Markdown
Member Author

修复说明

问题settleX402PaymentsettleX402PaymentDirect 中的 settlementId 使用 abi.encodePacked,而 x402NonceKey() 使用 abi.encode,两者编码方式不一致。

修复:将两处 settlementId 赋值均改为 abi.encode

// Before:
settlementId = keccak256(abi.encodePacked(from, to, asset, amount, nonce));

// After:
settlementId = keccak256(abi.encode(from, to, asset, amount, nonce));

注意:虽然 from/to/asset(address)、amount(uint256)、nonce(bytes32)均为固定长度类型,当前不存在实际碰撞风险,但改为 abi.encode 可确保与 x402NonceKey() 的编码方式保持对称,同时防止未来添加变长类型时引入隐患。

已更新 NatSpec:两个函数的 @dev 注释均说明了编码方式的设计理由。

测试forge test --match-path "contracts/test/v3/SuperPaymasterV5*" — 36/36 通过,含 test_SettleEIP3009_Successtest_SettleDirect_Successtest_SettleEIP3009_Replaytest_SettleDirect_Replay

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.
@jhfnetboy
Copy link
Copy Markdown
Member Author

PR #106 补丁:遗留 nonce 重放防护 + 跨路径测试

Issue 1(P1):升级后遗留 raw-nonce 可被重放

根因分析:
P0-13 之前,x402SettlementNonces原始 nonce bytes32 值作为 key(x402SettlementNonces[nonce] = true)。P0-13 切换为三元组 key keccak256(asset, from, nonce) 后,旧槽位永远不会被新代码检查,导致升级前已结算的 tuple 可在升级后被再次执行。

修复方案:
_validateX402AndComputeFee 的三元组 key 检查之前,先检查旧格式 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 路径均通过 _validateX402AndComputeFee 分发,一处修改即覆盖两个入口。


Issue 2(Medium):新增跨路径 nonce 共享验证测试

新增 3 个测试(SuperPaymasterV5Features.t.sol):

测试名 验证内容
test_Nonce_CrossPath_EIP3009ThenDirectBlocked EIP-3009 消费的 nonce,Direct 路径同一 (asset, from, nonce) 必须 revert
test_Nonce_CrossPath_DirectThenEIP3009Blocked Direct 消费的 nonce,EIP-3009 路径必须 revert
test_Nonce_LegacyRawNonceReplayBlocked stdstore 写入旧格式 raw-nonce 槽,两条路径均应 revert

说明:两条路径都经过同一个 _validateX402AndComputeFee 函数,因此天然共享同一个 nonce namespace。跨路径测试验证的是 SuperPaymaster 层的 x402SettlementNonces 映射,与 ERC20 token 层的 EIP-3009 nonce 无关。


测试结果

Ran 39 tests for SuperPaymasterV5Features.t.sol — 39 passed, 0 failed
Ran 306 tests across all v3 suites — 306 passed, 0 failed

所有现有测试通过,无回归。

_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.
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