Skip to content

fix(security): C-02+C-03 — require payer EIP-712 authorization for x402 settlement#228

Open
jhfnetboy wants to merge 10 commits into
mainfrom
sec/c02-c03-x402-auth
Open

fix(security): C-02+C-03 — require payer EIP-712 authorization for x402 settlement#228
jhfnetboy wants to merge 10 commits into
mainfrom
sec/c02-c03-x402-auth

Conversation

@jhfnetboy
Copy link
Copy Markdown
Member

Stacked PR 3/4 (merge after C-01). Base: sec/c01-credit-ceiling. Fixes C-02 (unsigned xPNTs drain via auto-allowance) + C-03 (recipient redirect): payer EIP-712 X402PaymentAuthorization verified at SP (SignatureCheckerLib, EOA+ERC-1271); USDC EIP-3009 path binds recipient via nonce=keccak256(to,salt). Codex-approved for mainnet. SDK: aastar-sdk#39. forge test 969 green.

jhfnetboy added 6 commits June 1, 2026 16:27
…02 settlement

C-02 (settleX402PaymentDirect, xPNTs): the direct path pulled a victim's xPNTs via
SuperPaymaster's auto-allowance with NO payer signature — any community-approved
facilitator could drain any holder to a caller-chosen recipient. Now requires the
payer's EIP-712 X402PaymentAuthorization (from,to,asset,amount,maxFee,validBefore,nonce),
verified via SignatureCheckerLib (EOA + ERC-1271 / AirAccount passkey). Binds the
recipient, caps the fee (maxFee), and expires (validBefore).

C-03 (settleX402Payment, EIP-3009 USDC): the final recipient 'to' was an unsigned
parameter, so a facilitator could redirect funds the payer authorized only to the SP.
Now the recipient is bound into the EIP-3009 nonce (nonce = keccak256(to, salt)); a
swapped 'to' yields a different nonce and the payer's EIP-3009 signature no longer
recovers — the transfer reverts. Reuses the existing token-level signature; no second sig.

- New: X402_AUTH_TYPEHASH, _x402DomainSeparator (proxy-safe, recomputed per call),
  _verifyX402Auth; errors InvalidX402Signature / X402AuthExpired / X402FeeExceedsMax.
- ISuperPaymaster updated to the new signatures.
- PoC_C02/PoC_C03 rewritten as fix regressions (no-sig drain reverts; redirect reverts;
  valid-signature happy paths pass). 6 x402 test files updated for the new ABI.

forge test 969 passed / 0 failed. SuperPaymaster 24,093 bytes (EIP-170 OK, 483 spare).
…ed settlement

ABI/nonce semantics changed in commit d7df0c3; bring every in-repo caller onto the
new protocol so none silently uses the old one (flagged by review gate):

- abis/SuperPaymaster.json regenerated (settleX402PaymentDirect gains maxFee/validBefore/
  signature; settleX402Payment nonce → salt).
- packages/x402-facilitator-node (DEPRECATED — superseded by @aastar/x402 SDK): ABI +
  call args updated; the payer-signature / salt signing flow is tracked in the SDK,
  see AAStarCommunity/aastar-sdk#39.
- script/gasless-tests/{test-helpers.js,test-x402-eip3009-settlement.js}: ABI updated;
  EIP-3009 signing step marked PENDING aastar-sdk#39 (needs nonce = keccak256(to, salt)).

Canonical client integration is the SDK (issue #39 filed with full spec). No contract
or Solidity-test change here.
…yer x402 paths confirmed separate at TA layer
…eTxLimit)

retryPendingDebt always retried the FULL pending balance, so once the accumulator
exceeded the token's per-tx limit (5000 aPNTs) recordDebt reverted forever and the
debt could only be written off via clearPendingDebt. Now takes an explicit
(clamped to the balance) so a large balance is recovered in chunks over multiple
owner calls. Reuses recordDebt + the existing pendingDebts accumulator — no new state.

- Test: test_RetryPendingDebt_Chunked (accumulate → drain in 2 chunks → empty reverts).
- Ops runbook: docs/operations/pending-debt-runbook.md (management-backend retry rule).

forge test 970 passed / 0. SuperPaymaster 24,159 bytes (EIP-170 OK, 417 spare).
Stale callers used the old retryPendingDebt(address,address) selector after H-01 added
the chunk `amount` param. test-group-B5 now passes amount=0 (full balance) and the
ABI JSON is regenerated. Flagged by the stop-time review gate.
@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner June 1, 2026 12:56
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: cd082a7d08

ℹ️ 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".

// payment payload once the SDK produces them; the fallbacks below keep the deprecated
// in-repo node type-correct against the new ABI but are NOT a substitute for the SDK.
const maxFee = (body.payment as { maxFee?: string | number | bigint }).maxFee;
const salt = (body.payment as { salt?: string }).salt ?? nonce;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require a real salt instead of reusing the nonce

For EIP-3009 requests that follow the current in-repo API shape, payment has only nonce and /verify still validates the token signature against that raw nonce, but this fallback passes the same raw nonce as salt to the new contract ABI. The contract then calls transferWithAuthorization with keccak256(abi.encode(to, salt)), so any request without an explicit SDK-produced salt will verify successfully and then revert during settlement because the signature covers a different nonce. Reject missing salt or derive/verify the recipient-bound nonce consistently before settling.

Useful? React with 👍 / 👎.

fanhousanbu
fanhousanbu previously approved these changes Jun 1, 2026
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.

