Core Contracts

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

Incorrect Calculation of Debt and Liquidity in `getNormalizedDebt` and `getNormalizedIncome` Functions Leads to Losses of Borrowers and Liquidity Providers

Summary

The getNormalizedDebt and getNormalizedIncome functions in the ReserveLibrary.sol contract returns the total interest instead of totalDebt and totalLiquidity, with the exception that the getNormalizedDebt function returns totalDebt if timeDelta < 1. This causes the contract to incorrectly calculate the borrow and liquidity rates, leading to incorrect calculation of borrow and liquidity amount as well as the minting of incorrect borrowing and liquidity tokens, as well as the incorrect redemption of these tokens.

Vulnerability Details

Core issue: The getNormalizedDebt and getNormalizedIncome functions in the ReserveLibrary.sol contract returns the total interest instead of totalDebt and totalLiquidity, with the exception that the getNormalizedDebt function returns totalDebt when timeDelta < 1. This affects the calculation of getLiquidityRate, getBorrowRateandupdateInterestRatesAndLiquidity` functions.

contracts/libraries/pools/ReserveLibrary.sol:getNormalizedDebt#L470-L473

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; // @audit: returns total debt
}
return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta)
.rayMul(reserve.usageIndex); // @audit: However, returns the total borrow interest rate here
}

contracts/libraries/pools/ReserveLibrary.sol:getNormalizedIncome#L457-L459

function getNormalizedIncome(ReserveData storage reserve, ReserveRateData storage rateData) internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp); if (timeDelta < 1) {
return reserve.liquidityIndex; // @audit: returns total liquidity interest
}
return
calculateLinearInterest(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
).rayMul(reserve.liquidityIndex); // @audit: returns total liquidity interest too
}

This impacts the getBorrowRate calculation. If timeDelta < 1, the calculateUtilizationRate function will use the totalDebt and total liquidity interest to calculate the incorrect utilizationRate. If timeDelta >= 1, it will use the total borrow interest and total liquidity interest to calculate an incorrect utilizationRate, which then leads to an incorrect LiquidityRate. The same issue exists in the getLiquidityRate and updateInterestRatesAndLiquidity functions.

contracts/libraries/pools/ReserveLibrary.sol:getBorrowRate#L432

function getBorrowRate(ReserveData storage reserve,ReserveRateData storage rateData) internal view returns (uint256) {
uint256 totalDebt = getNormalizedDebt(reserve, rateData); // When timeDelta < 1, it returns the interest factor
uint256 utilizationRate = calculateUtilizationRate(
reserve.totalLiquidity,
totalDebt // Incorrect! Using the borrow interest factor instead of actual debt amount
);
}
return calculateBorrowRate(rateData.primeRate, rateData.baseRate, rateData.optimalRate, rateData.maxRate, rateData.optimalUtilizationRate, utilizationRate);

contracts/libraries/pools/ReserveLibrary.sol:getLiquidityRate#L444

function getLiquidityRate(ReserveData storage reserve,ReserveRateData storage rateData) internal view returns (uint256) {
uint256 totalDebt = getNormalizedDebt(reserve, rateData); // Returns total interest (e.g., 1.1)
// @audit: The utilization rate calculation will be completely incorrect
uint256 utilizationRate = calculateUtilizationRate(
reserve.totalLiquidity,
totalDebt // @audit: for example this is liquidity interest factor , but should be debt amount(e.g., 1000 USDC)
);
}
return calculateLiquidityRate(utilizationRate, rateData.currentUsageRate, rateData.protocolFeeRate, totalDebt);

contracts/libraries/pools/ReserveLibrary.sol:updateInterestRatesAndLiquidity#L211-L212

function updateInterestRatesAndLiquidity(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal {
// @audit: both return interest not amount, with the exception that the getNormalizedDebt function returns totalDebt when `timeDelta < 1`.
uint256 computedDebt = getNormalizedDebt(reserve, rateData);
uint256 computedLiquidity = getNormalizedIncome(reserve, rateData);

Impact

  1. Incorrect Utilization Rate: The wrong values for totalDebt and totalLiquidity lead to inaccurate utilization rates, affecting the system's ability to properly assess liquidity and debt positions.

  2. Incorrect Borrow and Liquidity Rates: Errors in the utilization rate result in wrong borrow and liquidity rates, leading to incorrect interest charges for borrowers and returns for liquidity providers.

  3. Erroneous Token Minting and Redemption: Miscalculations lead to incorrect amounts of deposit and borrow tokens being minted or redeemed, disrupting the liquidity pools and user transactions.

  4. Exploitation Risks: Attackers could exploit these inaccuracies to arbitrage, manipulate rates, or trigger abnormal behavior, causing financial losses.

Tools Used

Manual Review

Recommendations

Method1: Based on the implementation of the getNormalizedDebt and getNormalizedIncome functions in the LendingPool contract, it is recommended that the getNormalizedDebt function in the ReserveLibrary.sol contract should also return the total interest rates instead of totalDebt and totalLiquidity. Additionally, the corresponding functions getBorrowRate, getLiquidityRate, and updateInterestRatesAndLiquidity should be updated accordingly. For example:

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
+ return reserve.usageIndex; // Fix: Return the current interest rate factor
}
return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta)
.rayMul(reserve.usageIndex);
}

Method2: Alternatively, both getNormalizedIncome and getNormalizedDebt functions could be modified to return totalLiquidity and totalDebt.

function getNormalizedIncome(
ReserveData storage reserve,
ReserveRateData storage rateData
) internal view returns (uint256) {
uint256 timeDelta = block.timestamp -
uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
- return reserve.liquidityIndex;
+ return reserve.totalLiquidity;
}
- return
- calculateLinearInterest(
- rateData.currentLiquidityRate,
- timeDelta,
- reserve.liquidityIndex
- ).rayMul(reserve.liquidityIndex);
+ uint256 cumulatedInterest = calculateLinearInterest(
+ rateData.currentLiquidityRate,
timeDelta,
+ reserve.liquidityIndex
+ ).rayMul(reserve.liquidityIndex);
+ return reserve.totalLiquidity * cumulatedInterest;
}
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;
}
- return
- calculateCompoundedInterest(rateData.currentUsageRate, timeDelta)
- .rayMul(reserve.usageIndex);
+ uint256 interestFactor =
+ calculateCompoundedInterest(rateData.currentUsageRate, timeDelta)
+ .rayMul(reserve.usageIndex);
+ return reserve.totalUsage * interestFactor;
}
Updates

Lead Judging Commences

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

LendingPool::getNormalizedIncome() and getNormalizedDebt() returns stale data without updating state first, causing RToken calculations to use outdated values

Support

FAQs

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