The DebtToken::mint function, which includes both the borrowed amount and any accrued interest (balanceIncrease). However, the LendingPool only stores the scaled borrowed amount (amount.rayDiv(reserve.usageIndex)) in the user's scaledDebtBalance, ignoring the balanceIncrease. This mismatch can lead to incorrect debt calculations, undercollateralized positions, and potential protocol insolvency.
The issue arises in the borrow function of the LendingPool contract. When a user borrows funds, the DebtToken::mint function is called in the DebtToken contract, which calculates the total amount to mint as amount + balanceIncrease. However, the LendingPool only stores the scaled borrowed amount (amount.rayDiv(reserve.usageIndex)) in the user's scaledDebtBalance. This discrepancy means that the scaledDebtBalance does not account for the balanceIncrease (accrued interest), leading to an underestimation of the user's debt.
DebtToken::mint
DebtToken.mint:
Mints amount + balanceIncrease as debt tokens.
balanceIncrease represents accrued interest due to index updates.
LendingPool.borrow:
Stores only amount.rayDiv(reserve.usageIndex) in user.scaledDebtBalance.
Ignores the balanceIncrease component.
This inconsistency can cause the protocol to miscalculate user debt, leading to:
Undercollateralized positions.
Incorrect health factor calculations.
Initial Conditions:
reserve.usageIndex = 1e27 (RAY).
User has no existing debt.
User borrows 100 units of the reserve asset.
DebtToken.mint:
amount = 100.
balanceIncrease = 0 (since it's the first borrow).
amountToMint = 100 + 0 = 100.
Debt tokens minted: 100.
LendingPool.borrow:
scaledAmount = 100.rayDiv(1e27) = 100e18.
user.scaledDebtBalance updated to 100e18.
Interest Accrual:
Over time, reserve.usageIndex increases to 2e27. @>>> just for better explanation
User borrows another 100 units.
DebtToken.mint:
amount = 100.
balanceIncrease = 100e18 * (2e27 - 1e27) / 1e27 = 100e18.
amountToMint = 100 + 100 = 200.
Debt tokens minted: 200.
LendingPool.borrow:
scaledAmount = 100.rayDiv(2e27) = 50e18.
user.scaledDebtBalance updated to 100e18 + 50e18 = 150e18.
Result:
Actual debt: 150e18 * 2e27 / 1e27 = 300.
Debt tokens minted: 200 (from first borrow) + 200 (from second borrow) = 400.
Mismatch between actual debt (300) and debt tokens minted (400).
The total debt tracked by the protocol (reserve.totalUsage) becomes inaccurate. Unrecorded interest creates a deficit between actual debt (accrued in DebtToken) and recorded debt (in LendingPool). This mismatch can lead to systemic insolvency if liquidations fail to cover the gap.
Health factor calculations rely on user.scaledDebtBalance, which excludes accrued interest. This results in false-positive safe positions, delaying liquidations until losses are irrecoverable.
Example:
User borrows 100 CRV when usageIndex = 1e27 (no interest).
DebtToken mints 100 debt tokens.
Later, interest accrues (usageIndex = 1.1e27).
User borrows again:
balanceIncrease = 10 CRV (accrued interest).
DebtToken mints 110 CRV debt.
But LendingPool only records 100 CRV in scaledDebtBalance.
Collateral checks use 100 CRV instead of 110 CRV, enabling over-leverage.
Result:
Positions are undercollateralized by 10%.
Protocol’s actual debt exceeds recorded debt, risking insolvency.
Liquidations trigger too late, amplifying losses.
Manual Review
Update LendingPool.borrow:
Store the total scaled debt (amountToMint.rayDiv(reserve.usageIndex)) in user.scaledDebtBalance.
Ensure the scaledDebtBalance accounts for both the borrowed amount and the balanceIncrease.
Refactor Debt Calculation:
Use the amountToMint value returned by DebtToken.mint to update the user's debt balance.
Add Validation:
Implement checks to ensure the scaledDebtBalance matches the actual debt calculated from the DebtToken contract.
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.