Core Contracts

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

Potential liquidation failure due to outdated userDebt calculation

Summary

The liquidation process in the protocol has a potential vulnerability due to the use of stale debt information when approving token transfers. This can lead to failures in the liquidation process if the approved amount is insufficient to cover the user's actual debt at the time of liquidation.

Vulnerability Details

In the liquidateBorrower() function in StabilityPool works as follows:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// @audit-issue Missing prior update of the lending pool state.
// @audit-issue User debt is retrieved based on stale usageIndex
>> uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// @audit-issue Gives Approval based on the above stale debt info
>> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// @audit-issue Performs late update to lending pool state
>> lendingPool.updateState();
// @audit Finally calls finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

1.Stale Debt Calculation
:

The getUserDebt() function returns the user's debt based on the current usageIndex, which may be stale.

function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
>> return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}

The reserve.usageIndex reflects the state of the reserve at the last update, and if there have been significant changes in the reserve's state (like deposits or withdrawals) since that last update, the returned debt may not accurately represent the user's current obligation.

2.Approval Based on Stale Debt:

The retrieved userDebt is then used to set the approval for the lendingPool to transfer the necessary amount of tokens from the StabilityPool. This approval is based on potentially outdated information.

3.State Update in Finalization:

After the approval is set, the finalizeLiquidation() function is called on the lendingPool.

// @audit It first update state
>> ReserveLibrary.updateReserveState(reserve, rateData);
---SNIP---
UserData storage user = userData[userAddress];
// @audit userDebt is then retrieved here again but based on updated index
>> uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
---SNIP---
// @audit the above userDebt is used in burn
>> (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// @audit Attempts to transfer reserve assets from Stability Pool to cover the debt
// @audit-issue If the calculated amountScaled exceeds what was approved, this will fail
>> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);

This function first updates the reserve state by calling ReserveLibrary.updateReserveState(), which recalculates the usageIndex and subsequently the user's debt based on the updated state.

Now, if the actual required tokens to be pulled from the StabilityPool (amountScaled) exceed the amount that was approved based on the stale userDebt, the transaction will fail when the lendingPool attempts to transfer the tokens.

Impact

This can lead to a failure in the liquidation process, leaving users in a precarious position where their debts are not settled, and they remain under liquidation. This undermines the reliability of the lending protocol and could result in financial losses for users.

Tools Used

Manual Review

Recommendations

Ensure that the debt is recalculated and the approval is set based on the most current state of the reserve when initiating the liquidation in the stabilityPool:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// @audit First update the reserve state
+ lendingPool.updateState();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
---SNIP---
// @audit Call this at the top. It serves no purpose here as the same functionality will
// be called at the begining of lendingPool.finalizeLiquidation()
- lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
Updates

Lead Judging Commences

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

Give us feedback!