Skip to content

fix: end-to-end audit remediation — money path, security, trust surfaces - #52

Merged
CodeWithEugene merged 2 commits into
mainfrom
fix/audit-remediation
Sep 27, 2026
Merged

CodeWithEugene merged 2 commits into
mainfrom
fix/audit-remediation

Conversation

@CodeWithEugene

Copy link
Copy Markdown
Owner

Summary

Full remediation of the 2026-09-27 end-to-end audit (85+ findings). Every critical and high item is fixed, tested, and verified live.

⚠️ Money-path changes — per repo policy this PR requires 2 maintainer approvals and a testnet deploy before the live-mode switch.

Critical money-path fixes

  • Webhook outbox: replay of a stored event with processedAt NULL now re-processes instead of returning a bare duplicate (the crash-window money-loss hole). Stale-event sweep added to escrow cron; orphaned charges are verified with Paystack and paged to ops.
  • Deposit overpayment cap: a second deposit landing on a locked vault records SUCCEEDED but never double-locks; audited + paged for manual refund.
  • Attestation survival: cold-start vaultAddressFor fallback, state-aware handlers (skip already-applied writes, mirror missing ledger rows, retryable AttestationNotReadyError), retry ×10 / 1h expiry, reconcile now has the chain leg with auto-repair.
  • Stuck-PROCESSING recovery: transferStatus provider-truth lookup; sweep resolves stuck payouts from Paystack state; admin retry refuses while the original transfer is unverified (double-pay path closed).
  • Reversal integrity: clawback-after-success is paged, success-after-reversed is refused, manual-paid attests with the receipt, vault states self-heal in the payout cron.
  • Refunds exist: refundLockedVault primitive + organizer (pre-funding) and admin (pre-winners, 2-admin dispute REFUND) cancellation paths.
  • Announce validation: distinct teams, exact place coverage, atomic winner+milestone+payout creation, friendly double-submit errors.
  • Judge/participant mutual exclusion at team join, invite activation, and openJudging.
  • Contracts: hot attester key reduced to ATTESTER_ROLE only; factory rotateAttester (grant-before-revoke) with 5 new tests.

Security & ops

  • CSP / HSTS / nosniff / Referrer-Policy / Permissions-Policy headers; X-Powered-By off (verified live).
  • Rate limiting is now DB-backed (RateLimitBucket, async, in-memory fallback) with per-IP secondary limits; register/cancel/judge-invite routes covered.
  • Fail-closed payments: keyless production throws unless PAYSTACK_ALLOW_SIMULATION_IN_PROD=true (explicit pre-go-live opt-in — set it in Vercel until real keys land).
  • Dev upload routes: path-traversal fix, upload validation, event-membership authz, explicit prod guard.
  • Sentry v11 wired (DSN-gated no-op without env), /api/health, coverage gate wired into CI (75/95/75 floors on escrow+payout, ratchet comment), operator-script prod guards, Cloudflare range auto-refresh.

Product integrity

  • /trust public ledger restored (write-only attestation layer now has its surface; simulation-mode banner when applicable), /organizers/[slug] public trust pages, /organizer/organization settings, 13 loading.tsx skeletons.
  • Invite-link onboarding dead-end fixed; claim-first invite acceptance with email binding; KYB PENDING precondition; newsletter re-subscribe confirmation + 90-day token TTL; registration receipts always-send on transition only.
  • Media upload confirm-after-PUT (phantom assets impossible); dev cover route verifies uploads; scoring screen no longer fabricates 5/10 defaults; shared ConfirmMoneyAction across announce / milestone / mark-paid; prize-verified badge wired on listing + detail; wizard per-step validation; ghost/empty-state and stale-copy fixes.
  • Seed rebuilt invariant-respecting (vaults for live events, full SETTLED graph, disputes, ADMIN/HIRING demo users); categories NOT NULL migration; docs synced (README, CONTRIBUTING, runbooks, BUILD_PLAN amendments, 45 stale image refs).

Verification

  • pnpm typecheck ✅ zero errors · pnpm lint ✅ clean · pnpm build ✅ zero warnings (67 routes)
  • 449 vitest (59 files) ✅ (was 336) · 14 hardhat ✅ (was 9)
  • Coverage gate: 80.45% lines aggregate (escrow 85.8%, payout 74.7%) — thresholds 75/95/75 held
  • Runtime smoke: /trust 200, /organizers/[slug] 200, /api/health 200 (db/queue ok), security headers present, dashboard auth redirect intact
  • DB: 16 migrations applied, migrate diff empty, seed idempotent (3 runs), reconcile 0 findings

Deployment notes

  • Set PAYSTACK_ALLOW_SIMULATION_IN_PROD=true in Vercel before this deploys (current prod intentionally runs simulation; without the flag the payment port now throws in production).
  • Optional: SENTRY_DSN + SENTRY_AUTH_TOKEN to activate error reporting.

Critical money-path hardening:
- webhook outbox: replay re-processes events with processedAt NULL instead
  of swallowing them; stale-event sweep wired into escrow cron; orphaned
  charges verified with provider and escalated to ops
- deposit overpayment capped: second deposit on a locked vault records but
  never double-locks; audited and paged for manual refund
- attestation survival: cold-start vault address fallback, state-aware
  handlers (skip already-applied chain writes, mirror missing ledger rows),
  retryLimit 10 / 1h expiry, reconcile gains the chain leg with auto-repair
- stuck-PROCESSING recovery: PaystackPort.transferStatus, sweep resolves
  from provider truth, admin retry gated on verified-terminal original
- reversal integrity: clawback-after-success paged, success-after-reversed
  refused; manual-paid now attests with receipt; vault healing in cron
- refundLockedVault primitive + organizer/admin event cancellation
- announce validation (distinct teams, exact place coverage), judge /
  participant mutual exclusion at join + invite + openJudging
- contracts: attester key holds ATTESTER_ROLE only; factory rotateAttester

Security & ops:
- CSP/HSTS/nosniff/referrer/permissions headers, X-Powered-By off
- rate limiting: DB-backed RateLimitBucket (async), per-IP secondary limits,
  coverage on register/cancel/judge-invite routes
- fail-closed payment port in production (PAYSTACK_ALLOW_SIMULATION_IN_PROD
  opt-in), dev upload routes hardened (traversal + authz + prod guard)
- Sentry v11 (DSN-gated), /api/health, coverage gate (75/95/75 floors on
  escrow+payout), script env guards, Cloudflare ranges auto-refresh

Product integrity:
- /trust public ledger restored + /organizers/[slug] trust pages +
  /organizer/organization settings; 13 loading.tsx skeletons
- invite-link onboarding dead-end fixed; claim-first invite acceptance with
  email binding; KYB precondition; newsletter confirm flow + token TTL
- media upload confirm-after-PUT (no phantom assets); scoring screen no
  longer fabricates defaults; ConfirmMoneyAction shared by announce /
  milestone / mark-paid; prize-verified badge wired; wizard per-step
  validation; dashboard/workspace/winners empty-state and copy fixes
- seed rebuilt invariant-respecting (vaults, winners, payouts, disputes,
  ADMIN/HIRING users); categories NOT NULL migration; docs synced to reality

449 vitest (59 files) + 14 hardhat green; typecheck/lint/build clean
@CodeWithEugene
CodeWithEugene merged commit b65c392 into main Sep 27, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — ed8cf31c Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant