Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

Critical Debt Accounting Error in LendingPool Repayment Logic

Summary

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.

Finding Description

The root cause lies in the _repay() function's debt scaling logic:
_repay

function _repay(uint256 amount, address onBehalfOf) internal {
// ...
uint256 userDebt = IDebtToken(...).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex); // 🚨 Critical error
// ...
user.scaledDebtBalance -= amountBurned;
}

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.

Impact

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.

Proof Of Concept

  1. Initial State:

    • User borrows 100 CRV at usageIndex = 1.0e27

    • scaledDebt = 100, actualDebt = 100 * 1.0 = 100 CRV

  2. Interest Accrual:

    • After 20% interest: usageIndex = 1.2e27

    • actualDebt = 100 * 1.2 = 120 CRV

  3. 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

  4. Compounding Effect:

    • Subsequent repayments leave 16.67% of previous residual

    • After 3 repayments: 20 → 3.33 → 0.55 CRV permanent loss

Mitigation

Remove the redundant interest rate division when calculating the scaled repayment amount. The corrected code should directly use the pre-scaled debt value:

- uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
+ uint256 scaledAmount = actualRepayAmount;

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.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

LendingPool::_repay double scales the debt

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

LendingPool::_repay double scales the debt

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.