StealthOSAll current reports

Solana program & wallet integration

Adversarial review

SolanaGroth16Browser wallet

Review date: 1 October 2026

Current remediation status — 12 October 2026

Finding-status verification against staging source, dated remediation records and targeted regression tests. Not a full re-audit or blanket deployment attestation.

F1 — Definitive refusal becomes permanently pending

High · Remediated in source

Preserve the proven submissionNotAttempted refusal across HTTP; close only demonstrably unsigned intents.

Evidence / limits: Current Solana route/recovery checks and October 8 recovery-resolution evidence.

Remediation commits: privacy-frontend 618f64b · stealthos-relayer a629115

F2 — Recovery selects an unrelated PDA mention

High · Remediated in source

Search finalized history for the actual matching settlement event instead of treating the newest PDA mention as creation.

Evidence / limits: Current leaf-discovery and recovery-security regressions.

Remediation commits: privacy-frontend 618f64b · stealthos-relayer a629115

F3 — Zero-output withdrawals exhaust the journal

Medium · Remediated in source

Complete chain-validated withdrawals even when no output delivery is required.

Evidence / limits: October remediation regression record and current recovery-security checks.

Remediation commits: privacy-frontend 618f64b · stealthos-relayer a629115

F4 — Processed signature is retried with a fresh signature

Medium · Remediated in source

Retain the original signed attempt through uncertain submission; require canonical resolution before replacing it.

Evidence / limits: Current Solana settlement regressions, including response loss and retry fencing.

Remediation commits: privacy-frontend 618f64b · stealthos-relayer a629115

F5 — Settlement before durable note delivery

High · Remediated in source

Require acknowledged encrypted off-device journal backup before funded settlement; preserve delivery-pending state until recovery completes.

Evidence / limits: Current journal-backup regressions; later October 8 devnet cold-restore and funded top-up evidence.

Remediation commits: privacy-frontend 618f64b · stealthos-relayer a629115

Download current status for all 19 findings · Status data and checked revisions


Original dated report

The original text below is preserved as a historical record. Use the current-status addendum above for finding closure.

Download original dated source report (without current addendum)

Solana integration adversarial fund safety audit

Remediation update (1 October 2026): The source findings below have been addressed in local frontend and relayer worktrees. See the remediation record for changes, validation and remaining rollout requirements. This audit describes the pre-fix revision.

Audit completed 1 October 2026 UTC (30 September local time). This source audit found four implementation defects and one recovery design risk. The highest impact is permanent loss of access to newly created notes if delivery fails and the sole browser copy is subsequently lost. Other findings can block withdrawals or waste relayer funds. No unauthorized vault withdrawal, forged proof acceptance, or double spend was demonstrated.

Release assessment: address the note durability gap and recovery deadlocks before relying on this integration for real funds. Passing the existing tests does not resolve these findings.

Scope and evidence

The new Solana implementation is in the existing .codex-worktrees/solana-audit-fixes-* checkouts. The top-level checkouts do not contain the same integration. Findings apply to these exact source revisions:

ComponentCommit
privacy-protocol4c234495b256adc611dc332a86b284163d26aaa0
stealthos-relayer2d24a354b6a71e5743d4e0f1842978758572b947
privacy-frontendd7dadc7512d087b198b40a5bb3bd1511180b0404

Reviewed the pool's deposits, settlements, buffers, rollover and admin instructions; proof/public-input binding and relevant circuit constraints; SDK encoding; relayer validation, signing, fees and durable spend accounting; and frontend submission, confirmation, note storage and journal recovery. Production sources were not changed. Reproduction tests and logs are saved beside this report.

Severity distinguishes irreversible loss from recoverable loss of access. High findings warrant correction before a real-funds launch; this does not mean all three high findings permit theft.

IDSeverityFindingFund impact
F5HighSettlement precedes durable note delivery, and delivery failure still permits successConditional permanent loss of recipient/change/deposit note access
F1HighHTTP serialization drops the definitive refusal flagSelected funds can remain blocked in the app after a transaction was never submitted
F2HighRecovery treats the newest transaction mentioning a PDA as its creation transactionAn unrelated transaction can indefinitely block recovery and recipient delivery
F3MediumEmpty-delivery withdrawals never complete their journal records64 records exhaust the journal and block subsequent actions
F4MediumRelayer treats a processed signature as expired and signs a replacementDuplicate paid transactions; staging corruption/failure and excess relayer cost

