Summary
When positions get liquidated, the collateral NFTs are sent to StabilityPool contract. However, there is no functions to send the NFTs out of the contract
Vulnerability Details
The function LendingPool::finalizeLiquidation()
allows the Stability Pool to finalize the liquidation after the grace period has expired. The position's collateral NFTs are transferred to StabilityPool
.
However, the current implementation does not have any functions to transfer/handle the NFTs from StabilityPool
contract, which causes the NFTs locked in the contract
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, amountScaled);
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
PoC
Add test to file test/unit/core/pools/StabilityPool/StabilityPool.test.js
describe("Deposits", function () {
it('NFT locked in the Stability Pool', async function(){
await userBorrow(user1, ethers.parseEther("100"), ethers.parseEther("100") , 1)
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user2).initiateLiquidation(user1.address);
await userBorrow(user3, ethers.parseEther("100"), ethers.parseEther("20") , 2)
await ethers.provider.send("evm_increaseTime", [3 * 86400 + 1]);
await ethers.provider.send("evm_mine");
await stabilityPool.connect(owner).addManager(user2.address, 1n)
await crvusd.mint(user2.address, ethers.parseEther("10000"))
await crvusd.connect(user2).transfer(stabilityPool.target, ethers.parseEther("10000"))
await lendingPool.connect(user2).updateState();
await stabilityPool.connect(user2).liquidateBorrower(user1.address);
expect(await raacNFT.ownerOf(1)).to.eq(stabilityPool.target)
})
async function userBorrow(user, collateralValue, borrowAmount, nftId){
const tokenId = nftId;
const price = collateralValue;
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId, price);
await crvusd.mint(user.address, price)
await crvusd.connect(user).approve(raacNFT.target, price)
await raacNFT.connect(user).mint(tokenId, price);
await raacNFT.connect(user).approve(lendingPool.target, tokenId);
await lendingPool.connect(user).depositNFT(tokenId);
await lendingPool.connect(user).borrow(borrowAmount);
}
Impact
Tools Used
Manual
Recommendations