Core Contracts

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

Incorrect debt calculation in liquidateBorrower

Summary

Incorrect debt calculation in liquidateBorrower

Vulnerability Details

When the borrower's position is unhealthy, the owner or managers can liquidate this borrower via liquidateBorrower() function. In liquidateBorrower(), we will get the borrower's debt from lendingPool.getUserDebt(userAddress). When we check the implementation of getUserDebt() function, the getUserDebt() return value is the borrower's actual debt amount.

The problem here is that we multiple another lendingPool.getNormalizedDebt(), this will cause that scaledUserDebt will be larger than the borrower's actual debt amount. And based on if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();, the owner has to transfer more crvUSD tokens than expected into the stabilityPool before the owner wants to liquidate this borrower.

When we check finalizeLiquidation() function, the lending pool will calculate the correct borrower's debt and transfer the correct crvUSD token from the stabilitiyPool.

The problem is that the owner has to transfer more crvUSD into the stability pool before we liquidate one position. And the most important thing here is that we do not have one interface in stabilityPool to transfer crvUSD token out. This will cause some crvUSD tokens locked in stabilityPool.

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
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();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
}
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
// scaledDebtBalance is one normalized balance.
return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
...
}

Impact

Some crvUSD tokens will be locked in stabilityPool.

Tools Used

Manual

Recommendations

Calculate the correct borrower' debt in liquidateBorrower(), then the owner will not need to transfer more crvUSD token to liquidate borrowers' positions.

Updates

Lead Judging Commences

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