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();
@> ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(
reserve.usageIndex
) + amount;
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(
bool isFirstMint,
uint256 amountMinted,
uint256 newTotalSupply
) = IDebtToken(reserve.reserveDebtTokenAddress).mint(
msg.sender,
msg.sender,
amount,
reserve.usageIndex
);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
@> ReserveLibrary.updateInterestRatesAndLiquidity(
reserve,
rateData,
0,
amount
);
_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();
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
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 {
.
.
.
@> 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