Skip to main content

Frontend analysis lane

[← Source sessions](/audits/fable/key-sessions) · [Transcript archive](/audits/fable/transcripts) · [Executive summary](/audits/fable/findings-summary)
# Frontend analysis lane | Field | Value | | --- | --- | | Session ID | `6c9354f7-23f9-4069-8ad6-106b0e138461` | | Lead session | `0a513245-3ae2-4076-a9b0-bc1de524c38f` | | Record type | Parallel workstream export | :::note[Appendix record] Readable export of a supplementary review session. Tool outputs and system context are omitted; download the [JSONL archive](/audits/fable-chats-4626-2026-06.zip) for the complete log. ::: ## Engagement brief

You are doing the frontend-correctness pass of a full-codebase review of the 4626 monorepo at /home/akitav2/projects/4626. READ-ONLY: do not modify files. Stack: Vite 7 + React 19 + TypeScript SPA in frontend/src/, wagmi + Privy auth, two host shells (marketing 4626.fun, app app.4626.fun).

Find CONCRETE bugs/risks with file:line evidence and severity. Focus on correctness, not style. Checklist:

  1. Double-submit / in-flight protection on money-moving flows: frontend/src/pages/Swap.tsx + frontend/src/hooks or lib useSwapExecution (can a user fire two swaps concurrently? is the submit button disabled during busy states?), DeployVault deploy submission (frontend/src/pages/deploy/DeployVault.tsx), creator strategy payment UI (frontend/src/pages/CreatorStrategyFeatures.tsx — USDC/x402/Stripe buttons).
  2. Error boundaries: does the app have top-level error boundaries (search ErrorBoundary in frontend/src/)? What happens on a render crash in app shell vs marketing shell?
  3. Async race conditions in hooks: effects missing cleanup/abort on unmount in data-fetch hooks (sample frontend/src/hooks/ and feature hooks); stale-closure setState after unmount; quote effects on Swap pausing during signing.
  4. Marketing-shell crashes: Privy/wagmi hooks reachable from marketing-only routes (the known invariant: marketing shells render outside PrivyClientProvider — verify Layout gating, look at frontend/src/components/Layout or AuthenticatedAppLayout).
  5. Unsafe HTML: any dangerouslySetInnerHTML usage — is content sanitized?
  6. localStorage/sessionStorage JSON.parse without try/catch reachable on boot (crash loops).
  7. Form validation gaps on waitlist email entry, telegram link flow (frontend/src/features/waitlist/, telegram link page).
  8. window.open / external links with user-controlled URLs (javascript: scheme risk).
  9. BigInt/Number precision in amount parsing/formatting for swap and deploy (find parseUnits/formatUnits misuse, parseFloat on token amounts that feed transactions).
  10. Route guards: can unauthenticated users reach app routes and what happens (redirect loop risks)?

Return findings sorted by severity with file:line, plus verified-sound items. Thoroughness: very thorough.

Analysis

Running a read-only frontend correctness pass across the checklist areas.

[REDACTED]

Tools invoked: UpdateCurrentStep, Grep, Grep, Grep, Grep, Glob

Analysis

[REDACTED]

Tools invoked: Grep, Grep, Grep, Grep, Glob, Glob

Analysis

[REDACTED]

Tools invoked: Read, Read, Read, Grep, Grep

Analysis

[REDACTED]

Tools invoked: Read, Read, Grep, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Read, Read, Read, Grep

Analysis

[REDACTED]

Tools invoked: Grep, Read, Read, Grep, Read

Analysis

[REDACTED]

Tools invoked: Grep, Glob, Read, Read, Read, Read

Analysis

[REDACTED]

Tools invoked: Grep, Grep, Grep, Grep, Read, Read

Analysis

[REDACTED]

Tools invoked: Read, Read, Grep, Read, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Read, Read, Read, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Grep, Grep, Read, Grep

Analysis

[REDACTED]

