Core Contracts

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

Double-Scaling of Debt in liquidateBorrower Leading to Overpayment

Summary

The liquidateBorrower function in StabilityPool incorrectly scales the user's debt twice, resulting in an overestimated debt amount. This causes the Stability Pool to transfer excess funds during liquidation, risking insolvency and failed transactions.

Vulnerability Details

lendingPool.getUserDebt() returns the user's debt scaled by reserve.usageIndex (already includes interest). scaledUserDebt is calculated by multiplying userDebt again with lendingPool.getNormalizedDebt() (which is the same reserve.usageIndex).

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
// @audit scaled 2x
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);
}

If userDebt = 100 and usageIndex = 1.1e27 (10% interest), the actual debt is 110. However, scaledUserDebt becomes 110 * 1.1 = 121, leading to a 10% overpayment.

Impact

  • Stability Pool Drains: Excess crvUSD is transferred to the LendingPool, depleting the Stability Pool’s reserves.

  • Failed Liquidations: Transactions revert if scaledUserDebt exceeds the pool’s crvUSD balance, even when the true debt is payable.

Tools Used

manual code review

Recommendations

Use userDebt directly without re-scaling.

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
// @audit scaled 2x
- 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);
}
Updates

Lead Judging Commences

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

Give us feedback!