10 KiB
Morphit pre-launch audit — Final report
Audit window: May 2026 Scope: Comprehensive 110-item static/security audit covering static code, deps/supply-chain, SQL/DB, HTTP/API, crypto, privacy, operator-trust, frontend, contracts, build/CI, threat modeling, and per-subsystem deep dives.
Outcome: ~57 findings; all but 1 pre-existing backlog item shipped or deferred to REVISIT-LIST. Smoke baseline maintained at 1989 scenarios across triple-pulse stability checks throughout the campaign.
Findings by severity
| Severity | Count | Status |
|---|---|---|
| CRITICAL | 4 | All shipped |
| HIGH | 7 | All shipped (1 deferred backlog) |
| MEDIUM | 7 | All shipped |
| LOW | ~37 | All shipped |
| NIT | 2 | Both shipped |
CRITICAL findings
- A5 — covered in Phase A (deps/supply-chain) — shipped
- A9 — covered in Phase A — shipped
- G1 — missing v24 schema migration — shipped
- H8 — ACT model drift: relay's
create.tsused inline-payaccount_createop while ADR-0010 §4 + scripts + operator docs specifiedcreate_claimed_accountconsuming pre-minted ACTs — shipped, full vertical fix acrossclient.ts,health.ts,create.ts, fixtures, brag list, FAQ entries, and 9-locale retranslation backlog logged
HIGH findings
A4, A6, A7, B1 (waiver-floor bypass), B3 (operatorAccountName split), G2 (broken schema-v8 errored on every fresh deploy), G6 (loyalty SAVEPOINT poisoning — silent order loss). All shipped with smoke regression coverage. Pre-existing relay tsc lint backlog (B4) deferred — non-blocking for launch.
MEDIUM findings
A8, A10, C1+C2 (relay endpoint repass), J5 (order_timeouts FAQ
referenced unimplemented 5-day pay window + business-hours toggle),
J8 (operator_payouts_timing FAQ referenced non-existent "Smart
Contract" + claimed BTC/XMR earnings flow to operators), K2 (§28
fail2ban-for-relay subsection described non-existent middleware
- wrong log shape + IP-privacy violation), O1 (duplicate-tx after retry not handled — silent successful broadcast misreported as failure, causing user to retry with different name while original account quietly existed on-chain).
Phase-by-phase summary
Phase A — doc accuracy
10 doc-accuracy fixes shipped.
Phase B — hostile-operator sweep
B1 HIGH waiver-floor bypass, B3 HIGH operatorAccountName split. B4 (pre-existing relay tsc lint backlog) deferred.
Phase C — relay endpoint repass
C1 + C2 MEDIUM with 13-test regression at
apps/relay/test/middleware/ip.test.ts.
Phase D-F — threat modeling
- STRIDE matrix at
docs/audit/2026-05-stride-matrix.md - Attack tree at
docs/audit/2026-05-attack-tree.md - Red-team narrative at
docs/audit/2026-05-red-team-narrative.md
Phase G — DB schema audit
8 findings (G1 CRITICAL, G2 + G6 HIGH, others LOW). G6 fix wraps
welcome and milestone INSERTs in nested SAVEPOINTs to prevent
transaction-abort cascades. 4-test regression at
apps/indexer/test/integration/loyalty-g6-regression.test.ts.
Phase H — ADR-vs-code drift
22 ADRs walked. H8 CRITICAL ACT-model fix is the headline item; H1-H7 LOW status updates and ref renumbering.
Phase I — i18n parity
Structurally clean (2161 keys × 10 locales). 48 strings
byte-identical to English captured in
docs/i18n-untranslated-2026-05.txt for retranslation backlog;
all 48 translated 2026-05-08 (Part 89, brag list entry #237) —
file retained as a tombstone for historical traceability.
Phase J — FAQ accuracy
All 103 entries walked. 9 fixes (J1-J9): 1 MEDIUM, 7 LOW,
1 dead-code REVISIT for unused feeStatusLabel UI branches.
Phase K — OPERATIONS.md walk
4137 lines / 28 sections. K1a + K1b LOW (env-var name drift), K2 MEDIUM (§28 fail2ban subsection rewrite). RUN-A-MORPHIT-NODE.md (1110 lines) walked clean.
Phase L — frontend dead-code
90 .svelte files (53 components + 37 routes) walked. 12 unused
imports / dead-code fixes shipped. Now zero unused locals
under tsc --noUnusedLocals --noUnusedParameters across the
entire frontend. Zero Svelte 4 legacy syntax remaining
(fully Svelte 5).
Phase M — memory leaks
Zero unbounded Map/Set growth, zero unbounded array.push without eviction across indexer + relay. Zero EventSource leaks, zero listener leaks in components.
Phase N — unused files
292 .ts source files walked. Zero unused (5 flagged were SvelteKit
framework conventions, smoke runners, component imports).
1 unused image (morphit-fee-flow.svg) logged as
"decide source-of-truth" REVISIT item — CLOSED 2026-05-06: SVG
chosen as source-of-truth, the orphaned PNG was deleted, and
FEES-AND-REWARDS.md now references the SVG directly.
Phase O — fallback / failover audit
All major failure surfaces walked; O1 fix shipped for the real gap. Battle-tested rotation patterns in BLURT RPC, XMR explorer, price-feed composite, frontend endpoint rotation, EventSource auto-reconnect, drainer exponential backoff.
Standing pre-launch action items (not audit findings, just
campaign-relevant tracking)
The following were on Ken's radar at the time this final report was written. All three have since been closed; the report is preserved as a historical artifact and the audit-trail below records what shipped:
Rotate theCLOSED — pattern inverted to denylist. Rather than removing the placeholder,CHANGE_ME_BEFORE_PRODUCTIONplaceholder inops/postgres/init.sqlbefore any production deploy.ops/postgres/init.sqlnow treats it (along withCHANGEME,CHANGE_ME,__SET_BEFORE_DEPLOY__,password,postgres) as a denied value: any operator who fails to replace it before running init.sql hits a clearRAISE EXCEPTIONwith the placeholder name in the message. Same denylist enforced at runtime in the indexer's and relay's Zod config schemas, so a misconfigured production boot refuses to start. See OPERATIONS.md §30 for the full provisioning + runtime guardrail rationale.CommitCLOSED by Part 70 —package-lock.jsonfor reproducible installs.package-lock.jsonis committed at the repo root for reproduciblenpm ciinstalls.WireCLOSED by Part 70 (initial), made explicit Part 122 cp111. The protection has been in place since Part 70 via thesvelte-kit sync + tsc --noEmitinto CI.workspace-typecheck-smokescript (root-level smoke listed inscripts/run-smokes.sh), which runsnpx svelte-kit syncfollowed bynpx svelte-checkagainst apps/web — wired into CI through thesmokesjob that invokesrun-smokes.sh. cp111 added a dedicatedweb-checkjob in.forgejo/workflows/ci.ymlthat runs the same two binaries directly, so the protection is legible in the CI surface without indirection through the smoke runner; the smoke remains as a defense-in-depth layer that also runs locally viabash scripts/run-smokes.sh. See cp111 entry inTARBALL.mdfor the wiring rationale.
REVISIT-LIST highlights (deferred work)
9-locale retranslation of FAQ entries that were edited in English (signup_stuck, why_multi_accounts_fail, order_timeouts, operator_payouts_timing, etc.) — non-EN locales currently show the pre-edit text.CLOSED — all retranslated by Part 89.- ~~48 i18n untranslated strings in non-EN locales (SEO meta
- fallback states) — captured in
docs/i18n-untranslated-2026-05.txt.~~ CLOSED — all translated by Part 89; file retained as tombstone.
- fallback states) — captured in
DeadCLOSED — Part 103 re-audit: the original diagnosis was inverted.feeStatusLabelUI branches in My Orders (missing,underpaid,unverified) — indexer never writes these states; either delete the dead UI branches or wire the indexer to actually emit them.feeStatusLabelwas MISSING explicit cases formissingandunderpaid(which the indexer DOES write — verified inapps/indexer/src/indexer/handlers/order.ts:649,714and the order-handler smoke), so an order in either state rendered an empty amber pill. Fixed by adding the two cases plus a defensive default that returns the raw status string. Two i18n keys added (my_orders.order.fee_missing,my_orders.order.fee_underpaid) translated to all 10 locales.'unverified'is the DB-level DEFAULT and remains a defensive UI branch in order_detail (no handler path lets it land today but the column constraint admits it).- Optional opt-in IP logging on relay access_log
(
MORPHIT_RELAY_ACCESS_LOG_INCLUDE_IP) for operators who want fail2ban integration without giving up the privacy default. Decide source-of-truth for fee-flow brand asset (PNG referenced from docs, SVG orphaned).CLOSED 2026-05-06: SVG chosen, PNG deleted, FEES-AND-REWARDS.md now points at SVG.
Test coverage at audit close
| Suite | Count | Status |
|---|---|---|
| Indexer default tests | 370 / 370 + 1 skipped | ✅ |
| Indexer integration tests | 67 / 67 across 9 files | ✅ |
| Relay tests | 178 / 178 (incl. 2 new O1 regressions) | ✅ |
| Smokes (triple-pulse) | 1989 / 1989 | ✅ stable |
| Frontend typecheck | 0 errors | ✅ |
Frontend noUnusedLocals |
0 issues | ✅ (was 18) |
svelte-check --threshold warning |
0 / 0 | ✅ |
Known flake
Root-caused and fixed post-audit: the
flake was a tamper-last-char padding collision in the live-fire
scenario. Fixed by tampering the first char instead — eliminates
the padding ambiguity entirely. Triple-pulse stable since.drain-defense-live-fire smoke scenario occasionally drops 23
scenarios on one pulse — known timing race, not introduced by
this audit campaign.
Audit close statement
The codebase is exceptionally clean for a pre-launch project of this complexity. The audit found real gaps (especially H8 ACT model drift, K2 fail2ban privacy violation, O1 duplicate-tx silent failure) that pre-launch fixing was the entire point of — those bugs are now bugs prevented, not bugs that hurt anyone.
The Phases L, M, N sweeps came up nearly empty after fixing the unused-locals — that's the signature of a well-maintained codebase, not a sloppy one we polished. Ship with confidence.