Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

NFT locked in the StabilityPool contract

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();
// update state
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;
// Transfer NFTs to Stability Pool
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;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update user's scaled debt balance
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
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(){
// @audit POC NFT locked in the Stability Pool
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)
// grace period passed
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)
})
// Helper function
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

  • NFTs get locked in Stability Pool

Tools Used

Manual

Recommendations

  • Add functions to transfer/handle NFTs from liquidated positions

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Liquidated RAACNFTs are sent to the StabilityPool by LendingPool::finalizeLiquidation where they get stuck

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.