Core Contracts

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

Stuck NFTs in StabilityPool Due to Missing ERC721 Handling Mechanism

Summary

The StabilityPool contract cannot receive or manage ERC721 tokens (NFTs) despite being the designated recipient of liquidated NFTs from the LendingPool. This results in permanently stuck NFTs within the StabilityPool, as there is no functionality to transfer, withdraw, or otherwise handle these assets, leading to protocol inefficiency and potential loss of collateral value.

Vulnerability Details

When a user deposits NFT into LendingPool and borrows crvUSD, if a collateral value drops liquidation is initiated via initiateLiquidation,after the liquidation Grace Period in the stabilityPool manager or owner call liquidateBorrower(userAddress), triggering LendingPool::finalizeLiquidation so all NFTs of the user are transferred to StabilityPool, but the StabilityPool has no functions to transfer, sell, or burn the NFTs. Admins cannot recover NFTs, as the contract lacks ownership or withdrawal logic, NFTs are permanently locked in the StabilityPool, Protocol loses access to collateral.

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); // NFTs sent to StabilityPool
}
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));
}
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
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();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
@>> lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

Root Cause

  1. Missing ERC721 Support: The StabilityPool inherits no ERC721 handling functions (e.g., onERC721Received, safeTransferFrom) and lacks a method to withdraw NFTs.

  2. Irreversible Transfers: During liquidation, the LendingPool transfers NFTs to the StabilityPool via raacNFT.transferFrom(address(this), stabilityPool, tokenId). However, the StabilityPool cannot process or forward these NFTs, trapping them indefinitely.

Impact

Collateral NFTs cannot be recovered or reused

Tools Used

Manual Review

Recommendations

  1. Implement ERC721 Receiver:
    Add onERC721Received to comply with ERC721 standards:

    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
    return this.onERC721Received.selector;
    }
  2. Add NFT Withdrawal Function:
    Allow admins to transfer NFTs to a designated treasury:

    function withdrawNFT(address nftContract, uint256 tokenId, address recipient) external onlyOwner {
    IERC721(nftContract).safeTransferFrom(address(this), recipient, tokenId);
    }
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!