This is a source/test review by the implementing agent, not an independent audit. Scope: local router, interfaces/config, unit/fuzz/invariant/fork tests, deployment scripts, frontend validation/signing/proxy. Upstream protocol code was inspected for integration behaviour; not comprehensively re-audited.
Findings and fixes
| ID | Severity | Affected location | Preconditions / attack or failure path | Impact | Proof / fix |
|---|---|---|---|---|---|
| A01 | Medium, fixed | src/IMDUniversalPaymentRouter.sol:129 | A manager integration returning a settled amount different from value was not explicitly checked | Accounting assumptions could be silently weakened by an incompatible manager | Slither unused-return; now require settlement return equals spent. testSettlementMismatchAtomic proves rollback |
| A02 | Medium, fixed | frontend/lib/payment.ts:84 | Current x402 core rejects non-default assets unless opted in; initial integration followed documentation example without that addition | A user could acquire IMD but be unable to start payment | SDK sequence test initially failed; add exact IMD allowedAssets entry with quote amount cap. SDK sequence test now passes |
| A03 | Medium, fixed | frontend/app/page.tsx:283 and pay():441 | API prepares/normalizes input after the user's draft | Signing a different prepared action without informed review, or blocking all normalized actions | Fetch initial challenge, save/display prepared input, require review checkbox; subsequent challenge input must equal reviewed canonical input |
| A04 | Medium, mitigated | frontend/lib/server.ts:6; app/api routes | Public unauthenticated proxy requests consume upstream quota | Availability/resource exhaustion | Fixed destinations, allowlists, timeout, 64-KiB bound, batch cap and 120/min process budget. Public hosting still requires edge/global rate controls; request-budget denial is an accepted availability tradeoff |
| A05 | Low, fixed in tests | test/IMDUniversalPaymentRouterFork.t.sol:22 | Deterministic test deployment address already has ETH in real chain snapshot | Incorrect balance-equals-zero assertion masked otherwise valid fork purchase | Record pre-existing balance and prove each purchase preserves it. No production refund code was changed to sweep that ETH |
Analyzer findings with High/Medium labels
Initial 66 reports included the fixed A01. Final 65 reports are fully listed in STATIC_ANALYSIS.md. The refund warning is High in the tool but false positive after accounting/reentry review. Two High shift and 22 Medium rounding warnings are in pinned upstream functions not invoked by router. They remain visible; no suppression configuration was added.
The refund target cannot be set by arbitrary calldata: it is msg.sender. Spending and refund total msg.value; nonReentrant prevents nested purchases. Forced ETH is excluded. Regression tests include rejected refunds, reentrancy, sequential users, partial output, reported-but-not-delivered output and max-input rollback.
Attack-surface review
| Area | Evidence / conclusion |
|---|---|
| Wrong PoolKey / manager | Fixed verified fields; constructor compares hook getters and independently hashed key/id; fork uses actual deployments |
| Hook exact-output assumptions | afterSwap returns zero delta, flags forbid return-delta adjustments; source bytecode matches chain; real Quoter and actual purchase agree in fork |
| ETH accounting / forced ETH | No balance sweep, no arbitrary recipient refund, funding exactly max; baseline preserved in real fork and stateful tests |
| WETH transformations | None in V1; native ETH pool verified |
| Exact-output / partial liquidity | Positive amountSpecified; output delta equality then recipient balance delta; partial fill mock reverts; max failure fork has no partial delivery |
| Slippage / sandwich | Fixed max spend, deadline and preflight simulation; adverse execution within chosen tolerance remains possible |
| Signed casts / integer limits | int128 maximum output bound, positive output before cast, widen input before negation; uint256 subtraction only after amount checks |
| Callback / reentrancy | Fixed manager + active purchase + context hash; one-shot deletion; altered/replayed/missing callbacks and malicious refund receiver tested |
| Recipient contracts | Supported by Solidity; can reject refund if also caller, atomically reverting. Standard IMD has no transfer receiver callback. UI payment signing supports EOA only |
| Token balance assumptions | Actual inherited ERC20 transfer inspected; not tax/rebase token. Bridge/operator compromise remains outside scope |
| Denial of service | Rejected refund affects caller's operation; hook owner can remove liquidity; frontend proxies bounded but public edge limits required |
| Malicious frontend parameters | Solidity enforces amounts/deadline/fixed path. User still must trust displayed destination and wallet confirmation; frontend cannot override wallet consent |
| Owner/admin / approvals | None in router. x402 allowance separate, exactly quote amount, official Permit2 only; SDK unlimited helper not used |
| x402 / quote manipulation | Same account, chain/token/destination/quote/resource binding, canonical hash, strict shape, checked Permit2 domain/types/spender and expiry before signing |
| Stale capabilities | Refetch at quote creation; compare current price; saved quote/challenge governs payment; expired quote rejected. Stale swap estimate >30s rejected |
| Replay / retry | Preserve exact signed payload for same order/account, fetch status first; never automatically create fresh permit on retry |
| Server key custody | None; browser wallet signs, RPC method allowlist excludes broadcast. Secrets not logged by application code |
Verification after fixes
Foundry 48 passing aggregate tests; this includes 35 unit, 3 fuzz, one aggregate invariant campaign with two properties, and 9 fork tests. Fork fuzz: 256 samples. Invariant campaign: 128 sequences / 4096 handler calls / 0 reverts. Frontend 19 tests including actual pinned SDK flow with a fake signer and proxy rejection cases. Typecheck/build pass. See evidence logs; initial failure logs are diagnostic history, not final status.
No confirmed unmitigated Critical/High code finding identified in this self-review. This is not proof of absence. Mainnet deployment, human-wallet acceptance and live paid settlement were not performed. External market owner powers, bridge trust, MEV, frontend hosting integrity and API availability remain material limitations.