Refunds for Money Never Taken — JavaScript Bug Hunt

Modelled on Revolut's US payments flaw (2022, reported by the Financial Times in 2023): a problem in how Revolut's US card system handled certain declined…

  • Language: JavaScript
  • Layer: Database
  • Difficulty: Easy
  • Concepts: Money, Validation, State
  • Modelled on: Revolut · 2022
  • Visible tests: a settled charge is refunded; a declined charge is never refunded
  • Reward: 50 XP for a complete fix

Briefing

Modelled on Revolut's US payments flaw (2022, reported by the Financial Times in 2023): a problem in how Revolut's US card system handled certain declined transactions meant refunds were issued from Revolut's own funds for payments whose money had never actually left the customer's account. Criminals exploited it before it was closed; the reported losses were around $20 million.

reversal.js credits a customer when the card network reports a transaction as failed or reversed. It credits every reversal it is told about.

Fix reverse so a refund only ever returns money that was actually taken, once.

Bug report

BUG-REV-US · Priority: Critical (direct financial loss) · Reported by: finance

reverse(store, txId) returns the number of cents credited:

  • unknown txId -> 0, nothing changes
  • tx.debited is false (declined / failed before settlement: the customer never lost the money) -> 0, nothing changes (tx.refunded stays as it was)
  • tx.refunded already true -> 0, nothing changes
  • otherwise credit store.accounts[tx.account] with tx.amount, set tx.refunded = true, and return tx.amount

Observed: declined transactions are "refunded" — the customer's balance goes up by money they never spent, and the same reversal pays out again when the network resends it.

Logs

[card-us] tx=tx_991 status=DECLINED debited=false
[reversal] tx_991 credited 50000 to acct_17
[recon] ledger short by 50000 against settlement file

The code as shipped

src/payments/reversal.js (editable)

// Handles a reversal notice from the card network.
exports.reverse = function (store, txId) {
  var tx = store.txs[txId];
  if (!tx) return 0;
  store.accounts[tx.account] += tx.amount;
  tx.refunded = true;
  return tx.amount;
};

Read-only context: src/payments/store.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.