Summary
Completing liquidations via Stability::liquidateBorrower
fail due to a timing mismatch in interest rate calculations compared to LendingPool::finalizeLiquidation
, causing insufficient approvals for debt repayment. When StabilityPool
goes to execute the liquidation the approval amount is stale if interest has accrued, while LendingPool::finalizeLiquidation
tries to transfer an amount of reserve assets higher than approved from StabilityPool
.
Vulnerability Details
The issue occurs in the following sequence:
[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L452-L461)
In StabilityPool::liquidateBorrower
:
uint256 userDebt = lendingPool.getUserDebt(userAddress);
@> uint256 scaledUserDebt = WadRayMul.rayMul(userDebt, getNormalizedDebt());
@> crvUSDToken.approve(address(lendingPool), scaledUserDebt);
[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/LendingPool/LendingPool.sol#L508-L525)
2. In LendingPool::finalizeLiquidation
:
@> ReserveLibrary.updateReserveState(reserve, rateData);
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
@> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
Real example from test trace:
Original Debt: 90000000000000000000 (90e18)
Updated Debt after interest: 90246913425588693651 (90.24e18)
Approved Amount: 90000000000000000000 (90e18)
Result: Transfer fails due to insufficient allowance
Impact
Breaks core liquidation functionality
Liquidations consistently fail when interest has accrued
Risk of protocol becoming undercollateralized
Tools Used
Foundry
Recommendations
Add an approval buffer to account for accrued interest inside StabilityPool::liquidateBorrower
:
function liquidateBorrower(address userAddress) external {
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
+ // Add 5% buffer for accrued interest or whatever possible max interest
+ uint256 buffer = (scaledUserDebt * 5) / 100;
+ uint256 approvalAmount = scaledUserDebt + buffer;
crvUSDToken.approve(address(lendingPool), approvalAmount);
...
}