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();
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);
ā
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);
}