The Donation That Skipped the Health Check — JavaScript Bug Hunt
Modelled on the Euler Finance exploit of 13 March 2023: the lending protocol's donateToReserves function let an account give away its collateral without the…
- Language: JavaScript
- Layer: Backend
- Difficulty: Medium
- Concepts: Money, Validation
- Modelled on: Euler Finance · 2023
- Visible tests: a donation that keeps the account healthy goes through; a donation that would leave the account unhealthy reverts
- Reward: 50 XP for a complete fix
Briefing
Modelled on the Euler Finance exploit of 13 March 2023: the lending protocol's donateToReserves function let an account give away its collateral without the account-health (liquidity) check that other balance-reducing actions performed. The attacker used leverage, donated enough to leave the account under-collateralised, then liquidated it from a second account at a discount, taking about $197 million. The funds were later returned.
This project is a reconstruction. In pool.js, withdraw refuses to leave an account unhealthy; donateToReserves reduces collateral just the same, with no such check.
Fix donateToReserves so it can never leave an account unhealthy.
Bug report
BUG-EULER · Priority: Critical · Reported by: protocol security
Every action that reduces an account's collateral follows one rule:
- amount must be > 0 and <= the account's collateral, else throw
- after the change the account must satisfy risk.isHealthy; if it would not, throw and leave the account AND the pool exactly as they were
donateToReserves(pool, id, amount) additionally adds amount to pool.reserves when it succeeds, and returns the account.
Observed: donateToReserves lets an account push itself below the health line, after which it can be liquidated at a profit.
Logs
[pool] donate acct=0x5f amount=100000000 collateral->310000000 debt=390000000
[risk] acct=0x5f health=0.79 -> liquidatableThe code as shipped
src/lending/pool.js (editable)
var risk = require("./risk");
exports.create = function () {
return { reserves: 0, accounts: {} };
};
exports.open = function (pool, id, collateral, debt) {
pool.accounts[id] = { collateral: collateral, debt: debt };
return pool.accounts[id];
};
exports.withdraw = function (pool, id, amount) {
var acc = pool.accounts[id];
if (amount <= 0 || amount > acc.collateral) throw new Error("bad amount");
acc.collateral -= amount;
if (!risk.isHealthy(acc)) {
acc.collateral += amount;
throw new Error("e/collateral-violation");
}
return acc;
};
exports.donateToReserves = function (pool, id, amount) {
var acc = pool.accounts[id];
if (amount <= 0 || amount > acc.collateral) throw new Error("bad amount");
acc.collateral -= amount;
pool.reserves += amount;
return acc;
};
Read-only context: src/lending/risk.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.