Core Contracts

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

call the `LendingPool::updateState` function in the `StabilityPool::liquidateBorrower` function, to properly update the `usageIndex`

Summary

Because of the way the usageIndex is updated, the crvUSD needed for the liquidation will be different in the StabilityPool::liquidateBorrower function and the LendingPool::updateState function

Vulnerability Details

lets examine the flow of the LendingPool::borrow deposit function:

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);
}

it firstly calls the updateReserveState, which updates the liquidityIndex and usageIndex and at the end of the function it calls the updateInterestRatesAndLiquidity, which updates the variable that compute the liquidityIndex and usageIndex. The thing here is that at its end, the updateInterestRatesAndLiquidity function calls updateReserveState, aiming to update liquidityIndex and usageIndex itself, but this just can't happen because of the following check in updateReserveInterests:

function updateReserveInterests(
ReserveData storage reserve,
ReserveRateData storage rateData
) internal {
uint256 timeDelta = block.timestamp -
uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}

This means that the liquidityIndex and usageIndex will remain inupdated until the next time the updateReserveInterests function is called.

With this out of the way lets go back to the StabilityPool::liquidateBorrower function. The problem here is that it gets the user debt with the outdated usageIndex, which will be updated in the first line of LendingPool::finaliseLiquidation as seen here:

@> function finalizeLiquidation(
address userAddress
) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);

This will result in a difference between the usage index in the StabilityPool contract and in the LendingPool contract, which will lead to liquidation reverts pretty often because the approved amount will be less than the amount the should be transferred to RToken

Impact

Often liquidation reverts, because of insufficient allowance

Tools Used

Manual Review

Recommendations

call the LendingPool::updateState in the StabilityPool::liquidateBorrower function

Updates

Lead Judging Commences

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

StabilityPool: liquidateBorrower should call lendingPool.updateState earlier, to ensure the updated usageIndex is used in calculating the scaledUserDebt

Support

FAQs

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