✅ APPROVE — C-02+C-03 x402 payer EIP-712 auth

C-02 (settleX402PaymentDirect) — payer authorization:

  • Signature check BEFORE nonce consumption: prevents an attacker from pre-burning a victim's nonce without a valid authorization. ✅
  • block.timestamp > validBefore checked before _verifyX402Auth to fail fast without the pairing cost. ✅
  • fee > maxFee cap enforced after fee computation — operator cannot overcharge beyond what the payer signed. ✅
  • SignatureCheckerLib.isValidSignatureNowCalldata accepts EOA + ERC-1271 (AirAccount passkey / smart-account). ✅

EIP-712 domain separator:

function _x402DomainSeparator() internal view returns (bytes32) {
    ...keccak256(..., address(this))...
}

Recomputed on each call (not cached at impl construction). Correct — caching in the implementation constructor would bind to the impl address, not the proxy. Gas overhead per settlement is acceptable. ✅

TypeHash:
"X402PaymentAuthorization(address from,address to,address asset,uint256 amount,uint256 maxFee,uint256 validBefore,bytes32 nonce)" — matches the _verifyX402Auth parameters. Domain name "SuperPaymaster" / version "1" aligns with what the KMS team encodes in SignX402Payment (PR #12, reviewed earlier). ✅

Note: No validAfter field in the authorization — the auth is valid from t=0 to validBefore. This is intentional (immediate settlement, not a delegated future authorization). Acceptable design choice.

C-03 (settleX402Payment / EIP-3009 path) — recipient binding:

// C-03: nonce = keccak256(abi.encode(to, salt))
bytes32 nonce = keccak256(abi.encode(to, salt));

Binding the recipient into the EIP-3009 nonce is elegant: swapping to changes the nonce, causing transferWithAuthorization to revert (different auth hash, existing sig doesn't recover from). No second signature required. ✅

One note for callers: the parameter rename from nonce to salt in the public API is a breaking change for any existing integrators calling settleX402Payment. The ABI sync + JS/TS callers in the PR cover the in-repo surfaces; downstream integrators should be notified.

@jhfnetboy jhfnetboy changed the base branch from sec/c01-credit-ceiling to main June 1, 2026 13:11
@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 1, 2026 13:11

The base branch was changed.

fix(security): H-01 — chunked retryPendingDebt + audit findings docs
fanhousanbu
fanhousanbu previously approved these changes Jun 1, 2026
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.

Re-approve after retarget — C-02+C-03 x402 payer EIP-712 auth fix unchanged, previously reviewed and approved.

@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 1, 2026 13:23

The merge-base changed after approval.

fanhousanbu
fanhousanbu previously approved these changes Jun 1, 2026
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.

Re-approve after H-01 (#227) merge into branch. The new commits are the retryPendingDebt 3-arg caller updates + ABI regen reviewed in PR #227 — no new security surface. C-02/C-03 x402 auth fix unchanged.

@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 1, 2026 13:52

The merge-base changed after approval.

fanhousanbu
fanhousanbu previously approved these changes Jun 1, 2026
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.

Re-approve: C-02/C-03 + H-01 fix confirmed, ready to merge.

@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 1, 2026 15:45

The merge-base changed after approval.

fanhousanbu
fanhousanbu previously approved these changes Jun 2, 2026
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.

Re-approve after latest dismiss — C-02/C-03 x402 auth fix unchanged, previously reviewed. Ready to merge.

@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 2, 2026 01:31

The merge-base changed after approval.

fanhousanbu
fanhousanbu previously approved these changes Jun 2, 2026
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.

Re-approve C-02/C-03.

@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review June 2, 2026 02:04

The merge-base changed after approval.

jhfnetboy added 3 commits June 2, 2026 09:16
…70 to deployable

The two red checks on this PR were both false alarms, not real issues:

- Stage 3 (Slither, fail-on:high): the only High was 'arbitrary-send-erc20' — transferFrom
  with a parameter `from` — flagged in 3 spots that are ALL guarded (GTokenStaking x2 via
  Registry access control; settleX402PaymentDirect via the C-02 EIP-712 payer signature,
  which is literally what authorizes `from`). Slither can't see the guards. Excluded the
  detector in slither.config.json; src now has 0 High. Rationale: docs/security/slither-triage.md.
- Stage 1 (EIP-170): `forge build --sizes` failed on SuperPaymasterV2Reinit, a test-only
  UUPS-reinit helper that is never deployed. Scoped to deployable contracts with forge's
  documented `--skip test --skip script` (same fix as #230). Real SuperPaymaster = 24,159 B.

No contract code change. Verified locally: slither High = 0; forge build --sizes (deployable)
exits 0.
…lly (Codex review)

Codex flagged the global `detectors_to_exclude` as weakening the gate (it would hide any
future unguarded transferFrom). Reverted the global exclusion; instead annotate each of the
3 known-safe sites with an inline // slither-disable-next-line + justification, so the
detector stays fully active and any NEW occurrence still trips fail-on:high:
- GTokenStaking.lockStakeWithTicket (x2) / topUpStake: onlyRegistry, Registry-supplied payer.
- SuperPaymaster.settleX402PaymentDirect: guarded by the C-02 EIP-712 payer signature
  (_verifyX402Auth) immediately above the transfer.

Comments only — no bytecode change. Verified: forge build OK; slither High = 0;
slither.config.json no longer excludes the detector. docs/security/slither-triage.md updated.
# Conflicts:
#	.github/workflows/security.yml
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