Summary
In src/controllers/crypto.controller.ts, the balance check and wallet update are two separate DB operations with no atomicity:
const wallet = await prisma.wallet.findUnique(...); // read
if (wallet.ngnBalance < totalDebit) { return 400; } // check
await prisma.wallet.update({ data: { ngnBalance: wallet.ngnBalance - totalDebit } }); // write
Impact
Two concurrent POST /crypto/buy requests with the same token both pass the balance check before either write commits — effectively double-spending the same funds.
Steps to reproduce
- Fund a wallet with N5,000.
- Fire two simultaneous buy requests each for N4,000.
- Both pass the
ngnBalance < 4000 check and both writes succeed — balance goes negative.
Proposed fix
Use a conditional updateMany that asserts the balance at write time:
const updated = await prisma.wallet.updateMany({
where: { userId: req.userId!, ngnBalance: { gte: totalDebit } },
data: { ngnBalance: { decrement: totalDebit } },
});
if (updated.count === 0) {
return res.status(400).json({ success: false, message: 'Insufficient balance' });
}
Same fix needed in:
src/controllers/wallet.controller.ts — sendPayment
src/controllers/utility.controller.ts — buyAirtime, buyData, payElectricity, payCableTv
src/controllers/savings.controller.ts — depositToBox
Summary
In
src/controllers/crypto.controller.ts, the balance check and wallet update are two separate DB operations with no atomicity:Impact
Two concurrent
POST /crypto/buyrequests with the same token both pass the balance check before either write commits — effectively double-spending the same funds.Steps to reproduce
ngnBalance < 4000check and both writes succeed — balance goes negative.Proposed fix
Use a conditional
updateManythat asserts the balance at write time:Same fix needed in:
src/controllers/wallet.controller.ts—sendPaymentsrc/controllers/utility.controller.ts—buyAirtime,buyData,payElectricity,payCableTvsrc/controllers/savings.controller.ts—depositToBox