Description
During the liquidation scenario of a user within LendingPool::finalizeLiquidation
the users NFT are getting transferred into the StabilityPool
which though lacks functionality to forward or approve those NFTs effectively locking them within the contract.
Vulnerable Code
LendingPool::finalizeLiquidation
:
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
@> raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountBurned);
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
Looking at the highlighted line above we clearly see, that NFTs are being transferred into the StabilityPool
. Looking now at the interface of it:
IStabilityPool
:
pragma solidity ^0.8.19;
interface IStabilityPool {
function deposit(uint256 amount) external;
function withdraw(uint256 deCRVUSDAmount) external;
function liquidateBorrower(address userAddress) external;
function getExchangeRate() external view returns (uint256);
function calculateDeCRVUSDAmount(uint256 rcrvUSDAmount) external view returns (uint256);
function calculateRcrvUSDAmount(uint256 deCRVUSDAmount) external view returns (uint256);
function calculateRaacRewards(address user) external view returns (uint256);
function getPendingRewards(address user) external view returns (uint256);
function getTotalDeposits() external view returns (uint256);
function getUserDeposit(address user) external view returns (uint256);
function balanceOf(address user) external view returns (uint256);
function getManagerAllocation(address manager) external view returns (uint256);
function getTotalAllocation() external view returns (uint256);
function getManager(address manager) external view returns (bool);
function getManagers() external view returns (address[] memory);
function addManager(address manager, uint256 allocation) external;
function removeManager(address manager) external;
function updateAllocation(address manager, uint256 newAllocation) external;
function setRAACMinter(address _raacMinter) external;
function depositRAACFromPool(uint256 amount) external;
function pause() external;
function unpause() external;
}
we can see that there is no functionality implemented to withdraw/transfer these liquidated NFTs.
Impact
Locking those NFTs within the StabilityPool
basically makes liquidating users unprofitable, since the liquidation mechanic directly relies on selling the liquidated NFTs. Therefore the spent crvUSD during liquidation are under these circumstances a direct loss to the protocol, breaking solvency.
By default his justifies a severity of High.
Tools Used
Manual Review
Recommended Fix
Integrate functionality into the StabilityPool
to be able to approve/send those NFTs.