Skip to content
All docs

Audit report (internal)

This is an internal review by the build assistant, not an independent third-party audit.

  • Date: 2026-10-08, about 15:30–16:30 UTC (16:30–17:30 Lagos).
  • Source: main at the commit that adds this file (based on 55832a1).
  • Hosted app (historical, 2026-10-08 15:56 UTC): returned 200 but served the copy from before this pass, and no deployed commit was exposed. Resolved 2026-10-09: every build now embeds its commit, served at /api/version, in the landing footer and on /status.

Findings marked "reported elsewhere" came from a separate review session whose patch was not available here. Each was reproduced in this repository before it was fixed.

Findings

IDSevWhereFindingFixRetest
A-01P1relayer/src/handler.tsA CORS preflight (OPTIONS) built a 204 response with a body. new Response("{}", { status: 204 }) throws, so every browser request to the relayer failed preflight. Earlier relayed trades worked only because they came from a Node script, which sends no preflight.Empty 204 with CORS headersNew regression test fails on the old code (TypeError: Invalid response status code 204) and passes now. Relayer suite 12/12.
A-02P1.github/workflows/ci.ymlpnpm/action-setup was given version: 11 while package.json pins pnpm@11.5.0. The action stops with "Multiple versions of pnpm specified", so CI has failed on every push.Version read from packageManager onlyCI green on all four jobs (run 37878125760, 2026-10-09)
A-03P2web/package.json, rootweb test ran Vitest over the Playwright files; lint called next lint, which Next.js 16 removed. pnpm test and pnpm lint failed.Removed both broken scripts. Root test runs the unit suites; test:e2e runs Playwright. No ESLint config exists, so no lint script is claimed.pnpm test: 45 passed
A-04P2app/package.jsonA fresh npm ci in the Expo app failed with ERESOLVE: react-dom 19.3.0 against react 19.2.3.Override react-dom to 19.2.3npm ci without --force succeeds; typecheck, 3/3 tests and the Android export pass
A-05P2AccountProvider.tsxA corrupted saved credential made unlocking throw on every attempt.Ignore and remove a corrupt entry; fall back to discoverable credentialsNew e2e test fails on the old code, passes now
A-06P2AccountProvider.tsxSigning out (or locking) while a passkey prompt was open: when the prompt completed, it signed the user back in.Generation counter; a late result is discardedNew e2e test (prompt held open, sign-out from another tab) fails on the old code, passes now
A-07P3AccountProvider.tsxWhen another tab switched to a different account, this tab cleared the old address but kept the old signing session in memory.The session is ended on the switchThe visible behaviour is tested; the in-memory session end is code review only, since it can't be observed from the browser
A-08P2webaxe-core (WCAG 2.2 A/AA) found serious issues: small muted text and short-side red below 4.5:1, chart role="img" containing focusable controls, a colour-only link in text, and docs tables that scroll but cannot be focused.Darker --color-muted, --color-long and --color-short; chart uses role="figure"; underlined in-text link; focusable scroll region around tablesa11y.spec.ts: 0 violations on 14 pages
A-09P2README, docs/FINAL_STATUS.md, SPONSOR_MATRIX.mdStale claims: graduation "pending" (it happened), "no mobile app", relayer and keeper "not running", and the tagline "Real capital" on a testnet product.Rewritten to match the receiptsManual review against proof/
A-11P1web/src/lib/history.tsWithout an indexer (the hosted site, and CI), History scanned in 1,000-block chunks; Monad's public RPC rejects eth_getLogs over 100 blocks, so History failed for every hosted visitor. Hidden locally because the local indexer was configured.100-block chunks, 5 at a time, last 5,000 blocks, labeled as suchNew no-indexer test fails on the old code, passes now
A-12P2ui.tsx, history.tsxLong raw RPC errors and long status pills overflowed narrow layouts (found by CI)Wrapping error text, shortened error copy, wrapping pillsCI-condition overflow sweep passes
A-13P2indexer/scripts/gen-config.mjsThe config generator required a local forge build, so it failed on a fresh clone and in CIFalls back to the committed ABIsCI typescript job passes
A-10P2landingThe first screen explained mechanism terms before what a trader actually does.New intro, six-step "How it works" with current thresholds, FAQRendered and checked at 390 and 1440 px

Not changed and why

  • Contracts: unchanged. SR-02 and SR-03 in SECURITY_REVIEW.md stay acknowledged; fixing them requires a redeploy, which would invalidate the testnet evidence.
  • Theme: light only with black primary buttons. This is the owner's decision; an earlier dark-mode request was superseded.
  • Profit claim: pending. No more trading is being done to chase it.