Tools invoked: Grep, Read, Grep, Read, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Grep, Read, Grep, Read

Analysis

[REDACTED]

Tools invoked: Read, Read, Grep, Grep, Read

Analysis

[REDACTED]

Tools invoked: Read, Read, Read, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Read, Grep, Read, Grep

Analysis

[REDACTED]

Tools invoked: Read, Read, Grep, Read

Analysis

[REDACTED]

Tools invoked: Read, Grep, Read, Grep, Read

Analysis

[REDACTED]

Tools invoked: Grep, Glob, Grep, Read

Analysis

[REDACTED]

Tools invoked: Grep, Read, Read, Read

Analysis

[REDACTED]

Tools invoked: Grep, Read, Grep

Analysis

[REDACTED]

Tools invoked: Grep, Read

Analysis

[REDACTED]

Tools invoked: Grep

Analysis

[REDACTED]

Tools invoked: Grep

Analysis

[REDACTED]

Tools invoked: Read

Analysis

[REDACTED]

Tools invoked: Grep, Grep, Grep, Read

Analysis

[REDACTED]

Tools invoked: Read, Grep, Read

Analysis

[REDACTED]

Tools invoked: Read, Grep

Analysis

[REDACTED]

Tools invoked: Grep, Read

Analysis

[REDACTED]

Tools invoked: UpdateCurrentStep

Analysis

Critical / High

1. Live deploy can start while dry-run is in flight

Severity: High