F5 Settlement can succeed before the only recoverable note is durably delivered

Locations: delivery and error handling (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/solana-app/hooks/useSolanaNotes.ts:416), success status (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/solana-app/hooks/useSolanaActions.ts:373), browser journal persistence (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/journal.ts:212), random note generation (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/notes.ts:115).

The flow writes output notes into an encrypted browser journal, settles on chain, and then attempts inbox delivery. completeDeliveries catches a delivery failure, logs a warning, and resolves normally. The caller can display “Sent … privately.” Recipient notes use keepLocal: false, so the sender's ordinary note cache does not provide another copy. The sender's journal remains the recovery mechanism until remote delivery succeeds.

The note's random secret is freshly generated, not deterministically reconstructed from a wallet restore. The Solana data router (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/backend/engine-api/solana-data.js:100) also excludes the existing EVM recovery endpoints; the audited Solana submission path has no off-device, pre-submission journal backup.

Failure sequence: settle a private transfer; fail the note-store request; lose or clear the sender's browser storage before any successful retry. The recipient has no delivered secret, the sender has no journal, and the commitment cannot reconstruct that secret. Access to that output is then lost. Deposits and self/change outputs have the same browser failure domain while their remote delivery is pending.

Evidence: the F5 React-hook reproduction invokes the real delivery code with a finalized record and a failing store. It confirms normal return, an undelivered note retained only in the journal, no recipient note in the sender's note list, and no recoverable journal after storage loss. The test models a finalized settlement; it does not move money. Permanent fund inaccessibility is the consequence of losing the secret required to spend the committed note.

Limits: a delivery failure alone does not destroy funds. Reloading with local storage intact is recoverable. This finding requires loss of the remaining local secret before a successful retry.

Fix: durably back up encrypted output secrets off-device before submitting a transaction that commits their value. Use scoped sender recovery records and/or pending recipient envelopes with authenticated receipt and a safe finalization protocol. Return structured delivery status and show “settled, delivery pending” until durability is acknowledged. Do not treat the settlement itself as retryable merely because delivery failed.

F1 A definitive pre-signing refusal becomes a permanently pending client intent

Locations: HTTP error response (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/relayer/solana-pool-v2/route.js:111), service's definitive refusal flag (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/relayer/solana-pool-v2/service.js:404), client parsing (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/relay.ts:146), reconciliation (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/reconcile.ts:302).

The relayer service sets details.submissionNotAttempted = true when its durable record establishes that signing never occurred. The HTTP route omits details. Consequently, the browser cannot distinguish this explicit refusal from an ambiguous lost response and keeps the record active.

A changed fee requirement or an invalidated root can make the original intent unusable. Refresh replays that obsolete intent. A newly proved intent for the same nullifier set gets the existing journal record, and settlement rejects the resumed record (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/settle.ts:359). For a wallet dependent on that note selection, the app has no successful withdrawal path without journal repair or a change that makes the original intent acceptable again.

Evidence: the real HTTP route serializes a service error carrying the flag. A frontend test consumes that exact JSON through the real relay client, runs reconciliation, and proves that a replacement fee intent resumes the old fee. A control with the flag preserved correctly marks the refusal as confirmed failure.

Impact: funds remain on chain and cryptographically spendable; this is an application access lock, not theft or an irreversible on-chain freeze.

Fix: serialize only the validated, safe refusal boolean, and preserve the service's durable-history guard. Do not infer “not submitted” from HTTP status or expose arbitrary error details. Add an HTTP-to-client recovery test that permits a fresh proof after a genuinely unsigned refusal and forbids that release after any signing attempt.

F2 Any later transaction mentioning the nullifier can block lost-response recovery

Locations: newest-signature lookup (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/reconcile.ts:46), settlement recovery branch (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/reconcile.ts:244).