Open risks

  • The relayer, keeper health endpoint and indexer are not publicly hosted. The hosted site falls back to self-submitted transactions and direct chain reads, and says so on /status.
  • Economic: a price gap larger than the stake can cost the pool principal (paired-desk results). Perpl and Agora admin powers are outside Imprest's control.
  • Passkeys were tested with Chrome's virtual authenticator, not physical biometrics. Native passkeys need a linked domain.
  • No independent audit.

Browser walkthrough audit, 2026-10-09

The owner ran the full direct evaluation flow in a browser: faucet, approve, open desk, open and close a 0.001 BTC long, then close the desk with 99.943818 AUSD returned. Desk 0x4802…bFaD, trader 0x976C…25fE. A separate review of that session raised the findings below. Each was checked against the code before it was fixed. Regression tests: web/e2e/audit-fixes.spec.ts.

IDSevFindingCause (confirmed)Fix
B-01MediumA closed desk showed −100 AUSD realized PnL, a −94 risk buffer and "anyone can call enforce()"Pages rendered active-desk metrics for any desk, including a settled oneA closed desk now shows a settlement card: stake, total returned (totalPaidToTrader read from the contract) and net change (−0.056182 AUSD here). Risk, eligibility and enforcement text are hidden on the dashboard, Claims, Risk and Trade.
B-02MediumAfter sign-out, the previous account's numbers stayed until a refreshThe polling hook kept its last data when its key changed, and a late response could land after the switchData is cleared on every key change, and stale responses are ignored. The test fails on the old hook.
B-03MediumClaims said "Needs 0.00 AUSD more" while equity (99.94) was below the high-water mark (100)The shortfall ignored fees owed and was rounded to 2 decimalsShortfall = HWM + fees owed − realized equity + 1 unit, shown to 6 decimals when small, with the full condition explained
B-04MediumThe order preview showed distance to the desk floor while the daily limit was nearerThe preview always used floorUses the stricter of the desk floor and the active daily floor, and names it
B-05MediumFills showed "@ 82,639.6" as if it were the execution priceTradeExecuted records the limit and the mark, not an average fillShows "limit X · mark Y"; no fill price is claimed
B-06Medium"Send to contract anyway" sat in the normal close flow and cost 0.153 MONProminent buttonCollapsed "Developer" section with a gas warning and a confirmation dialog; never shown for a closed desk
B-07MediumThe GitHub About link pointed to a dead Vercel deploymentStale repository settingHomepage set to https://imprest-chi.vercel.app; description and topics added
B-08Medium/proof linked a mainnet address to the testnet explorerAddress and transaction links always used the app's networkLinks follow each claim's chain_id
B-09Medium"Gasless" was claimed more strongly than the browser evidence supportsCopyLanding, FAQ, README and /status now say relayed intents were verified from a script and gasless trading is not available on the website
B-10LowCopy: "flat desks can claim"; "No activity yet" in a bounded scan; implementation-speak on the slider; raw parser errors; zero estimates for invalid input; "% of drawdown buffer"; "keeper not running"; "principal at risk" ambiguous; closed desks with negative distances on LPCopy and stateAll rewritten: claim conditions, "nothing found in the scanned block range", allowed leverage, field-specific errors and "—" estimates, buffer in AUSD, "not configured here" versus "unreachable", current shortfall, "—" for closed desks
B-11Low/proof led with three PENDING mainnet items; evidence paths were plain textLayoutLeads with three completed testnet results with transaction links; pending items are in their own card; every evidence path links to the file on GitHub
B-12LowThe runbook's cast wallet new rendered as broken text on /docsThe Markdown renderer ignored indented code fencesIndented fences are supported
B-13LowThe deployed commit could not be determinedNot exposedEach build embeds its commit: /api/version, landing footer, /status
B-14LowStale docs: dark-only UI, "real AUSD" on testnet, mobile status, advice to keep trading for a claim, runbook says "not deployed"Docs driftKNOWN_LIMITATIONS, TESTNET_DEPLOYMENT, README, AUDIT_REPORT and TEST_RESULTS updated and dated

Confirmed as not a bug: the ReduceOnlyInvalid revert at block 69518585 came after the successful close at block 69518531. The position was already flat, so the contract correctly refused a reduce-only order. That order only reached the chain through the rejection-test button, which B-06 now moves out of the normal flow.

Still blocked, and not fixable in code:

  • gasless trading from the browser needs a publicly hosted relayer;
  • a funded-desk browser journey and a profit claim need a genuinely qualifying desk;
  • the physical-phone checklist needs the owner's device;
  • the hackathon rules need the signed-in dashboard;
  • an independent audit.