Deep base-layer audit — verified findings
Read-only audit of the app foundation (money/decimal conversion, state/async, error handling, null-safety), run against a fresh pinned clone of develop @ fff5035.
Method (why these are trustworthy, not noise)
- Full CI-equivalent run first:
generate_localization + generate_release_info + build_runner → flutter analyze (0 issues) → flutter test (2380 pass, 1 fail). The single failure is the known home_golden_test macOS pixel drift (0.24 %), not a logic bug.
- 25 hypotheses were generated, then each was turned into a reproducing unit test. Only findings with a red test (or behaviour-confirming test) are listed as bugs. 15 of 25 hypotheses were refuted by guards already present in the code and are deliberately not listed as bugs.
A. Verified, user-visible
| # |
Location |
Symptom |
Severity |
| 19 |
lib/screens/legal/subpages/legal_document_page.dart:62 |
On asset-load failure the catch sets _markdownContent = '', which build treats as loaded → blank legal page, no error message, no retry. |
Medium |
| 4–7 |
dfx_price_service.dart:35/38/64/66, price_chart.dart:69/113, portfolio_chart.dart:104, real_unit_account_service.dart:38 |
BigInt.from(double * 100) truncates toward zero (e.g. 0.29 * 100 = 28.999… → 28) → price/chart labels can read one cent low. |
Low (cosmetic) |
| 22 |
lib/styles/currency.dart:10 via buy/sell payment-info DTOs (real_unit_*_payment_info_dto.dart) |
Currency.fromCode uses firstWhere without orElse → StateError on an unsupported backend currency. Not a crash: both payment cubits catch it and emit PaymentInfoError.unknown, so the user sees a generic error and the log shows a cryptic Bad state: No element. Robustness/clarity hardening. |
Low |
B. Verified, internal (not directly user-visible)
| # |
Location |
Symptom |
Severity |
| 14 |
lib/screens/kyc/cubits/kyc/kyc_cubit.dart:48 |
Future.timeout does not cancel the underlying getKycStatus/getUser work. Rapid retries stack multiple concurrent in-flight requests (test observed 3). State stays correct (generation guard), but it leaks requests/connections. |
Medium |
C. Candidate — needs verification before any fix
| # |
Location |
Hypothesis |
| 15 |
lib/setup/lifecycle_initializer.dart:61 |
unawaited(lockCurrentWallet()) on the hidden lifecycle event: if the lock fails/times out, the seed may stay unlocked in memory while the app is backgrounded. Security-relevant if confirmed. Not reproducible at cubit-mock level — needs a lifecycle/widget test with a mocked WalletService whose lockCurrentWallet() errors. Listed as a candidate, not a confirmed bug. |
Plan
Reviewed and deliberately not pursued
- Decimal fiat-input rounding in buy/sell (
double.parse(amount).round()) — the amount pipeline is integer-only by API contract; treated as by-design.
- 15 refuted hypotheses, where a guard already exists: EIP-7702 wei mismatch guard (
expectedWei != actualWei), balances.isEmpty guard, isClosed/sequence guards before every emit in the converter/bitbox cubits, the id != null / pdfUrls != null null-guards, the empty-array balance guard, the deliberately nullable rate/?? 0 mappings, and the documented graceful-degradation paths in the converters / KYC registration.
Findings were verified with throwaway probe tests; no product code was changed during the audit.
Deep base-layer audit — verified findings
Read-only audit of the app foundation (money/decimal conversion, state/async, error handling, null-safety), run against a fresh pinned clone of
develop@fff5035.Method (why these are trustworthy, not noise)
generate_localization+generate_release_info+build_runner→flutter analyze(0 issues) →flutter test(2380 pass, 1 fail). The single failure is the knownhome_golden_testmacOS pixel drift (0.24 %), not a logic bug.A. Verified, user-visible
lib/screens/legal/subpages/legal_document_page.dart:62_markdownContent = '', whichbuildtreats as loaded → blank legal page, no error message, no retry.dfx_price_service.dart:35/38/64/66,price_chart.dart:69/113,portfolio_chart.dart:104,real_unit_account_service.dart:38BigInt.from(double * 100)truncates toward zero (e.g.0.29 * 100 = 28.999… → 28) → price/chart labels can read one cent low.lib/styles/currency.dart:10via buy/sell payment-info DTOs (real_unit_*_payment_info_dto.dart)Currency.fromCodeusesfirstWherewithoutorElse→StateErroron an unsupported backend currency. Not a crash: both payment cubits catch it and emitPaymentInfoError.unknown, so the user sees a generic error and the log shows a crypticBad state: No element. Robustness/clarity hardening.B. Verified, internal (not directly user-visible)
lib/screens/kyc/cubits/kyc/kyc_cubit.dart:48Future.timeoutdoes not cancel the underlyinggetKycStatus/getUserwork. Rapid retries stack multiple concurrent in-flight requests (test observed 3). State stays correct (generation guard), but it leaks requests/connections.C. Candidate — needs verification before any fix
lib/setup/lifecycle_initializer.dart:61unawaited(lockCurrentWallet())on thehiddenlifecycle event: if the lock fails/times out, the seed may stay unlocked in memory while the app is backgrounded. Security-relevant if confirmed. Not reproducible at cubit-mock level — needs a lifecycle/widget test with a mockedWalletServicewhoselockCurrentWallet()errors. Listed as a candidate, not a confirmed bug.Plan
Reviewed and deliberately not pursued
double.parse(amount).round()) — the amount pipeline is integer-only by API contract; treated as by-design.expectedWei != actualWei),balances.isEmptyguard,isClosed/sequence guards before everyemitin the converter/bitbox cubits, theid != null/pdfUrls != nullnull-guards, the empty-array balance guard, the deliberately nullable rate/?? 0mappings, and the documented graceful-degradation paths in the converters / KYC registration.Findings were verified with throwaway probe tests; no product code was changed during the audit.