When the settlement response was lost and no signature was saved locally, recovery calls getSignaturesForAddress with limit: 1 and assumes that the newest successful transaction is the one that created the nullifier. The API actually returns transactions mentioning the address in their account keys, newest first. Solana RPC documentation.

A third party can include the public nullifier PDA in an unrelated transaction, such as a small lamport transfer. Recovery then selects that later transaction. Settlement-event validation properly rejects it, but the outer recovery handler leaves the record pending; every refresh selects the same unrelated transaction. Because the nullifier exists, this branch also bypasses the relayer replay path that could return the real settlement signature.

Evidence: the F2 test supplies a real encoded settlement event and matching commitment metadata followed by an unrelated successful transaction mentioning the nullifier. Two reconciliation attempts produce no recovered outputs or delivery work. Removing only the unrelated transaction lets the same record recover successfully.

Preconditions and impact: the legitimate settlement succeeded, its response/signature was lost, and a later transaction mentions its nullifier PDA. Inputs are spent, but the app cannot finalize or deliver outputs through this path. The sender journal still contains secrets, so correct-signature recovery can repair access. Undelivered recipient funds can remain inaccessible indefinitely; loss of the journal can combine with F5.

Fix: paginate transaction history and identify the actual pool settlement by authenticated program events, expected nullifiers, output commitments and relevant slot metadata. Treat a durable relayer signature as another candidate to validate. Never equate an arbitrary successful address mention with account creation. The deposit branch uses the same helper and should receive the same correction.

F3 Full withdrawals with no change permanently consume active journal slots

Locations: delivery loop (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/solana-app/hooks/useSolanaNotes.ts:346), withdrawal outputs (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/operations.ts:324), 64-record limit (.codex-worktrees/solana-audit-fixes-privacy-frontend/src/joinsplit-v2/solana/journal.ts:388).

A full withdrawal creates no output notes and therefore no deliveries. The only completion transition in completeDeliveries occurs inside its delivery loop via markSolanaJournalDeliveryCompleted. Empty arrays skip that transition, leaving successfully finalized withdrawals in an active state.

After 64 such withdrawals in the same scope, prepareSolanaJournalRecord rejects new operations. Refresh does not clear these already-successful records, so deposits, transfers and withdrawals requiring new records become unavailable.

Evidence: the F3 test creates 64 finalized, zero-output withdrawals and calls the real React hook's delivery completion for each. All 64 remain active, and preparing the next deposit fails with the capacity error.

Impact: completed withdrawals are not reversed or lost. Subsequent use of funds is blocked until records are repaired.

Fix: explicitly complete a chain-validated finalized record with no required deliveries, and migrate existing finalized empty records. Do not blindly complete prepared or broadcast records. Keep records with actual undelivered notes recoverable.

F4 Processed transactions can be incorrectly retried with new signatures

Locations: confirmation and expiry (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/relayer/solana-pool-v2/executor.js:69), fresh-signature retry (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/relayer/solana-pool-v2/executor.js:99), buffer append (.codex-worktrees/solana-audit-fixes-privacy-protocol/programs/solana-joinsplit-pool/src/instructions/buffer.rs:75), fee estimate (.codex-worktrees/solana-audit-fixes-stealthos-relayer/src/relayer/solana-pool-v2/fees.js:71).

The relayer checks block height at confirmed commitment. If it exceeds lastValidBlockHeight, a second status lookup that still reports processed is treated as landed: false. sendStep then signs the same instructions with a new blockhash, even though the first signature was observed and may still finalize. This is not a safe proof of non-execution. See Solana's confirmation guidance.

Staging writes append bytes, so executing a replacement is not generally idempotent: it can duplicate a chunk or fail an overflow/payload check. Additional successful or failed transactions also cost fees, while the estimate counts one transaction per planned step.

Evidence: one controlled RPC test returns processed status both before and after the height check, then finalized for the original signature. The executor prematurely returns landed: false. A second test exercises sendStep and records two distinct signed transactions for the same instruction while the first remains processed.

Impact and limits: the duplicate-signing behavior is directly reproduced. Staging damage and excess fees follow from non-idempotent appends and paid retries; no real network fee was spent in the test. On-chain nullifiers protect against successful double settlement, so this is not evidence of a double withdrawal. Retry counts and spend controls constrain exposure.

