Core Contracts

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

Miscalculation of interest rates due to Incorrect handling of 'computedDebt' and 'computedLiquidity'

Summary

Miscalculation of dynamic interest rates due to

i) Incorrect logic in getNormalizedDebt and getNormalizedIncome
ii ) not factoring the accrued interests (computedDebt , computedLiquidity) while calculating utilization rate.

Vulnerability Details

Contract : LendingPool

Related functions : updateInterestRatesAndLiquidity , calculateUtilizationRate

UtilizationRate is a critical parameter as it decides how interest rates are dynamically adjusted.

Popular protocols like Aave and Compound calculate utilization by using up-to-date,
accrued values of total debt and total liquidity.
This helps ensure that the dynamic interest rate adjustments
reflect the current state of the market.

Core Issues in updateInterestRatesAndLiquidity

A) Incorrect logic in getNormalizedDebt and getNormalizedIncome

getNormalizedDebt and getNormalizedIncome are expected to return
totalDebt and totalLiquidity in the system accounting for the accrued interest rates.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/pools/ReserveLibrary.sol#L454-L460

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/pools/ReserveLibrary.sol#L467-L473

Issue Identified:

The current getNormalizedDebt and getNormalizedIncome functions only compute updated indices
(compounded or linear factors).

They ignore the stored totalUsage and totalLiquidity, respectively.
This means the effective debt and liquidity are not correctly computed.

function getNormalizedDebt(ReserveData storage reserve,
ReserveRateData storage rateData)
internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return reserve.totalUsage;
}
// --> BUG: This returns only an “interest factor” applied to the usageIndex,
// but never multiplies by the stored scaled debt (totalUsage)
return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta).rayMul(reserve.usageIndex);
}

B) Computed vs. Stored Values
Although the RAAC LendingPool calculates computedDebt and computedLiquidity (which include accrued interest)
it then ignores these values when calculating the utilization rate.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/pools/ReserveLibrary.sol#L211-L215

uint256 computedDebt = getNormalizedDebt(reserve, rateData);
uint256 computedLiquidity = getNormalizedIncome(reserve, rateData);
// Calculate utilization rate
uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);

Instead, it uses the raw stored reserve.totalLiquidity and reserve.totalUsage(which excludes accrued interest)

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/pools/ReserveLibrary.sol#L307

uint256 utilizationRate = totalDebt.rayDiv(totalLiquidity + totalDebt).toUint128();

This can lead to an underestimation of the effective utilization of the reserve.

This mismatch in utilization calculation is critical
because it directly affects how interest rates are adjusted in the subsequent steps
calculateBorrowRate and calculateLiquidityRate

Example Proof of concept

Dynamic Example:
Imagine a reserve in which:

Liquidity Rate = 5 % per year and
Usage Rate = 20 % per year

Stage 1 : Initial reserve status

  • Total Liquidity available = 1,000 tokens

  • Stored Total Usage (Debt) = 500 tokens

Stage 2: After an year (Accrued Interest):
Due to accrued interests,

  • the normalized (effective) liquidity = 1,050 tokens

  • the **normalized (effective) debt = ** 600 tokens

The current implementation calculates **utilization **as below

Miscalculated Utilization = 500 / (1000 + 500) = 33.3 %

However the correct effective utilization should be calculated using the accrued values:

Effective Utilization = 600 / (1050 + 600) = 36.36 %

the utilization rate is underestimated by about 3.3 percentage points,
the dynamic interest rate formulas in the subsequent steps will calculate a borrow rate that is too low
relative to the actual economic stress on the reserve.

The risk appears lower than it really is.

Impact

Improper interest rate adjustments results in
i)_ Encourage excessive borrowing_
ii) Delay repayments
iii) Unfair incentives for Liquidity providers
iv) Undercollateralization and eventual insolvency

Impact : High

Likelihood : High

Recommendations

Fix getNormalizedDebt by taking into account the reserve.totalUsagealong with the compounded factor.
A similar correction should be made for getNormalizedIncomeas well.

function getNormalizedDebt(ReserveData storage reserve,
ReserveRateData storage rateData)
internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
uint256 compoundedFactor = calculateCompoundedInterest(rateData.currentUsageRate, timeDelta);
uint256 newUsageIndex = reserve.usageIndex.rayMul(compoundedFactor);
// Effective debt: scaled debt multiplied by the updated index, normalized back from RAY.
return reserve.totalUsage.rayMul(newUsageIndex) / WadRayMath.RAY;
}

Utilise the computedDebt and computedLiquidity (which includes the accrued interest)
while calculating utilizationrate

// Fixed logic to Calculate utilization rate
uint256 utilizationRate = calculateUtilizationRate(computedLiquidity, computedDebt);
uint256 utilizationRate = computedDebt.rayDiv(computedLiquidity + computedDebt).toUint128();
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

getNormalizedDebt doesn't return total debt but only the index, causing incorrect utilization and interest rate calculations

updateInterestRatesAndLiquidity() uses reserve.totalUsage while other functions use getNormalizedDebt(), causing inconsistent utilization rate calculations

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

getNormalizedDebt doesn't return total debt but only the index, causing incorrect utilization and interest rate calculations

updateInterestRatesAndLiquidity() uses reserve.totalUsage while other functions use getNormalizedDebt(), causing inconsistent utilization rate calculations

Appeal created

inallhonesty Lead Judge 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

getNormalizedDebt doesn't return total debt but only the index, causing incorrect utilization and interest rate calculations

ReserveLibrary calculates computedDebt and computedLiquidity but never uses them, leading to stale totalUsage and totalLiquidity values in utilization rate calculations

Support

FAQs

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