Dry-run sets dryRunBusy but the primary deploy button and submit() do not treat it as blocking.

  const submit = async (opts?: { planOnly?: boolean; validateDepositBalance?: boolean }): Promise<DeployPlanExport | null> => {
const planOnly = opts?.planOnly === true
const validateDepositBalance = !planOnly || opts?.validateDepositBalance === true
if (busy || exportBusy) return null
          <Button
type="button"
variant="primary"
className="w-full rounded-lg"
onClick={() => void submit()}
disabled={disabled || exportBusy}
>
{busy ? 'Deploying…' : '1‑Click Deploy (Gas-Free)'}

Dry-run correctly gates itself (7250:7251) and disables its own button (8108, 8117), but 1-Click Deploy ignores dryRunBusy. A user can start a real gas-sponsored deploy while submit({ planOnly: true }) is still building/simulating on the fork.


2. Auto-quote can abort an in-progress swap review

Severity: High (swap correctness / failed mid-submit UX)

The debounced quote effect only skips when txState === 'signing' and when busyRef === 'executeSwap'. It does not skip during busy === 'review' or busy === 'quote'.

  useEffect(() => {
...
if (txState === 'signing') return
...
const timer = window.setTimeout(() => {
if (busyRef.current === 'executeSwap') return
void handleQuote()
}, 450)

handleReviewTrade and handleQuote share quoteRunRef (1759, 1520). Starting auto-quote during review bumps the epoch and causes review to bail with if (runId !== quoteRunRef.current) return mid-flight — potentially after partial signing/approval work.

handleReviewTrade also does not guard on general busy (only executeSwap / signing / pending):

    if (busy === 'executeSwap' || txState === 'signing' || txState === 'pending') {
setStatus('Swap already in progress. Wait for the current transaction to finish.')
return
}

3. Swap submit lacks an atomic in-flight mutex (double-submit window)

Severity: Medium–High

Guards check busy / txState, then setBusy('executeSwap') later — classic check-then-act race. Same pattern in confirmAndExecute:

  const confirmAndExecute = useCallback(async () => {
if (!confirmIntent || busy) return
...
setConfirmIntent(null)
try {
setTxState('signing')
...
await executeSwapNow({

Two rapid paths (auto confirmIntent effect in Swap.tsx:935-938 + double activation) can both pass before busy updates. UI disables the primary button when busy !== null (SwapCard.tsx:70-71), but programmatic/StrictMode timing gaps remain.


Medium

4. Creator strategy: concurrent payments across different features

Severity: Medium

inflightFeature is per feature key only; startPayment does not block when another feature is already in flight:

  const startPayment = useCallback(
async (feature: CatalogDto, path: PaymentPath) => {
if (!data || !creatorToken) return
setInflightFeature(feature.key)

User can start Stripe on feature A and x402/USDC on feature B concurrently → duplicate authorizations / conflicting wallet prompts.

5. Creator strategy USDC path: no client tx-hash validation

Severity: Medium (bad UX / wasted API round-trips)

          case 'usdc_txhash': {
const txHash = window.prompt(...)
if (!txHash) return
const res = await fetch(`${base}/api/creator/strategy/activate`, {

Any non-empty string is sent; no ^0x[a-fA-F0-9]{64}$ check before POST (server should reject, but client allows garbage).

6. Stripe checkout: no URL allowlist before navigation

Severity: Medium (trust-boundary)

            window.location.href = json.data.sessionUrl

If API/session were compromised, arbitrary URL assignment is possible. Should restrict to https://checkout.stripe.com/ (or similar).

7. Server deploy session auto-resume effect re-fires when busy clears

Severity: Medium

  useEffect(() => {
if (!useServerContinue) return
if (busy) return
const sessionId = loadDeploySession()
if (!sessionId) return
...
}, [busy, ...])

clearDeploySession runs on terminal poll outcomes (sessionClient.ts:136-142), but transient poll/network errors leave the session in localStorage and the effect re-runs whenever busy flips false — duplicate resume ticks / surprise continuation after partial failure.

8. toParsableAmount uses Number() before parseUnits

Severity: Medium (precision edge cases)

function toParsableAmount(value: string): string | null {
...
const n = Number(normalized)
if (!Number.isFinite(n) || n <= 0) return null
return normalized
}

Validation goes through IEEE double; extremely long fractional strings or scientific notation can pass validation inconsistently vs what parseUnits accepts. Balance-percent path is safer (amountUnitsFromBalancePercent uses bigint math in swapDisplayAmount.ts:14-22).


Low

9. Waitlist email: no client-side format gate before Privy login

Severity: Low

Waitlist defers entirely to Privy login() (WaitlistFlow.tsx ~598+). Unlike Telegram (telegramLinkHelpers.ts:181-183, getEmailSubmitAssessment), there is no local regex/disabled-state before OTP send. Privy likely validates server-side, but UX/error timing is weaker.

10. Deploy double-submit: 2s localStorage throttle only

Severity: Low

        const retryWindowMs = 2000
if (Number.isFinite(last) && last > 0 && now - last < retryWindowMs) {

Bypassable via second tab or waiting 2s; busy guard helps but is not set until after the throttle check path continues.

Severity: Low (current call sites are fixed URLs)

export function openTelegramExternalLink(url: string): boolean {
const targetUrl = asTrimmed(url)
...
window.open(targetUrl, '_blank', 'noopener,noreferrer')

Live callers use constructed same-origin URLs (TelegramLink.tsx:252-257) or constants — not user-controlled today. Helper itself would allow javascript: if misused later.


Verified sound

Money-moving flows (partial)

AreaAssessment
Swap UIPrimary button disabled when busy !== null (SwapCard.tsx:70-73, 152-153); shows loading state.
Swap execute guardsBlocks when busy === 'executeSwap' or signing/pending (useSwapExecution.ts:2133-2135); uses submitEpoch + assertSwapSubmitEpochUnchanged for dev HMR.
Swap quote during signingAuto-quote paused when txState === 'signing' (Swap.tsx:908).
Deploybusy || exportBusy early return; 2s throttle; button disabled when disabled || exportBusy (8319).
Creator strategyPer-feature isInflight disables all three payment buttons (PaymentPathGrid.tsx:523-538); finally clears inflight (CreatorStrategyFeatures.tsx:190-192).
Deploy deposit amountsEnforced 50M integer tokens → bigint (DeployVault.tsx:8605-8608, parsePositiveTokenAmount in deployVaultHelpers.ts:122-132).

Error boundaries

  • App-wide: RootErrorBoundary wraps full tree in main.tsx:379-388 (marketing + app SPA).
  • Deploy-only: DeployVaultErrorBoundary (DeployVault.tsx:905+, 992-994).
  • Dev tactical map: local HoloRenderErrorBoundary.
  • Marketing /faq, /arena, etc. rely on root boundary only (no route-scoped boundary) — acceptable; crash shows recovery UI, not blank page.

Marketing shell / Privy / wagmi gating

  • Layout: useSiweAuth / useAccountTrayPortfolio only when interactive && hostMode === 'app' (Layout.tsx:129-136, 144-146).
  • Marketing routes in App: Layout interactive={false} (App.tsx:119-121); FAQ/explore marketing paths do not mount Privy.
  • Waitlist: Own PrivyClientProvider (Waitlist.tsx:43-48); group chat mounts WalletProviders before AccountContextProvider (WaitlistSetupWorkspace.tsx:25-27) — avoids wagmi hook crash.
  • Explore: No usePrivy in explore pages/components (grep clean); wagmi via LazyAccessBoundary only.

Async / hook hygiene (samples)

  • useAccountMe: cancelled flag + no null cache (useAccountMe.ts:114-150).
  • useSwapExecution: receipt poll AbortController (2692+); quote run IDs for stale updates.
  • Leaderboard: inFlightRef dedupes fetches (Leaderboard.tsx:43-47).
  • MermaidDiagram: isCancelled cleanup (Arena.tsx:605-626).

Unsafe HTML

  • Only production dangerouslySetInnerHTML is Arena Mermaid output (Arena.tsx:648) with securityLevel: 'strict' and static diagram strings (208, 233, etc.) — low XSS risk.

localStorage JSON.parse on boot

Most hot paths wrapped in try/catch: useSwapRecentTokens.ts:12-24, useDeploymentTracker.ts:52-58, deployVaultVanityPersistence.ts:77-91, xmtp/provider.tsx:412-427, appActivityJournal.ts:33-40, DeployVault session load 2708-2720.

Form validation

  • Telegram link: Strong — isValidEmail, disabled reasons, OTP length checks, gesture lock (telegramLinkHelpers.ts, TelegramLink.tsx, extensive tests in TelegramLink.test.tsx).
  • Share/waitlist intents build fixed HTTPS URLs via URLSearchParams (ShareVaultButton.tsx:27-41, WaitlistDailyActionsHub.tsx:39-41) — no user URL injection in current paths.

Route guards

  • App-only on marketing host: RootRouter.tsx:71-89, HostGuard + HandoffOnMount (routeGuuards.tsx:78-87).
  • Session + waitlist acceptance: SessionAcceptedRouteRequireSession + RequireAccepted (routeGuards.tsx:125-134, accessRuntime.tsx:115-136).
  • Waitlist-me hydration race: guarded with acceptedUnknown to prevent bounce redirect (accessRuntime.tsx:66-79) — tested in accessRuntime.test.tsx.
  • Unauthenticated /swap: redirects to marketing waitlist URL, not an in-app loop (accessShared.tsx:48-55).
  • /accounts / /add: Under LazyAuthWalletBoundary but without RequireSession — intentional for owner-install deep links; APIs must enforce auth server-side.

Summary priority fixes (if you switch to Agent mode)

  1. Add dryRunBusy to deploy submit() guard and deploy button disabled.
  2. Pause auto-quote when busy is 'review', 'quote', or txState === 'pending'.
  3. Add a ref-based submit mutex in executeSwapNow / confirmAndExecute.
  4. Global inflightPayment (or disable all pay buttons) in CreatorStrategyFeatures.tsx.

I can implement these in Agent mode if you want.

[REDACTED]