Core Contracts

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

NFTLiquidator Stuck NFT Issue Report

Summary

When an NFT is liquidated via liquidateNFT and no bids are placed during the auction period, the NFT remains held in the contract indefinitely.

Vulnerability Details

There is no mechanism to withdraw the nfts , where no bids were placed.

Proof of concept

The given code snippet checks for the fix recommended as well
Add this In test/e2e/nftnotbidcheck.ts

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("NFTLiquidator No Bid Withdrawal PoC", function () {
let owner, stabilityPool, user;
let crvUSD, nft, nftLiquidator;
const tokenId = 1;
const debt = ethers.parseEther("1");
before(async function () {
[owner, stabilityPool, user] = await ethers.getSigners();
const ERC20Mock = await ethers.getContractFactory("ERC20Mock");
crvUSD = await ERC20Mock.deploy("CRVUSD", "CRV", 18, ethers.parseEther("10000"));
await crvUSD.deployed();
const ERC721Mock = await ethers.getContractFactory("ERC721Mock");
nft = await ERC721Mock.deploy("MockNFT", "MNFT");
await nft.deployed();
await nft.connect(owner).mint(stabilityPool.address, tokenId);
const NFTLiquidator = await ethers.getContractFactory("NFTLiquidator");
nftLiquidator = await NFTLiquidator.deploy(crvUSD.address, nft.address, owner.address, 10);
await nftLiquidator.deployed();
// Set stability pool to the designated signer.
await nftLiquidator.connect(owner).setStabilityPool(stabilityPool.address);
// Approve and liquidate NFT with no bid.
await nft.connect(stabilityPool).approve(nftLiquidator.address, tokenId);
await nftLiquidator.connect(stabilityPool).liquidateNFT(tokenId, debt);
});
it("NFT should remain in the contract if no bids are placed", async function () {
const tokenData = await nftLiquidator.tokenData(tokenId);
expect(tokenData.highestBid).to.equal(0);
expect(tokenData.highestBidder).to.equal(ethers.constants.AddressZero);
});
it("should allow stabilityPool to withdraw the NFT after auction expiry", async function () {
// Fast-forward time beyond the auction expiry (3 days + 1 second)
await ethers.provider.send("evm_increaseTime", [3 * 24 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine");
// Call withdraw function
await expect(nftLiquidator.connect(stabilityPool).withdrawUnbidNFT(tokenId))
.to.emit(nftLiquidator, "NFTWithdrawn");
// Verify the NFT owner is now the stability pool (caller)
const newOwner = await nft.ownerOf(tokenId);
expect(newOwner).to.equal(stabilityPool.address);
});
});

Impact

Assets remain locked, reducing the available collateral.

Recommendations

Add this function which allows you to withdraw nft from an auction if no bids were placed.

function withdrawUnbidNFT(uint256 tokenId) external {
TokenData storage data = tokenData[tokenId];
require(block.timestamp >= data.auctionEndTime, "Auction not ended");
require(data.highestBidder == address(0), "Bids have been placed");
require(msg.sender == stabilityPool || msg.sender == owner(), "Not authorized");
delete tokenData[tokenId];
nftContract.transferFrom(address(this), msg.sender, tokenId);
emit NFTWithdrawn(tokenId, msg.sender);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

calc1f4r Submitter
7 months ago
inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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

Give us feedback!