feat(svm): add simulation-based smart wallet verification#1527
feat(svm): add simulation-based smart wallet verification#1527BranchManager69 wants to merge 9 commits intox402-foundation:mainfrom
Conversation
🟡 Heimdall Review Status
|
|
@BranchManager69 is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
9bfa824 to
870b3c6
Compare
|
This approach is intentionally outcome-based rather than wallet-parser-based. The verifier does not trust arbitrary wallet programs blindly; it only accepts transactions that (1) never reference the facilitator fee payer in instruction accounts, (2) stay within bounded compute/priority-fee limits, (3) simulate successfully, and (4) produce exactly one TransferChecked matching the required mint, destination ATA, and exact amount. Standard wallets remain on the existing static verification path unchanged; this is only a fallback for smart-wallet transaction shapes the current verifier cannot parse structurally. |
3a7b6b6 to
27e3e6d
Compare
|
Nice to see this direction getting an upstream implementation. This aligns closely with the outcome-based verification model I proposed in #646 back in November - same two-path architecture, fee payer isolation, simulation with We've been running this approach in production (facilitator.cascade.fyi) and wanted to flag two bugs you'll likely hit: 1. Solana RPC returns When you call {
"parsed": {
"type": "transferChecked",
"info": { "mint": "...", "destination": "...", "authority": "...", "tokenAmount": { "amount": "10000" } }
},
"program": "spl-token",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}There's no You need to handle both formats: parsed (read from 2. Raw inner instructions use base58, not base64 The raw format Both confirmed against real Squads Multisig v4 vault transactions on mainnet. We have regression tests covering parsed inner instruction handling if useful as reference. One design note: your PR requires exact amount match, while #646 specifies |
63e0f0d to
6948c57
Compare
a9ee98e to
10601a6
Compare
|
Great work on this PR — we like the outcome-based verification direction and think simulation + CPI trace inspection is the right architecture for supporting smart wallets without per-wallet parsers. We've done a security review and have some feedback. +1 to @tenequm's comment on jsonParsed vs raw format handling — worth verifying against real Squads v4 vault transactions. Security Invariants
Recommendations1. Post-Settlement Verification ✅Commit 2. Address Lookup Table Blindness (must fix)
Wallet providers like Crossmint use ALTs when constructing transactions, so rejecting ALTs outright would limit smart wallet coverage. We recommend resolving them — 3. Program Allowlist (should have)Path 2 currently allows any program to execute. An allowlist for the top-level non-ComputeBudget instructions (e.g., Squads, Swig) constrains the surface to audited programs. 4. Relax "Exactly One" Transfer (should have)Some smart wallets bundle ATA creation with the payment, producing multiple transfers. Recommend relaxing to "at least one transfer matching the full required amount." Summary
|
f25b533 to
b0af04c
Compare
|
Thanks for the detailed feedback and the production validation from Cascade. This aligns well with what we found during development. A few responses: 1 & 2: jsonParsed format + base58 encoding — Good catches — these are exactly the traps we ran into during development. Both are handled in the current code:
Both paths were confirmed against real Squads Multisig v4 Overpayment tolerance: Changed Would welcome any reference to your regression tests for parsed inner instruction handling if you're open to sharing. |
|
Thanks for the thorough security review and the invariant framework — it's exactly the right way to think about this. Responses to each recommendation: 1. Post-Settlement Verification ✅ — Implemented. Primary path fetches confirmed transaction inner instructions via 2. Address Lookup Table Blindness ✅ — Fixed. 3. Program Allowlist ✅ — Agreed, this is a good additional layer. Added an operator-configurable allowlist with sensible defaults (Squads Multisig v4, Squads Smart Account, Swig, SPL Governance). Transactions invoking programs not in the list are rejected with 4. Relax "Exactly One" Transfer — We checked the CPI patterns for Squads Multisig v4, Squads Smart Account ( All changes are in the latest commits. Happy to discuss any of these further. |
b0af04c to
b9678c8
Compare
4a3ac7b to
40b993f
Compare
|
@BranchManager69 any update guys ? :) |
ab5a89c to
0ee2470
Compare
|
@tenequm rebased past #1688 and mirrored your memo enforcement onto Path 2, so smart wallet payments can't bypass a seller-required memo. Same error codes as your Step 5b, single top-level instruction shape, covered end-to-end in |
|
Thanks @BranchManager69 nice work! Looks good to me and excited to have this supported! @phdargen @CarsonRoscoe could you please take a look at this when you get a chance? |
|
This PR supports only TS, is it normal ? It should support all language right Go and Python ? |
Yes it should support all. Would like to get some input from the maintainers first before more work is put into it |
|
@phdargen @CarsonRoscoe can you check this PR ? this is essential for Solana. |
|
Could you please also add |
|
Swig as well! |
Adds wallet-agnostic smart wallet verification to the SVM facilitator. Verifies payment outcomes by inspecting CPI inner instructions from transaction simulation, rather than parsing each wallet type's proprietary instruction format. Works for any smart wallet program (Squads, Swig, SPL Governance, etc.) that executes TransferChecked via CPI. No per-wallet code required. Security model: - Fee payer isolation (fee payer never in instruction accounts) - Operator-configurable compute budget caps - Exactly one matching TransferChecked in CPI trace - Simulation proves transaction viability Made-with: Cursor
After confirmTransaction on Path 2 (smart wallet) settlements, verify the TransferChecked actually executed on-chain before returning success. Primary: fetch confirmed transaction inner instructions via getTransaction. Fallback: balance-delta check on destination ATA (no indexing lag). Closes the TOCTOU gap where a malicious program could pass simulation but skip the transfer during on-chain execution. New optional signer methods: - getConfirmedTransactionInnerInstructions - getTokenAccountBalance 5 new test cases covering both verification paths and failure modes. Made-with: Cursor
- assertFeePayerIsolated now resolves Address Lookup Tables before checking, catching fee payer hidden in ALT-resolved accounts - Constructor requires all 3 signer methods when smart wallets enabled (simulateWithInnerInstructions, getConfirmedTransaction, getTokenBalance) - Changed === to >= for overpayment tolerance (per RFC x402-foundation#646) - Removed silent "unverified" fallthrough in post-settlement verification - New optional fetchAddressLookupTables signer method - 8 new tests: ALT handling, constructor enforcement, overpayment, double RPC failure Made-with: Cursor
Operator-configurable allowlist gates which programs can reach simulation-based verification. Prevents custom malicious programs from exploiting the simulation path entirely. Default: Squads Multisig v4, Squads Smart Account, Swig, SPL Governance. Override via smartWalletAllowedPrograms in ExactSvmSchemeOptions. Unknown programs rejected with smart_wallet_program_not_allowed. Made-with: Cursor
…ests Address review feedback from notorious-d-e-v (second review, Mar 23): Token-2022 post-settlement fallback: - Balance-delta fallback now derives ATAs for both SPL Token and Token-2022 - scheme.ts captures balanceBefore from whichever token program has a live ATA - Passes token program hint to verifyPostSettlement for optimized lookup - When all balance checks throw, correctly returns "unverified" not "balanceDelta" sigVerify/replaceRecentBlockhash alignment: - Changed smart wallet simulation from sigVerify:false/replaceRecentBlockhash:true to sigVerify:true/replaceRecentBlockhash:false, matching existing Path 1 behavior - Closes forged-signature attack vector identified in review - Correct for future durable nonce support per RFC x402-foundation#646 Tests: - Token-2022 fallback when SPL Token ATA shows no delta - Hinted token program optimization in balance fallback
Path 1 (upstream x402-foundation#1688) verifies that a seller-required extra.memo is present as exactly one top-level Memo instruction with matching content. Path 2 previously ignored memo instructions entirely, so smart wallet transactions silently bypassed the seller's memo requirement. This commit brings Path 2 to feature parity with Path 1: - scheme.ts: exempt Memo program from the wallet-program allowlist. The allowlist exists to restrict which wallet programs can reach simulation; memo is a standalone system-category program with no security surface, same as ComputeBudget. Leaving it subject to the allowlist meant smart wallet users could not include a memo at all, even when the seller required one. - smartWalletVerification.ts: add Step 4a verifying memo count and content against requirements.extra.memo, mirroring Path 1's Step 5b semantics and returning the same error codes (invalid_exact_svm_payload_memo_count, invalid_exact_svm_payload_memo_mismatch). Tests cover the four memo paths end-to-end through ExactSvmScheme.verify: required memo present-and-matching (accept), missing (reject_count), mismatched content (reject_mismatch), duplicated (reject_count).
Per request from @blockiosaurus on the PR — Metaplex's agent identity platform routes agentic execution through the Metaplex Core program PDA (assetSignerPda). Verified end-to-end against mainnet through a real $0.02 USDC payment for the live x402 endpoint at mcp.cryptoiz.org/btc/regime. The on-chain transaction shape is exactly what Path 2 was designed for: top-level Metaplex Core `execute` (unknown to static parsing), inner SPL TransferChecked matching the merchant's payment requirements. extractTransfersFromInnerInstructions matches the inner transfer, simulation passes, settlement lands. Settlement tx: 5g5B3ZPHndS19tzUjPANgFC7J3nS8rT6dAFSuZ21bDfRHSSREKTbnN7XrYVp5haBmj4xDq2gD5A375evHq5y3vw6
0ee2470 to
bc7a9bd
Compare
|
Added Metaplex Core ( @romeo4934, Swig ( |
Templates were reverted to upstream's version during rebase; CI's check-paywall-template caught the drift. Regenerated locally via pnpm --filter @x402/paywall run build:paywall. AVM template, Go, and Python equivalents already match upstream.
|
no all good ;) |
Description
Adds wallet-agnostic smart wallet verification to the SVM facilitator (TypeScript). Verifies payment outcomes by inspecting top-level instructions and the CPI trace from transaction simulation, rather than parsing each wallet type's proprietary instruction format.
Works for any smart wallet program (Squads, Swig, SPL Governance, Crossmint, future wallets) that executes
TransferCheckedvia CPI. No per-wallet code required. This approach has been running in production at Dexter and is the first implementation to verify and settle smart wallet transactions on-chain.Motivation
The existing SVM facilitator rejects any transaction that doesn't match the exact 3-6 instruction layout (ComputeLimit + ComputePrice + TransferChecked + optional Lighthouse/Memo). Smart wallet programs wrap transfers inside their own instructions (e.g., Squads
vaultTransactionExecute, SwigSignV2), so the facilitator sees an unknown program at position [2] and rejects withno_transfer_instruction.This is a fundamental limitation of positional instruction validation. Every smart wallet has a different instruction format, but they all do the same thing: execute a
TransferCheckedvia CPI.Approach
ExactSvmScheme.verify()now has two paths:Path 1 (Static): Existing positional instruction validation for standard wallets. Unchanged. Standard wallets keep this fast path; simulation is only a fallback for transactions the current verifier cannot structurally understand.
Path 2 (Simulation): When Path 1 rejects and
enableSmartWalletVerificationis set:innerInstructions: true: the facilitator already callssimulateTransactionas its final verification step. Path 2 uses the same RPC method withinnerInstructions: trueto get the full CPI trace. No additional round-trip.TransferCheckedacross top-level instructions and the CPI trace, matching payment requirements (correct mint, destination ATA derived frompayTo, amount exactly equals required).flowchart TD TX["Transaction arrives"] --> DECODE["Decode"] DECODE --> P1{"Path 1: Static layout check"} P1 -->|"Layout matches"| SIM1["Sign + Simulate"] SIM1 --> PASS1["VERIFIED"] P1 -->|"Unknown program / wrong layout"| SW{"Smart wallet enabled?"} SW -->|"No"| REJECT["REJECTED"] SW -->|"Yes"| FPI{"Fee payer isolated?"} FPI -->|"No"| REJECT FPI -->|"Yes"| CB{"Compute budget OK?"} CB -->|"No"| REJECT CB -->|"Yes"| SIM2["Simulate + innerInstructions"] SIM2 -->|"Fails"| REJECT SIM2 -->|"Succeeds"| EXTRACT["Extract TransferChecked"] EXTRACT --> MATCH{"Exactly 1 match?"} MATCH -->|"No"| REJECT MATCH -->|"Yes"| PASS2["VERIFIED"]Security model
payTo+ mint, matched against observed transfersTransferCheckedchecked against requirementsTransferCheckedchecked against requirementsProduction data
Tested against ecosystem facilitators using a real Squads multisig vault transaction, both verify and settle. No other facilitator currently passes.
Changes
svm/src/signer.tssimulateTransactionWithInnerInstructionstoFacilitatorSvmSigner; implement in default factorysvm/src/exact/facilitator/scheme.tsExactSvmSchemeOptionswith smart wallet config; refactorverify()into dual-path (static + simulation)svm/src/exact/facilitator/smartWalletVerification.tsassertFeePayerIsolated,validateComputeBudgetLimits,extractTransfersFromInnerInstructions,verifySmartWalletTransactionsvm/src/index.tssvm/test/unit/smartWalletVerification.test.tssvm/test/unit/smartWalletFallback.test.tsGo and Python implementations are left for follow-up. This PR is intentionally TypeScript-only to validate the upstream architecture first. If maintainers agree with outcome-based verification as the direction, Go/Python parity can follow without replicating wallet-specific parsers across all SDKs. This PR targets non-ALT smart-wallet transaction shapes first; ALT-aware inner instruction resolution can follow with concrete test cases.
Usage
Tests
Checklist