Withdraw, Then Withdraw Again — JavaScript Bug Hunt
Modelled on The DAO attack (June 2016): The DAO's contract sent ether to a caller before updating the caller's balance.
- Language: JavaScript
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Money, State
- Modelled on: The DAO · 2016
- Visible tests: an honest withdrawal pays the balance; re-entering withdraw pays nothing extra
- Reward: 50 XP for a complete fix
Briefing
Modelled on The DAO attack (June 2016): The DAO's contract sent ether to a caller before updating the caller's balance. The recipient was itself a contract whose receive hook called straight back into the withdrawal, and every nested call still saw the untouched balance. Roughly a third of The DAO's funds were drained, and Ethereum hard-forked to reverse it.
vault.js has the same order of operations. The re-entrant recipient is modelled deterministically in wallets.js: its receive calls withdraw again a fixed number of times.
Fix withdraw so a nested call can never pay out the same balance twice.
Bug report
BUG-REENTRY · Priority: Critical (funds drained) · Reported by: audit
vault.withdraw(who, wallet):
- pays the caller's whole balance by calling wallet.receive(amount, vault) once and returns amount
- a balance of 0 returns 0 without calling receive
- receive may call vault.withdraw again (re-entrancy); that nested call must see the balance as already paid out and return 0
- after any sequence of calls, vault.reserve equals the sum of the balances still held
- if receive throws, the withdrawal is undone — the balance and the reserve are restored — and the error propagates
Observed: an attacker with a balance of 10 re-entered three times, was paid 40, and the honest depositor's 30 could no longer be withdrawn.
Logs
[vault] withdraw mallory 10 (depth 4)
[vault] reserve 40 -> 0, balances { mallory: 0, alice: 30 }
[vault] withdraw alice: Error: insufficient reserveThe code as shipped
src/vault/vault.js (editable)
exports.createVault = function () {
var vault = { balances: {}, reserve: 0 };
vault.deposit = function (who, amount) {
vault.balances[who] = (vault.balances[who] || 0) + amount;
vault.reserve += amount;
};
vault.withdraw = function (who, wallet) {
var amount = vault.balances[who] || 0;
if (amount === 0) return 0;
if (amount > vault.reserve) throw new Error("insufficient reserve");
wallet.receive(amount, vault);
vault.reserve -= amount;
vault.balances[who] = 0;
return amount;
};
return vault;
};
Read-only context: src/vault/wallets.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.