Core Contracts

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

StabilityPool liquidateBorrower() function can revert due to incorrect user debt calculation

Summary

The StabilityPool.liquidateBorrower() function allows managers or owners to call LendingPool.finalizeLiquidation(). It's primary purpose is to check whether the stability pool has sufficient crvUSD tokens to cover the liquidation.

Vulnerability Details

The StabilityPool.liquidateBorrower() function retrieves a user's debt using LendingPool.getUserDebt(), which returns userDebt * usageIndex. It then multiplies this value by LendingPool.getNormalizedDebt(), which also returns usageIndex.

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
// @audit getUserDebt() returns userDebt * usageIndex
uint256 userDebt = lendingPool.getUserDebt(userAddress);
// @audit getNormalizedDebt() returns usageIndex
šŸ“Œ uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
​
if (userDebt == 0) revert InvalidAmount();
​
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
​
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
​
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
​
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

StabilityPool.sol#450

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

LendingPool.sol#580
LendingPool.sol#610

Impact

The liquidateBorrower() function can revert for larger user positions with the error InsufficientBalance, even when the crvUSDToken balance is sufficient to cover the user debt.

Proof Of Concept

Example scenario:

  • StabilityPool holds 7e18 crvUSD tokens

  • User1 has 5e18 debt

  • the reserve.usageIndex is 1.25e27

  • getUserDebt(user1) returns 6.25e18
    $$
    \text{getUserDebt()} = (5e18 \times 1.25e27) \div 1e27
    $$

  • This value is then multiplied again by usageIndex, resulting in 7.8125e18
    $$
    \text{scaledUserDebt()} = (6.25e18 \times 1.25e27) \div 1e27
    $$

  • The function reverts with InsufficientBalance, even though the actual balance (7e18) is enough.

  • The calculated scaledUserDebt is 1.5625e18 higher than the actual user debt.

Tools Used

Manual Review

Recommendations

Modify StabilityPool.liquidateBorrower() as follows:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
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();
- if (crvUSDBalance < userDebt) revert InsufficientBalance();
​
// Approve the LendingPool to transfer the debt amount
- bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
​
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
​
- emit BorrowerLiquidated(userAddress, scaledUserDebt);
+ emit BorrowerLiquidated(userAddress, userDebt);
}
Updates

Lead Judging Commences

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

StabilityPool::liquidateBorrower double-scales debt by multiplying already-scaled userDebt with usage index again, causing liquidations to fail

Support

FAQs

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