UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin of navbar, connect button, dapp page, loan cards, settings modal, and readme. Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched. Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]], callouts, Home MOC, folders Architecture/Operations/Audits) and added a full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy key value from the security audit example (rotate it at Schedy). Also commits the previously-untracked server layer: app/api (cron + tasks routes) and lib (redis, ssrf-guard, task-store). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 KiB
title, tags, type, status, updated
| title | tags | type | status | updated | |||||
|---|---|---|---|---|---|---|---|---|---|
| Project Audit 2026-06 |
|
audit | current | 2026-06-14 |
Project Audit — June 2026
A fresh, whole-project review of MortgageFi after a period of inactivity, covering the frontend web3 logic, the server/API layer, the Go nftcache service, infrastructure/ops, and dependencies. It complements the deeper, topic-specific Security Audit and Performance Audit — where those go further on a topic, they are linked inline.
[!danger] Three issues need attention before anything else
- A live secret is shipped to every browser —
NEXT_PUBLIC_SCHEDY_API_KEYis set in.env, so it is inlined into the client bundle. Rotate the Schedy key now and remove theNEXT_PUBLIC_variant.- The scheduler's SSRF guard is bypassable — it validates the hostname string at creation time but the cron worker fires the request later with no re-validation, no redirect control, and no DNS-rebinding protection.
GET /api/tasksandDELETE /api/tasks/[id]have no authentication — anyone can enumerate every scheduled task (and the secrets stored in their headers) or delete them.
Executive summary
The codebase is, structurally, in good shape: a modern and internally-consistent core stack (Next.js 16, React 19, wagmi v3 + viem v2, TypeScript strict), a clean scheduling migration to Next.js API routes + Upstash Redis, non-root Docker images that aren't port-published, and genuine security thought (RPC allowlist, known-preset compound guard, SSRF allowlist scaffolding, a real CSP).
The problems are concentrated in secret handling, the outbound-request (SSRF) trust model, endpoint authentication, and the NFT-scanning design (both the frontend ownerOf walk and the backend serial scan are correctness-fragile and DoS-prone). There is also notable dead weight: two unused web3 stacks (@web3-react, @web3modal/wagmi) and the orphaned schedy submodule that Docker still builds and runs.
Findings by severity
| Domain | 🔴 Critical | 🟠 High | 🟡 Medium | 🟢 Low | ⚪ Info |
|---|---|---|---|---|---|
| Frontend web3 | 0 | 3 | 7 | 4 | 1 |
| API & server libs | 2 | 3 | 3 | 2 | 1 |
| nftcache (Go) | 2 | 4 | 4 | 3 | 2 |
| Infra & ops | 2 | 3 | 3 | 2 | 2 |
| Dependencies & build | 0 | 2 | 2 | 2 | 1 |
| Total | 6 | 15 | 19 | 13 | 7 |
[!note] Severity is engineering judgement, not a formal CVSS score. The two "Critical" deps/web3modal-style items were normalised down because the affected code is unused; see each finding.
Priority remediation roadmap
[!todo] Do first (Critical)
- Rotate the Schedy API key and delete
NEXT_PUBLIC_SCHEDY_API_KEY; proxy Schedy through a server-side route that reads the non-publicSCHEDY_API_KEY. Remove the straymanwe-secretline.- Rewrite
lib/ssrf-guard.tsto validate at fire-time: resolve all A/AAAA records, reject any private/reserved/loopback/link-local IP, pin the connection to the validated IP (defeat rebinding), setredirect: 'manual'and re-validate each hop, and parse decimal/octal/hex/IPv4-mapped-IPv6 forms with a real IP library.- Authenticate
GET /api/tasksandDELETE /api/tasks/[id], scope tasks to an owner, and never return rawheaders/payload.- nftcache: move the API-key check to the top of every handler (before any param parsing, cache access, or RPC), and restrict
nft_contract/networkto the YAML allowlist so an arbitrary address can't trigger a 10k-call scan.- Audit published images for baked-in secrets (
COPY . .+ missing frontend.dockerignore); add a.dockerignoreexcluding.env*, then rotate anything that may have shipped.
[!todo] Do next (High)
- Make
CRON_SECRET(frontend) andX-API-Key(nftcache) comparisons constant-time (crypto.timingSafeEqual/crypto/subtle).- Fail the stack closed when
REDIS_REST_TOKENis unset; stop shippinglocal-redis-token-change-meas a working default.- Make the rate limiter atomic + fail-closed, and stop trusting client
X-Forwarded-Forfor the key.- Replace the frontend's sequential
ownerOfgap-scan withTransfer-log / enumerable / multicall detection; only count a "gap" on a truenonexistent tokenrevert, and never permanently mark a wallet "complete" after transient errors.- Give nftcache HTTP-server timeouts + graceful shutdown (
cache.Close()), threadcontextthrough RPC calls, and add single-flight to the stale-refresh path.- Add nginx
limit_req/client_max_body_size; confirm ntfy isn't open (NTFY_AUTH_DEFAULT_ACCESS=deny-all).- Gate write actions (
pay/approve/compound) onchainId === selectedChainId; scope the compound preset check to the selected chain.- Remove
@web3-react/*and@web3modal/wagmi; remove the orphanedschedyservice fromdocker-compose.yml/Dockerfile.
1. Frontend web3 & UI logic
Files:
app/dapp/page.tsx,config/web3.ts,providers/Web3Provider.tsx,utils/scheduler.ts,utils/useLocalStorage.ts,components/SettingsModal.tsx
The web3 read plumbing is mostly sound (on-chain decimals/symbol, parseUnits/BigInt money math, payment capped to currentPaymentPending). The weak spots are the NFT-discovery heuristics and several correctness traps around chain switching.
🟠 High
- Gap-limit scan conflates RPC errors with non-existent tokens —
app/dapp/page.tsx:339-392. Any non-429 error is treated as a "definitive gap"; 5 in a row writecomplete: true, after whichscanMoreearly-returns forever and the wallet looks empty — a safety-relevant false negative that can hide a position nearing liquidation. Fix: only count a gap on an actualnonexistent tokenrevert; persist a "stopped due to errors" state. - Sequential scan assumes contiguous tokenIds —
app/dapp/page.tsx:325-371. Walks0,1,2,…and stops after 5 misses; burns/non-sequential IDs are undetectable except by manual entry. Fix: preferTransfer-log scan or ERC721Enumerable (tokenOfOwnerByIndex) — see the dead-code finding below. - Chain-switch race —
app/dapp/page.tsx:429-437.selectedChainIddrives every read while the wallet may be on another chain; the auto-switchChainresult is ignored and reads/writes proceed regardless. Fix: gate writes onchainId === selectedChainId, awaitswitchChainAsync, prompt the user instead of silent auto-switch.
🟡 Medium
- APR display is a magnitude heuristic —
:991-1003. Infers scale at runtime (>1e9 ⇒ 1e18 scale, else basis points); any other denominator is mis-rendered, potentially off by orders of magnitude for a financial figure. Fix: read/hardcode the real per-preset denominator; unit-test against on-chain value. - Loan-term/expiration heuristic —
:1226-1237.expiration <= 200 ? "N Years" : date. Conflates two meanings of one field by magnitude. Fix: format deterministically from the ABI's actual semantics. - Provider tokens & API keys in
localStorage, logged to console —:116-118,:1404;SettingsModal.tsx:330-334;useLocalStorage.ts. Gotify token, ntfy topic, emails, NFTCache API key persisted and the whole settings object repeatedlyconsole.log'd. Fix: strip the logging; treat the tokens as secrets (don't persist, or schedule server-side). nftcacheApiKeysaved but no UI to view/clear it —SettingsModal.tsx:36,51-56. Half-wired secret that can be persisted but never edited or removed via the UI. Fix: add the field or remove the state.- Custom-RPC trust model is partial & silent —
config/web3.ts:11-49. The allowlist only governs read transports (not the wallet's signing RPC), the Alchemy regex accepts any key-shaped suffix, and a rejected RPC silently falls back to a public default with only aconsole.warn. Fix: surface rejection in the UI; document the scope. - Monolithic component re-renders + un-debounced auto-reschedule — whole file; effects at
:250-304,:929-955. ~25 hooks in one component; every keystroke re-renders the tree, and the auto-reschedule effect firesscheduleJob/cancel on >60s drift withexhaustive-depsdisabled — risking duplicate/cancelled jobs from stale closures. Fix: extract child components/hooks, debounce, add an in-flight guard. See Performance Audit for the broader re-render analysis. - Compound guard checks address but not chain —
:1017-1039. ValidatesdebtAddressacross all chains' presets, then writes withselectedChainId. Fix: scope toPRESETS[selectedChainId]and require chain match.
🟢 Low
- Dead enumerable code path —
:481-502.tokenOfOwnerByIndexreads are built then discarded; the more-correct enumeration is never used. Fix: implement viamulticallor delete. parseUnitsfailures swallowed in pay/approve handlers —:697-736. Bad input ("1.2.3","1e5") throws and is onlyconsole.warn'd; the money action silently no-ops. Fix: surface parse errors; reuse the render'samount === nullguard.- Scheduler fabricates a job id on ambiguous responses —
utils/scheduler.ts:45-65. A non-JSON/id-less response yieldstask_<random>, so a failed schedule looks successful (false confidence an alert exists). Fix: treat missingidas failure. useLocalStoragetrusts persisted JSON with no schema validation —utils/useLocalStorage.ts:18-39. Tampered/corrupt storage is accepted as typed settings that drive network requests; double-effect can cause a redundant write. Fix: validate shape (zod) before accepting.
⚪ Info
- Deep-link payload from
localStoragedrives chain/preset —:170-195. Same-origin, age-bounded, and IDs are later guarded — acceptable; consider range-validatingpreset/tokenId.
2. API & server libs
Files:
app/api/cron/route.ts,app/api/tasks/route.ts,app/api/tasks/[id]/route.ts,lib/redis.ts,lib/ssrf-guard.ts,lib/task-store.ts,utils/cronhost.ts
The skeleton is sound (CRON_SECRET gate, an SSRF allowlist, a rate limiter, header sanitisation) but the SSRF guard doesn't actually protect the outbound request, and two endpoints are unauthenticated.
🔴 Critical
- SSRF guard bypassable via DNS rebinding, redirects, and IP encodings —
lib/ssrf-guard.ts:30-84; enforcement gap atapp/api/cron/route.ts:36. Validation runs on the literal hostname at creation time only. Bypasses: (1) DNS rebinding —attacker.compasses, then resolves to169.254.169.254/127.0.0.1at fire time; (2) noredirect: 'manual', so a whitelisted host can 302 to metadata; (3) decimal/octal/hex IPv4 (http://2130706433/,0177.0.0.1,127.1) skip the dotted-quad regex; (4) IPv4-mapped IPv6 ([::ffff:169.254.169.254]) and long-form loopback bypass the prefix checks; (5) GCPmetadata.google.internaland the decimal form of the IMDS address aren't blocked. Fix: see the roadmap — resolve-and-pin at fire time. TheBLOCKED_HOSTSDocker-service-name list confirms an internal network is reachable. (Also tracked in Security Audit.) - No auth on
GET /api/tasks&DELETE /api/tasks/[id]—app/api/tasks/route.ts:65-72;app/api/tasks/[id]/route.ts:4-19. GET returns every task including eachurl, fullheaders(which legitimately carryAuthorization/API keys for the target) andpayload; DELETE removes any task whose idstartsWith('task_'). Anonymous credential disclosure + sabotage/DoS, with no per-owner scoping anywhere. Fix: authenticate + scope + redact.
🟠 High
- Stored task headers carry secrets and are returned/forwarded unredacted —
app/api/tasks/route.ts:50,59;lib/task-store.ts:21.sanitizeHeadersstrips only hop-by-hop headers;Authorization/Cookie/X-Api-Keysurvive, are stored in plaintext, echoed in the POST 201, exposed by the unauthenticated GET, and replayed on every retry. Fix: encrypt at rest, never echo, redact on cross-origin redirects. - Rate limiter fails open + check-then-act race + spoofable key —
lib/task-store.ts:76-91;app/api/tasks/route.ts:6-12. Returns allow when Redis is unconfigured; non-atomic count under concurrency; keyed off client-controlledx-forwarded-for; collapses missing-IP callers to oneunknownbucket. Fix: atomic sliding window (Lua/INCR+EXPIRE), fail closed, trusted-proxy IP only. CRON_SECRETcomparison not timing-safe —app/api/cron/route.ts:9-13. Plain!==on the one credential protecting the highest-privilege endpoint. Fix:crypto.timingSafeEqualover equal-length buffers.
🟡 Medium
listAllTasksusesKEYS+ N+1GET—lib/task-store.ts:58-73(andlistDueTasksN+1 at:51-54).KEYSis O(N)/blocking and Upstash-discouraged; an attacker can trigger it repeatedly via the open GET. Fix: drive listing off the existingTASK_ZSETwithZRANGE+MGET.- Cron forces POST + overrides stored content-type inconsistently —
app/api/cron/route.ts:37,40. Tasks store no method; the worker always POSTs and hardcodes content-type while letting other arbitrary headers through. Fix: make method/content-type explicit and validated; consistent precedence. - Weak input validation (URL length, payload size, header values) —
app/api/tasks/route.ts:21-26,50. No max URL length, no payload-size cap (stored & sent verbatim), header values not type/CRLF-checked. Fix: enforce limits; reject control/CRLF chars.
🟢 Low
- Internal error detail leaked to clients —
cron/route.ts:20,tasks/route.ts:34,61,70.Blocked URL: ${reason}even discloses which SSRF rule fired. Fix: generic client errors, detailed server logs. - Contradictory localhost policy in the guard —
lib/ssrf-guard.ts:39-48. Dev carve-out forhttp://localhostis immediately undone byBLOCKED_HOSTS; signals the guard wasn't tested end-to-end. Fix: resolve the contradiction; key protocol policy off an explicit flag, notNODE_ENV.
⚪ Info
utils/cronhost.tsis dead/legacy stub code — confirmed no imports anywhere. Fix: delete it.
3. nftcache (Go service)
Files:
cmd/nftcache/main.go,internal/config/config.go,internal/fetcher/rpc.go,internal/fetcher/alchemy.go,internal/store/store.go,go.mod
A BadgerDB-backed stale-while-revalidate cache fronting per-token ownerOf scans. Auth and address validation exist but the authorization ordering is fragile and the scan design is a DoS amplifier.
🔴 Critical
- API-key check ordering is fragile —
cmd/nftcache/main.go:82-124,239-245. The key is checked after param/config resolution; gating currently works but any future edit touching RPC before line 122 would expose an unauthenticated amplifier. Fix: check the key first thing in every handler (middleware). - Serial RPC scan is a DoS amplifier —
internal/fetcher/rpc.go:55-113, called frommain.go:172,245. One request → up tomaxTokenId(cap 10000) serialeth_calls against the upstream, throttled by a global 5 TPS limiter, for any caller-suppliednetwork+nft_contract. 1 HTTP request → up to 10k upstream calls, and one abusive scan starves all legitimate traffic. Fix: reject contracts not in the YAML allowlist; bound per-request concurrency; per-call deadlines.
🟠 High
- Non-constant-time API-key compare —
main.go:123,239,262. Fix:crypto/subtle.ConstantTimeCompare. - No HTTP-server timeouts & no graceful shutdown —
main.go:303,main(). Default server (no Read/Write/Idle timeouts → slowloris), no SIGTERM handler,cache.Close()never called (Badger not flushed). Fix: explicithttp.Servertimeouts +server.Shutdown+defer cache.Close(). - No context/timeout on RPC calls; scans uncancellable —
rpc.go:23,227;FetchAllTokenOwnerstakes nocontext. Hung upstream blocks a scan goroutine indefinitely; client disconnect doesn't abort a 10k-call scan; background refreshes are fire-and-forget. Fix:http.Client{Timeout}, threadcontext, derive refresh ctx from server lifetime. - Stale-refresh thundering herd (no single-flight) —
main.go:143-162. N concurrent stale requests launch N full scans of the same contract. Fix:singleflightper contract key.
🟡 Medium
- Serial scanning instead of batch/multicall —
rpc.go:65-109. 1000 tokens ≥200s, 10000 ≥33min. Multicall3 (same address on eth/arb/base) or JSON-RPC batch, ERC721Enumerable, or the already-implemented-but-unused AlchemygetNFTsForOwnerwould collapse this. Fix: adopt multicall/Alchemy path. (See Performance Audit.) - Upstream/internal error detail leaked to clients —
main.go:182,246,267. Fix: generic messages, server-side logs. - Rate-limit handling deletes cache → feedback loop —
main.go:151-154,177-180. On 429 exhaustion it deletes the entry, so the next request rescans into the already-limited upstream; detection is brittle substring matching. Fix: keep stale data + back off; typed/sentinel errors. - No
networkallowlist; CORS supports*—main.go:83,38-52. Bad networks fail deep in the scan;CORS_ALLOW_ORIGIN=*is allowed (default empty is safe). Fix: validatenetworkup front; document*must not pair with credentialed flows.
🟢 Low
- Leftover "Token 103" debug + unconditional
fmt.Printf—rpc.go:106-111,main.go:197-199. Fix: remove special-cases; gate behind a debug flag; uselogconsistently. canonAddraccepts any 40-hex string; duplicated in two files —main.go:64-80,rpc.go:141-157. Fix: combine with the allowlist; dedupe the helper.json.Marshalerrors ignored in store writes —internal/store/store.go:50,58. Fix: check & propagate.
⚪ Info
- No Badger-layer TTL / value-log GC —
store/store.go:49-66. Entries never expire on disk (app-side SWR only) → unbounded growth. Fix: optional Badger TTL + periodicRunValueLogGC. - Rate limiter is approximate (burst >5 TPS) + refill goroutine leaks on exit —
rpc.go:24-43. Acceptable for a soft limiter; usex/time/rateif strictness is needed.
Stale dependencies
go 1.22.0 (current 1.24+), badger/v4 v4.2.0, and several stale indirect deps with advisories — notably golang.org/x/net v0.7.0 (HTTP/2 rapid-reset family), google.golang.org/protobuf v1.28.1 (< 1.33.0 JSON unmarshal loop), golang.org/x/sys v0.5.0. Fix: go get -u ./..., bump the Go directive, run govulncheck.
4. Infrastructure & ops
Files:
docker-compose.yml,nginx/nginx.conf,.env.example,.env/.env.local,config/contracts.yaml, bothDockerfiles,next.config.ts
Self-hosted Compose stack. Architecture is reasonable (internal services not port-published, non-root images, a real CSP) but secret handling and the nginx edge need work.
🔴 Critical
- Secret shipped to the client via
NEXT_PUBLIC_—.env/.env.local. A real Schedy API key is set as bothSCHEDY_API_KEYandNEXT_PUBLIC_SCHEDY_API_KEY; the latter is inlined into the JS every browser downloads. A straymanwe-secretduplicates the WalletConnect id. Fix: delete theNEXT_PUBLIC_variant, proxy Schedy server-side, rotate the key, remove the stray line. - Live third-party secrets on disk —
.env,mortgagefi-frontend/.env.local. Contains realALCHEMY_API_KEY,NFTCACHE_API_KEY,SCHEDY_API_KEY,CRONHOST_API, and a Gmail SMTP app password. Root.gitignorecovers them and this isn't a git repo today, but onegit init/sub-repo commit away from exposure. Fix: secret manager / Docker secrets; verify the frontend subdir's own.gitignorecovers.env.local; ensure Docker build context doesn't bake them in (next finding).
🟠 High
- Weak hardcoded default Redis token —
docker-compose.yml:16,76;.env.example:23.SRH_TOKEN/UPSTASH_REDIS_REST_TOKENdefault tolocal-redis-token-change-me, shipped as a working value in.env.example; the stack is likely running with it. Anyone on the Docker network can then read/write all scheduling state. Fix:${REDIS_REST_TOKEN:?set me}(fail closed); set a strong random token. COPY . .may bake.envinto the frontend image —mortgagefi-frontend/Dockerfile:17. No confirmed.dockerignore; the.env.localtarget lives inside the frontend dir, andNEXT_PUBLIC_*values inline into the bundle in the published image…/mortgagefi-frontend:alert. Fix: add.dockerignore(.env*,node_modules,.next,data/); inspect the published image; rotate anything baked in.- No rate limiting / body-size limit / edge auth on proxied paths —
nginx/nginx.conf. Port 80 forwards/ntfy/(full UI + publish API),/nftcache/, and the app with nolimit_req,limit_conn, orclient_max_body_size; security rests entirely on each service's own auth (ntfy default is open). Fix: addclient_max_body_size+limit_reqzones;auth_basic/IP-allowlist/ntfy/; setNTFY_AUTH_DEFAULT_ACCESS=deny-all.
🟡 Medium
- Cron loop swallows all errors + plaintext Bearer + spurious deps —
docker-compose.yml:84-102.curlimages/curl:latestloop with|| truehides every failure (no alerting/dead-man's-switch); depends onredisit never uses. Fix: log non-2xx (drop|| true), pin the image, drop the redis dep, confirm/api/cron401s on bad secret and isn't reachable via nginx/. - Mutable /
:latestimage tags —docker-compose.yml:11,23,67,85,105.serverless-redis-http:latest, untaggedntfy, mutablemortgagefi-frontend:alert,curl:latest. Fix: pin to digests/immutable tags. - No healthchecks on any service —
docker-compose.yml.depends_ononly waits for start, not readiness → startup 502s, no auto-restart on hung process. Fix: addhealthcheck+condition: service_healthy.
🟢 Low
- Duplicate YAML key in nftcache config —
config/contracts.yaml:10-18.cbbtcdefined twice (placeholder vs real); YAML silently keeps the last. Fix: remove the placeholder. - Security-header drift between nginx and Next.js —
nginx/nginx.conf:6-9,74-79vsnext.config.ts. nginx sets no CSP; Next.js does; overlap can diverge on error pages. Fix: centralise headers in one layer (prefer the Next.js CSP).
⚪ Info
proxy_buffering offis global —nginx/nginx.conf:12. Needed for ntfy SSE but applied everywhere → higher upstream hold time. Fix: scope it to/ntfy/.- ntfy uses a personal Gmail app password —
docker-compose.yml:31-34. Fix: prefer a scoped transactional-mail credential; rotate if ever shared.
5. Dependencies & build
Files:
mortgagefi-frontend/package.json,tsconfig.json,next.config.ts,eslint.config.mjs,nftcache/go.mod,submodules/schedy
Core stack is modern and coherent (Next 16, React 19, wagmi v3 + viem v2, TS strict). The issues are dead weight and a few hardening gaps.
🟠 High
- Deprecated, unused, version-mismatched
@web3modal/wagmi@^5—package.json. v5 targets wagmi v2 but the project runs wagmi v3; it's also superseded by Reown AppKit — and it isn't imported anywhere. Fix: remove it; adopt@reown/appkit+@reown/appkit-adapter-wagmionly if a connect modal is later needed. (Listed High, not Critical: unused = not exploitable, but it's deprecated + mismatched.) - Unused redundant
@web3-react/*stack —package.json.@web3-react/core@^8,@web3-react/injected-connector@^6are unmaintained and fully unused; the app is bare wagmi v3. Fix: remove both; usewagmi/connectorsif needed. - Orphaned
schedysubmodule still built & run by Docker —submodules/schedy/,docker-compose.yml,Dockerfile:22. Scheduling moved to Next.js API routes + Upstash; all in-sourceschedyreferences are now legacy-prune code or an SSRF-allowlist token, yet Docker still builds the Go scheduler and setsNEXT_PUBLIC_SCHEDY_URL. Fix: confirm nothing deploys it, then delete the submodule, theschedyservice/depends_on, andNEXT_PUBLIC_SCHEDY_URL; clean residual comments/keys.
🟡 Medium
- nftcache Go module on an old toolchain with stale transitive deps —
nftcache/go.mod. (Same set as §3.) Fix: bump Go +go get -u ./...+govulncheck. - CSP allows
'unsafe-eval'and'unsafe-inline'for scripts —next.config.ts. Meaningfully weakens XSS protection for a wallet-signing dApp. Fix: move toward nonce/hash-basedscript-src, drop'unsafe-eval'if wagmi/walletconnect tolerate it; drop the deprecatedX-XSS-Protection; consider adding HSTS.
🟢 Low
- Packages a minor behind (some security-relevant) —
npm outdated:next16.0.10 → 16.2.9,wagmi3.1.0 → 3.6.16,viem2.42.1 → 2.52.2,@tanstack/react-query,tailwindcss,framer-motion. Treat Next.js patch bumps as security-relevant. Fix: run in-range minors; evaluate eslint 10 / TS 6 majors separately. - Minimal ESLint config —
eslint.config.mjs. Only Next defaults; nono-floating-promises/no-misused-promisesdespite fire-and-forgetawait fetchpatterns. Fix: add@typescript-eslintrecommended-type-checked rules.
⚪ Info
- TS
target: ES2017is dated —tsconfig.json(strict is otherwise good). Fix: optionally raise to ES2022, considernoUncheckedIndexedAccess.
What's done well
- On-chain
decimals/symboland BigInt/parseUnitsmath throughout — no float drift in money paths; payments capped to the reset-timer amount. - Genuine security scaffolding: RPC allowlist, known-preset compound guard, SSRF allowlist + header sanitisation,
CRON_SECRET-gated cron that fails closed, future-dating bounds onexecute_at, unguessablecrypto.randomUUID()task ids. - nftcache: API key required at startup (fails fast if unset), CORS closed by default, correct Badger transaction usage, stale-while-revalidate for latency, 429 backoff.
- Infra: internal services not port-published, non-root + distroless images,
CGO_ENABLED=0, read-onlyconfigmount, consistentplatformpinning, a real CSP + security-header baseline, correct.gitignore,output: 'standalone'. - Clean, self-contained scheduling migration to Next.js API routes + Upstash Redis.
Related
Home · Architecture · Security Audit · Performance Audit · Deployment · Development