Skip to content

hard: registration wallet-existence checks are TOCTOU — concurrent registers create duplicate identities #120

Description

@EmeditWeb

Problem

AuthService.register() (src/modules/auth/auth.service.ts, lines 46–77) enforces uniqueness application-side: findByWallet() then checkUsernameExists() then createProfile() — three separate round trips with no transaction and no reliance on a database-level unique constraint. Two concurrent registrations with the same wallet (double-click, retry storm, deliberate race) both observe "no existing wallet" and both insert. Same for usernames.

Downstream blast radius: identity lookups key on wallet address throughout the platform (loans, reputation reads, liquidity summaries). Duplicate profiles mean some requests resolve to identity A while others hit identity B — split credit history, split balances display, and an authentication ambiguity where findOrCreateUser()'s upsert (line 148) silently adopts whichever row the onConflict target selects. On-chain, one wallet maps to one borrower; off-chain now mapping to two profiles corrupts every reconciliation job.

If the users.wallet_address unique constraint exists in the schema, the second insert errors — but the current code surfaces it as a generic 500 rather than AUTH_WALLET_EXISTS, and username uniqueness handling is equally accidental.

Ground Rules

  1. Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
  2. Read src/database/repositories/users.repository.ts in full
  3. Inspect the actual table schema/constraints before coding; do not assume them

What To Build

  1. Ensure DB-level UNIQUE constraints exist on users.wallet_address and users.username; add a migration if missing.
  2. Rewrite register() to attempt the insert directly and map unique-violation errors to the existing structured 409 codes (AUTH_WALLET_EXISTS, AUTH_USERNAME_TAKEN) — eliminating the pre-check race entirely.
  3. Wrap profile creation + avatar upload + token issuance failure handling so a failed registration does not leave orphaned avatar objects.
  4. Tests: parallel duplicate-wallet registrations yield exactly one profile and one 409; duplicate username likewise; sequential re-register returns the same structured 409 as today (compatibility).

Files To Touch

  • src/modules/auth/auth.service.ts
  • src/database/repositories/users.repository.ts
  • migration file
  • tests
  • relevant docs/progress tracker

Acceptance Criteria

  • Uniqueness enforced by the database, not by racy pre-checks
  • Error responses unchanged for legitimate callers
  • Race covered by automated test
  • Suite green

Mandatory Checks Before Opening PR

Standard checklist applies. PRs failing any check will be closed without review.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions