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