The Hub.eliminateDeficit() function has a 1e18 scaling error.
When bad debt occurs, it's tracked in RAY precision (1e27). When someone tries to repay,
the code converts by 1e9 instead of 1e27. Result: you would need 1 quintillion USDC
to repay 1 USDC of bad debt. The protocol becomes permanently insolvent.
Aave uses a precision library called WadRayMath for handling decimal math in Solidity. Understanding this library is essential to understanding the bug.
| Unit | Precision | Multiplier | Use Case |
|---|---|---|---|
| WAD | 18 decimals | 1e18 | Token amounts (ETH, most ERC20) |
| RAY | 27 decimals | 1e27 | Interest rates, indexes, internal accounting |
// WadRayMath.sol
// WAD to RAY: multiply by 1e9
function toRay(uint256 a) returns (uint256) {
return a * 1e9; // 1e18 * 1e9 = 1e27
}
// RAY to WAD: divide by 1e9 (rounds up)
function fromRayUp(uint256 a) returns (uint256) {
return (a + 1e9 - 1) / 1e9;
}
The critical insight: toRay() is designed for converting
WAD (1e18) to RAY (1e27). It multiplies by 1e9.
When a position becomes underwater and gets liquidated, any remaining debt becomes "bad debt"
(deficit). This deficit is tracked in deficitRay using RAY precision.
// When bad debt occurs (simplified)
uint256 drawnShares = position.debt; // e.g., 1e6 (1 USDC worth of shares)
uint256 drawnIndex = reserve.index; // e.g., 1e27 (RAY precision)
// Deficit stored as: shares * index = assets in RAY
deficitRay = drawnShares * drawnIndex; // 1e6 * 1e27 = 1e33
// Hub.sol - eliminateDeficit()
function eliminateDeficit(address asset, uint256 amount) external {
uint256 deficitRay = reserve.deficitRay;
// THE BUG: toRay() multiplies by 1e9, but deficit is assets * 1e27
uint256 deficitAmountRay = (amount < deficitRay.fromRayUp())
? amount.toRay() // WRONG! Scales by 1e9 instead of 1e27
: deficitRay;
// This subtraction barely makes a dent
reserve.deficitRay = deficitRay - deficitAmountRay;
}
Scenario: 1 USDC of bad debt
User attempts to repay 1 USDC:
After "repayment":
To eliminate 1e33 deficit with the broken formula:
Total USDC in existence: ~40 billion ($4e10)
Market crash causes underwater positions. Protocol accumulates $1M in bad debt.
DAO allocates $1M USDC to eliminate deficit. Transaction succeeds but debt unchanged.
Team discovers the $1M was consumed but debt remains at 99.9999999999999999%.
Bad debt can never be cleared. Protocol is permanently insolvent. Users cannot fully withdraw.
CRITICAL - Once any bad debt occurs, the protocol becomes permanently insolvent. The deficit elimination mechanism is mathematically broken. This is not an edge case - it affects 100% of bad debt repayment attempts.
// Foundry test demonstrating the bug
function test_DeficitCanNeverBeRepaid() public {
// Setup: Create 1 USDC of bad debt
uint256 badDebtAmount = 1e6; // 1 USDC
uint256 deficitRay = badDebtAmount * 1e27; // 1e33
// User attempts to repay the full amount
uint256 repayAmount = badDebtAmount; // 1 USDC
// Simulate eliminateDeficit logic
uint256 deficitAmountRay = repayAmount.toRay(); // 1e6 * 1e9 = 1e15
uint256 remaining = deficitRay - deficitAmountRay;
// Assert that debt is essentially unchanged
assertGt(remaining, deficitRay * 99 / 100); // >99% remains
// Calculate how many USDC would actually be needed
uint256 requiredUsdc = deficitRay / 1e9; // 1e24 USDC
assertEq(requiredUsdc, 1e24); // 1 quintillion USDC
}
The fix is straightforward: scale by 1e27 (RAY) instead of 1e9 (WAD-to-RAY conversion).
// BEFORE (Broken)
uint256 deficitAmountRay = amount.toRay(); // 1e9 scaling
// AFTER (Fixed)
uint256 deficitAmountRay = amount * 1e27; // Direct RAY scaling
// Or use a dedicated function:
function assetToDeficitRay(uint256 assets, uint256 index) returns (uint256) {
return assets * index; // Matches how deficit is recorded
}
The deficit is stored as assets × index (where index is in RAY).
Repayment must use the same formula. The bug used toRay() which
assumes the input is already in WAD - but USDC has only 6 decimals.