May 29, 2026
Full-stack hardening pass — fund-flow, trust, mobile, Phantom Blowfish, dependency vulns
Field-reported on a live mainnet sweep: the success card displayed nonsense numbers (660,500 SOLSCORE for a 660-score wallet; 6,830,544 SOL reclaimed for 0.068 SOL of lifetime reclaim) AND Phantom showed a 'malicious dApp' warning on every signing prompt. Spent the night running five parallel adversarial audits across fund-flow safety, security, type-confusion bugs, UX/accessibility, and Phantom Blowfish reputation. Shipped 31 fixes across 19 files in priority order. Nothing here is cosmetic — every bullet either closes a fund-flow risk, removes a trust-killing UI lie, or drops a vulnerability.
- ·Money math: BIGINT columns returned as strings by the Neon driver are now normalized to JavaScript numbers at the data-layer boundary (db.ts toNum + normalizeNumericCols) so client `+` arithmetic adds instead of concatenates. Throws loudly if any value ever exceeds Number.MAX_SAFE_INTEGER. The 660,500 and 6,830,544 display bugs both root-caused here.
- ·Money math: the SystemProgram fee transfer in signed transactions is now read as BigInt (capped at 1000 SOL) instead of Number — closes a 9-quadrillion-lamports precision-loss class on the fee-enforcement comparator.
- ·Consent: the in-app confirm dialog now computes the fee for the SELECTED subset of accounts (calculateFees(prev+selected) - calculateFees(prev)) instead of the scan-time fee for ALL accounts. After /api/build-transactions returns, if the server's actual fee exceeds the disclosed amount (race vs another tab), the client aborts BEFORE the wallet prompts — you'll never sign a tx charging more than the modal showed.
- ·Consent: /api/build-transactions now re-scans the wallet and rejects any pubkey the scanner wouldn't have offered (non-empty balance, frozen, delegated, foreign close-authority). A malicious or buggy client can no longer build a sweep tx for accounts it didn't show you.
- ·Trust: post-sweep card redesigned. Each stat now reads 'lifetime (+this-sweep delta)' with an explicit 'Lifetime' label. The '+N points' celebration also names the this-sweep amounts (closed / SOL reclaimed) so the lifetime number below can never be mistaken for the sweep result.
- ·Trust: formatSol refuses any value above 100,000 SOL (renders '—' + console.warn); formatNumber refuses above 100M. If a future type bug ever ships wild data to the UI, users see a missing-data dash instead of a fabricated number on a money screen.
- ·Trust: SweepHistory, GlobalStats, and LeaderboardTable now show explicit error states when their fetch fails, instead of rendering nothing or 'No history yet' which looked indistinguishable from a real empty state.
- ·Trust: scoring.ts getGradeFromScore is now defensive against non-numeric inputs (was silently returning F via lexicographic string comparison).
- ·Trust: sweep error categorizer now preserves SubmitSimulationError.userMessage on the outer catch path — actionable 'Wallet does not have enough SOL to pay transaction fees' messages no longer get overwritten with generic 'Sweep failed' when the throw bubbles past the inner per-tx loop.
- ·Privacy: NEXT_PUBLIC_TREASURY_WALLET is no longer baked into the client bundle. The treasury address was previously recoverable via view-source despite never appearing in rendered UI. Server-side verifyAllSignedTransactions is the authoritative destination gate; tip-jar still verifies the transfer AMOUNT it asked for, just not the address.
- ·Phantom Blowfish mitigations: every sweep tx now carries an SPL Memo (`SolScore: close N empty token accts (M/total)`) — Phantom's confirm dialog + Solscan/SolanaFM now show explicit context instead of an opaque blob. Reduces drainer-pattern false-positives on close+outbound-transfer txs.
- ·Phantom Blowfish: site now ships robots.txt, sitemap.xml, JSON-LD Organization + SoftwareApplication structured data (Schema.org), Twitter site/creator handles, stronger Open Graph copy, and an OG-image route fallback so a Neon outage doesn't blank the preview thumbnail wallet reviewers scrape.
- ·Mobile: nav at 360px no longer overflows. Primary actions (Sweep, Leaderboard) inline; secondary routes (Fees, Stats, Security, Changelog) behind a hamburger Sheet. Every touch target now ≥44px (WCAG/Apple HIG).
- ·Mobile: leaderboard table no longer hides Swept + Reclaimed columns on small screens — they're now stacked as a muted secondary row under the wallet, so phone users actually see the SOL numbers that make the leaderboard interesting.
- ·Accessibility: confirm-sweep dialog rewritten on Radix Dialog (shadcn) — proper focus trap, return-focus on close, aria-describedby. Previous hand-rolled overlay had role=dialog but none of the actual focus management. Account-selector checkbox now has a visible focus-ring. Sweep dashboard has sr-only h2 landmarks. Wallet-deeplink emoji are now aria-hidden. --muted-foreground lightened from oklch(0.65) to oklch(0.74) to clear WCAG AA contrast on small text.
- ·Rate limits: /api/build-transactions now per-wallet rate-limited (was per-IP only — the more expensive endpoint deserves the tighter budget). /api/scan correctly upgrades to the batchScan tier (3/60s) when the request actually contains multiple wallets (previously every scan landed in the lighter scan tier, so attackers could 10x their RPC burn rate by batching).
- ·Rate limits: missing forwarding headers now bucket by user-agent fingerprint instead of collapsing every header-stripped client into a single 'unknown' token — one attacker can no longer saturate the shared bucket and DoS every Tor/VPN exit out of the budget.
- ·Telegram webhook hardening: signature verification moved outside the outer try block so a 403 can never be silently rewritten to ok:true by the catch. Buffers padded to equal length so timingSafeEqual never throws on mismatched-length tokens.
- ·Copy: the 'always free' badge on /fees and the 'First 10 accounts are always free' line in the in-app fee-breakdown both replaced with posted-rate language. The fees page no longer says 'And we're telling you' after 'We take less' (transparency theater — the page IS the transparency).
- ·Dependency vulns: swapped @solana/wallet-adapter-wallets (the meta package that hauls in every adapter — Keystone, Particle, Reown/WalletConnect, Torus) for the 5 individual per-wallet packages we actually register. Dropped npm audit from 56 vulns / 1 critical / 13 high to 11 / 0 critical / 3 high. Remaining vulns require absurd downgrades (Next.js to v9, spl-token to 0.1.8) that don't fix anything in practice — their attack vectors aren't reachable from our usage. Next.js bumped to 16.2.6 to clear the App Router XSS / SSRF / cache-poisoning advisories.
- ·Polish: MatrixRain on /security now plays once per session (sessionStorage flag) instead of every visit blocking content for 2.2s. Security page got a 'Last reviewed' timestamp.