Partial liquidation can leave debt with no collateral and no write-off
src/LiquidationEngine.sol:L118–L164
- Likelihood
- Medium
- Impact
- High
- Weakness
- CWE-682
- Description
Property P-05 failed. When an account’s collateral value is close to its debt, the engine computes collateral to seize as
repay × (1 + incentive) / price, rounds it up, and only afterwards applies the close-factor cap to the repay amount.The prover produced a sequence in which the rounded seizure equals the account’s entire collateral while the capped repayment covers only part of the debt. The account ends with zero collateral and positive debt. Nothing marks the remainder as bad debt, so it stays in
totalBorrowsand keeps accruing interest.Because the supplier exchange rate is computed from
cash + totalBorrows − totalReserves, phantom debt inflates the rate that suppliers redeem at.- Preconditions
- An account becomes undercollateralised close to the liquidation boundary
- A liquidator repays the maximum allowed by the close factor
- Exploit scenario
- 1.Collateral price drops so that an account’s collateral is worth slightly more than its debt.
- 2.A liquidator repays the close-factor maximum and seizes collateral including the incentive.
- 3.Rounding makes the seizure equal to all remaining collateral; residual debt remains.
- 4.Residual debt accrues interest; suppliers who withdraw early are paid from value that does not exist.
- Impact
Unbacked debt inflates the exchange rate. Early withdrawers are overpaid and the shortfall falls on the last suppliers to exit. Each occurrence is small, but they accumulate in volatile markets.
- Recommendation
When a liquidation reduces collateral to zero, write off the remaining debt against reserves (or socialise it explicitly) in the same call and emit an event. Compute the close-factor cap before the incentive.
- Fix
- src/LiquidationEngine.soldiff
uint256 seize = _collateralToSeize(repayCapped); _seize(borrower, liquidator, seize);+ if (collateralOf(borrower) == 0 && debtOf(borrower) > 0) {+ uint256 bad = debtOf(borrower);+ _writeOffAgainstReserves(borrower, bad);+ emit BadDebtRealised(borrower, bad);+ } - Resolution
Residual debt is written off against reserves in the same transaction and BadDebtRealised is emitted. P-05 re-proved.
