Core Contracts

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

`LendingPool::getNormalizedDebt` function doesn't properly fetch the `usageIndex`

Summary

We can make this conclusion after looking in the RAACMinter::getUtilizationRate function

Vulnerability Details

By taking a look a the function we see the following block of code:

function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}

It aims to represent the ration between totalBorrowed and totalDeposits as percentage (in 0-100 range). the totalBorrowed is fetched by the LendingPool::getNormalizedDebt, which return value can be seen here:

function getNormalizedDebt() external view returns (uint256) {
@> return reserve.usageIndex;
}

The problem here is that the returned usageIndex from getNormalizedDebt is not accurate. This is due to the fact that is not called prior to that. To demonstrate the vulnerability, lets take a look into the LendongPool::borrow function. Actually the only thing we need to see is the exchange between updateReserveState and updateInterestRatesAndLiquidity functions:

function borrow(
uint256 amount
) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender])
revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
// Update reserve state before borrowing
@> ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// Fetch user's total debt after borrowing
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(
reserve.usageIndex
) + amount;
// Ensure the user has enough collateral to cover the new debt
//FIXME: Bad maths here. User is undercollateralised
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
// Update user's scaled debt balance
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// Mint DebtTokens to the user (scaled amount)
(
bool isFirstMint,
uint256 amountMinted,
uint256 newTotalSupply
) = IDebtToken(reserve.reserveDebtTokenAddress).mint(
msg.sender,
msg.sender,
amount,
reserve.usageIndex
);
// Transfer borrowed amount to user
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
// reserve.totalUsage += amount;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
@> ReserveLibrary.updateInterestRatesAndLiquidity(
reserve,
rateData,
0,
amount
);
// Rebalance liquidity after borrowing
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}

When called, the updateReserveState function does the follwoing:

function updateReserveInterests(
ReserveData storage reserve,
ReserveRateData storage rateData
) internal {
uint256 timeDelta = block.timestamp -
uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
uint256 oldLiquidityIndex = reserve.liquidityIndex;
if (oldLiquidityIndex < 1) revert LiquidityIndexIsZero();
// Update liquidity index using linear interest
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
// Update usage index (debt index) using compounded interest
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
// Update the last update timestamp
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveInterestsUpdated(
reserve.liquidityIndex,
reserve.usageIndex
);
}

As seen in the block of code, it makes some state variable updates, one of which is the usageIndex. The updateInterestRatesAndLiquidity function calls updateReserveInterests again, but this time nothing is going to get updated because of the check in the start of updateReserveInterests:

function updateInterestRatesAndLiquidity(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal {
.
.
.
// Update the reserve interests
@> updateReserveInterests(reserve, rateData);
emit InterestRatesUpdated(
rateData.currentLiquidityRate,
rateData.currentUsageRate
);
}

This means that the new and actual usageIndex will be updated the next time updateReserveInterests is called, meaning that the getNormalizedDebt function will always return the outdated usage index.

Impact

This will lead to some wrong emission rate updates in the RAACMinter, since the RAACMinter::getUtilizationRate function is one of the important parts of emission rate calculations

Tools Used

Manual review

Recommendations

Instead of just returning the usageIndex, either call the updateReserveInterests before calling it or just return the ReserveLibrary::getNormalizedDebt() function. It will calculate the usageIndex properly and will return the true usageIndex value

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!