A miscalculation in the debt repayment mechanism of LendingPool.sol
causes systematic under-repayment of user debts when interest rates are positive. This creates permanent protocol insolvency by leaving residual debt that compounds over time, ultimately making the contract unable to honor withdrawals and liquidations.
The root cause lies in the _repay()
function's debt scaling logic:
_repay
The function incorrectly applies two successive divisions by the usageIndex
when converting the repayment amount. First, userDebt
(which is already scaledDebt * usageIndex
) gets divided by the index to obtain userScaledDebt
. Then actualRepayAmount
(a scaled value) is divided again by the same index when calculating scaledAmount
. This double division creates exponential residual debt that persists even after full repayment attempts.
The protocol's documentation states: "DebtToken balances represent the accumulated interest-bearing debt". However, this implementation violates that invariant by introducing arithmetic drift. When interest rates are positive (normal case per RAACPrimeRateOracle
), each repayment leaves (repayment * interest_rate)
as unaccounted debt. For a 20% interest rate scenario, a user repaying 120 CRV to clear 100 CRV principal + 20 CRV interest would only clear 83.33 CRV of principal, leaving 16.66 CRV residual debt.
This issue error systematically drains protocol reserves by creating unrecoverable debt discrepancies. Over multiple repayment cycles, residual debt compounds exponentially, eventually making the protocol unable to honor withdrawals. Liquidations through StabilityPool
would fail to cover actual debt amounts, leading to undercollateralization of the entire system.
Initial State:
User borrows 100 CRV at usageIndex = 1.0e27
scaledDebt = 100
, actualDebt = 100 * 1.0 = 100 CRV
Interest Accrual:
After 20% interest: usageIndex = 1.2e27
actualDebt = 100 * 1.2 = 120 CRV
Full Repayment Attempt:
User sends 120 CRV to repay()
userScaledDebt = 120 / 1.2 = 100
(correct)
scaledAmount = 100 / 1.2 = 83.33
(error)
Debt reduced by 83.33 scaled units → 16.67 remaining
Result: 20 CRV (16.67 * 1.2) debt remains despite full payment
Compounding Effect:
Subsequent repayments leave 16.67% of previous residual
After 3 repayments: 20 → 3.33 → 0.55 CRV permanent loss
Remove the redundant interest rate division when calculating the scaled repayment amount. The corrected code should directly use the pre-scaled debt value:
This change ensures debt reduction matches the repayment amount precisely. The DebtToken.burn()
call already handles interest rate scaling internally through its currentUsageIndex
parameter, making the second division unnecessary and harmful.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.