Link to Affected Code:
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L449-L471
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();
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
}
Description:
The StabilityPool's liquidateBorrower function incorrectly scales the user's debt twice(Index**2), causing valid liquidations to fail at the balance check stage. The issue occurs because:
lendingPool.getUserDebt() returns debt already scaled by the normalized debt index
The function then applies rayMul with getNormalizedDebt() again
This double-scaled amount is used for the balance check if (crvUSDBalance < scaledUserDebt)
Even if the pool has enough funds to cover the actual debt, it will revert due to checking against an inflated amount
Impact:
Valid liquidations to fail at the balance check stage
StabilityPool needs index² times more funds than actually required
System cannot liquidate positions even with sufficient funds
Bad Debts would accumulate
Proof of Concept:
baseDebt = 1000e18
index = 1.4e27 (40% interest accrued)
StabilityPool balance = 1500e18
userDebt = rayMul(baseDebt, index)
= (1000e18 * 1.4e27 + 0.5e27) / 1e27
= 1400e18
scaledUserDebt = rayMul(userDebt, index)
= (1400e18 * 1.4e27 + 0.5e27) / 1e27
= 1960e18
if (1500e18 < 1960e18) revert InsufficientBalance()
Recommended Mitigation:
Remove the second scaling since getUserDebt() already returns the normalized amount:
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
uint256 userDebt = lendingPool.getUserDebt(userAddress);
if (userDebt == 0) revert InvalidAmount();
if (crvUSDBalance < userDebt) revert InsufficientBalance();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, userDebt);
}