Summary
StabilityPool.sol
attempts to bypass the required liquidation sequence by directly calling finalizeLiquidation()
on the LendingPool without proper initiation and grace period handling. This makes all liquidation attempts through the StabilityPool fail and renders the liquidation mechanism non-functional.
Vulnerability Details
The vulnerability exists in the StabilityPool's liquidateBorrower()
function:
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();
lendingPool.finalizeLiquidation(userAddress);
}
The LendingPool requires a specific sequence for liquidations:
function initiateLiquidation(address userAddress) external {
uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
}
function finalizeLiquidation(address userAddress) external {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
}
The required sequence is:
Check health factor
Call initiateLiquidation()
Wait for grace period (3 days)
Call finalizeLiquidation()
The StabilityPool attempts to skip steps 1-3, which causes all liquidation attempts to revert.
Impact
All liquidation attempts through StabilityPool will fail because:
isUnderLiquidation
is never set to true
liquidationStartTime
is never set
Grace period is never respected
Health factor is never checked
This effectively breaks the entire liquidation mechanism of the protocol as:
Unhealthy positions cannot be liquidated
Protocol's risk management is compromised
Bad debt cannot be recovered
The failure is deterministic and affects 100% of liquidation attempts through the StabilityPool.
Tools Used
Recommendations
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
if (!lendingPool.isUnderLiquidation(userAddress)) {
uint256 healthFactor = lendingPool.calculateHealthFactor(userAddress);
require(healthFactor < lendingPool.healthFactorLiquidationThreshold(),
"Health factor too high");
lendingPool.initiateLiquidation(userAddress);
emit LiquidationInitiated(userAddress);
return;
}
uint256 startTime = lendingPool.liquidationStartTime(userAddress);
require(block.timestamp > startTime + lendingPool.liquidationGracePeriod(),
"Grace period not expired");
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();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}