Repository navigation
Vault balance update is a read-modify-write race: concurrent deposits lose funds #421
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdatabaseImported from PRODUCTION_ISSUES.mdImported from PRODUCTION_ISSUES.mdfinancialImported from PRODUCTION_ISSUES.mdImported from PRODUCTION_ISSUES.mdreliabilityImported from PRODUCTION_ISSUES.mdImported from PRODUCTION_ISSUES.md
on Aug 20, 2026 I'd like to be assigned to this issue. I'll deliver a clean, maintainable solution that matches the requirements.
Payout wallet (Stellar): GCJS6J54NK4FJFT5I4AUIOSFONKAM7GI6RRHWYH55TUY2WZAEIH2Q5D7
GrantFox OSS campaign — smart escrow payout on merge.TechBroAfrica commented
on Aug 20, 2026 More actionsFocused on building trust through consistent, meaningful contributions. I approach every issue with curiosity, attention to detail, and a commitment to delivering clean, maintainable, and well-tested solutions. Driven by impact, not just commits, I value clear communication, effective collaboration with maintainers, continuous learning, and delivering meaningful value to every project I contribute to.
Hi team, I'd like to work on this issue. I'm a full-stack developer experienced in TypeScript, Rust, and smart contract development across EVM and Soroban/Stellar ecosystems. I've built and shipped production systems covering backend APIs, frontend interfaces (Next.js, React), CI/CD pipelines, and onchain protocols — so I'm comfortable working across whatever layer this issue touches. My approach I'll start by reproducing the issue locally and reading the surrounding code to understand context and constraints. I'll scope the fix tightly to avoid unnecessary churn, write or update tests to cover the change, and make sure linting, formatting, and existing CI checks all pass before opening a PR. If anything in the issue is ambiguous, I'll flag it early rather than guess. What you can expect A single clean PR with a clear description linking back to this issue. Minimal, well-tested changes that follow the project's existing conventions. Fast turnaround — I'm available to start immediately and responsive to review feedback. Happy to discuss the approach further if needed. Looking forward to contributing.
- addedGrantFox OSSIssue tracked in GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignCampaign: Third Campaign
on Aug 20, 2026 portableDD commented
on Aug 20, 2026 ContributorMore actionscan i work on this?
grantfox-oss commented
on Aug 20, 2026 grantfox-ossboton Aug 20, 2026 – with GrantFox OSSMore actions🦊 GrantFox — @portableDD has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
- Open a Pull Request referencing this issue (e.g.,
Closes #421) - Your PR will be reviewed by the Quantarq maintainers
Good luck! Track your progress on GrantFox.
- Open a Pull Request referencing this issue (e.g.,
- added 2 commits that reference this issue
on Aug 20, 2026 grantfox-oss commented
on Aug 22, 2026 grantfox-ossboton Aug 22, 2026 – with GrantFox OSSMore actions🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@portableDD's PR #434 was approved and merged by @YaronZaki.
🏆 @portableDD: You earned 40 FoxPoints for this contribution! Your current tier: Builder (1,105 total points). Track your full progress on GrantFox.
👏 Great work, @portableDD! Keep contributing to Quantarq.
Labels / Complexity: bug, financial, database, Backend, reliability · Extremely High — 500
Problem
DepositDBConnector.add_vault_balanceinquantara/web_app/db/crud/deposit.pyis a non-atomic read-modify-write:Two concurrent requests for the same
(wallet_id, symbol)both read the samevault.amount, each computeold + their_amount, and the second write overwrites the first — a lost update. On a money ledger this silently discards one of the deposits.A related defect is in the data model and the deposit path:
create_vault(quantara/web_app/db/crud/deposit.py) always inserts a newVaultrow on every/api/vault/deposit, and theVaulttable (quantara/web_app/db/models.py) has no unique constraint on(user_id, symbol).get_vaultreturns.first(), so once more than one row exists for a user+symbol,add_vault_balanceupdates an arbitrary row while others accumulate stale balances — the total is never the sum the user actually deposited.The codebase already contains the correct pattern for exactly this situation:
add_extra_deposit_to_positioninquantara/web_app/db/crud/position.pyuses a PostgreSQLinsert(...).on_conflict_do_update(...)withcast(amount, Numeric) + cast(amount, Numeric)to atomically upsert. The vault path does not reuse it.Root cause
Why this is architecturally hard
SELECT ... FOR UPDATE— requires a single transaction that spans the read and the write, butget_vaultopens its ownSessionand closes it before returning, so the contributor must restructure the method into one session/transaction rather than bolting a lock onto the current split.INSERT ... ON CONFLICT (user_id, symbol) DO UPDATE SET amount = amount + excluded.amount), which requires adding the missing unique constraint to theVaultmodel — an Alembic migration and a data-cleanup decision for any existing duplicate rows.amountis stored as aString(Vault.amountandPosition.amountareString), so arithmetic must go throughDecimal/NUMERICcasting inside SQL, exactly asadd_extra_deposit_to_positionalready does — the contributor must reconcile string storage with atomic numeric addition.create_vaultandadd_vault_balanceare two divergent write paths (one inserts, one updates-first-row); unifying them into one upsert is the real fix, and the API endpoints inquantara/web_app/api/vault.pymust both route through it.Proposed design
The maintainer decision is whether to add a unique constraint on
(user_id, symbol)and migrate existing rows, or to key the upsert by an explicit natural key.Downstream impact
Adding a unique constraint on
(user_id, symbol)requires an Alembic migration (quantara/web_app/alembic/) and reconciliation of any existing duplicateVaultrows. The response shape of/api/vault/depositand/api/vault/add_balanceneed not change.Acceptance criteria
Database
(user_id, symbol)forVault.add_vault_balance(or its replacement) performs the increment atomically in SQL.Service
(wallet_id, symbol)are not lost: N concurrent deposits of X produce a final balance of N*X.create_vaultandadd_vault_balanceconverge on a single upsert path.Tests
cd quantara && poetry run pytest web_app/tests -k vault.Out of scope
Do not migrate
Position.amount/ExtraDeposit.amountstorage in this issue; only fix theVaultbalance path.Getting started
Files in scope:
quantara/web_app/db/crud/deposit.py,quantara/web_app/db/models.py,quantara/web_app/api/vault.py, plus an Alembic migration. Verify with:Good first files to read:
quantara/web_app/db/crud/deposit.py,quantara/web_app/db/crud/position.py(theon_conflict_do_updatepattern),quantara/web_app/db/models.py.