Fix: use finalized height for definitive expiry, and do not classify a still-observed processed signature as absent. Reconcile its final outcome before rebuilding. Persist the locally known signature before network submission; verify step effects before repeating non-idempotent instructions. Budget all possible paid attempts. The frontend confirmation logic is a useful reference for stricter expiry handling.

Validation performed

SuiteResultEvidence
Existing frontend tests1,094 passed across 148 suitesfrontend-baseline.log (audit-reports/solana-adversarial-20261001/frontend-baseline.log)
Existing Solana relayer/backend tests35 passed, 1 skippedrelayer-baseline.log (audit-reports/solana-adversarial-20261001/relayer-baseline.log)
SDK Solana encoding, pool and verification32 passedsdk-tests.log (audit-reports/solana-adversarial-20261001/sdk-tests.log)
Native Solana program tests30 passedpool-wsl.log (audit-reports/solana-adversarial-20261001/pool-wsl.log)
New frontend reproductions4 passedfrontend-reproductions.log (audit-reports/solana-adversarial-20261001/frontend-reproductions.log)
New relayer reproductions3 passedrelayer-reproductions.log (audit-reports/solana-adversarial-20261001/relayer-reproductions.log)

The new tests deliberately assert the defective behavior; their passing results confirm reproduction, not remediation. The hook log contains React act warnings from asynchronous initialization. Assertions still completed successfully.

The skipped test requires a real PostgreSQL configuration and covers HTTP delivery persistence and namespace isolation. The first native Windows Rust build could not link because MSVC link.exe was unavailable; the native tests subsequently passed in WSL. Those tests use the unsafe-test-setup-keys feature and native program testing, not a production SBF deployment.

Reproduce the findings

Run run-reproductions.ps1 (audit-reports/solana-adversarial-20261001/run-reproductions.ps1) in PowerShell. It uses the existing worktrees and dependencies, runs the relayer reproduction to generate the HTTP response fixture, temporarily installs the two frontend audit tests, and removes only those unchanged copies afterward.

Sources: relayer reproductions (audit-reports/solana-adversarial-20261001/relayer-reproductions.test.mjs), frontend recovery reproductions (audit-reports/solana-adversarial-20261001/audit-20261001-recovery.test.ts), frontend delivery reproductions (audit-reports/solana-adversarial-20261001/audit-20261001-delivery.test.tsx).

Existing-suite commands, from the corresponding worktrees:

Relayer:
node --test src/test/solana-pool-v2-config.test.js src/test/solana-pool-v2-settlement.test.js src/test/solana-pool-v2-route.test.js src/test/solana-devnet-startup.test.js src/test/engine-solana-data.test.js

Frontend (PowerShell):
$env:CI='true'
npm test -- --watch=false --runInBand --testMatch '**/src/**/*.{spec,test}.{js,jsx,ts,tsx}' --testPathPattern=solana

Protocol SDK:
node --test packages/sdk/test/solana-encoding.test.mjs packages/sdk/test/solana-pool.test.mjs packages/sdk/test/solana-verify.test.mjs

Protocol native tests (WSL, from protocol worktree):
cargo test --locked --manifest-path programs/Cargo.toml -p stealthos-solana-joinsplit-pool --features unsafe-test-setup-keys

The frontend checkout path itself includes “solana,” so that test-path filter selected the entire frontend suite. The WSL run used a separate existing build cache.

Boundaries of the conclusion

The reviewed pool includes proof binding for payout/action fields, nullifier and commitment reuse protection, exact SPL deposit balance checks, restrictive Token-2022 extension handling, and authority checks. No bypass was demonstrated in this audit. These observations do not constitute a proof of soundness or a blanket security approval.

This was a source review with local tests and controlled RPC/storage faults. No production transactions were submitted. Deployed bytecode, upgrade authority custody, production proving-key ceremony provenance, live balances, operator configuration and a real PostgreSQL deployment were not verified. The configured frontend surface is Solana devnet. Findings therefore describe the audited integration's behavior and its implications for any future real-funds use, not an assertion that a live mainnet vault is currently exposed.