Root + Impact
The listNFT function doesn't verify whether an NFT is already listed before creating a new listing, which can lead to state inconsistencies and lost bids.
Already listed NFTs can be re-listed, potentially erasing existing auction data, disrupting active auctions, and causing users to lose track of their bids.
Description
-
The normal behavior in auction marketplaces is to prevent NFTs that are already in an active auction from being re-listed until they are properly unlisted or the auction concludes.
-
The specific issue is that the listNFT function overwrites any existing listing data without checking if the NFT is already part of an active listing, potentially disrupting ongoing auctions and existing bids.
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
if (_buyNowPrice > 0) {
require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
}
BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({ @>
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
auctionEnd: 0,
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}
Risk
Likelihood: Low
-
This would occur if a token is transferred back to its original owner (or to a new owner) after being listed, and then listed again.
-
The scenario requires multiple transactions, but is entirely possible within the normal operation of the contract.
Impact: Medium to High
-
Existing bids on the NFT would be lost as the listings mapping is overwritten.
-
Bidders would lose track of their active bids and have no way to claim their NFT even if they were previously the highest bidder.
-
The auction timing would be reset, potentially allowing the seller to manipulate the auction to their advantage.
-
This could lead to auction participants losing confidence in the marketplace.
Proof of Concept
async function demonstrateRelistingIssue() {
const nft = await BidBeasts.deploy();
const marketplace = await BidBeastsNFTMarket.deploy(nft.address);
const owner = accounts[0];
const bidder = accounts[1];
const tokenId = await nft.mint(owner.address);
await nft.approve(marketplace.address, tokenId);
await marketplace.listNFT(tokenId, ethers.utils.parseEther("1.0"), ethers.utils.parseEther("2.0"));
console.log("NFT Listed");
const listing1 = await marketplace.getListing(tokenId);
console.log("Original Listing:", {
seller: listing1.seller,
minPrice: ethers.utils.formatEther(listing1.minPrice),
listed: listing1.listed
});
await marketplace.connect(bidder).placeBid(tokenId, {
value: ethers.utils.parseEther("1.1")
});
console.log("Bid placed by bidder");
const bid = await marketplace.getHighestBid(tokenId);
console.log("Highest Bid:", {
bidder: bid.bidder,
amount: ethers.utils.formatEther(bid.amount)
});
await mockResetOwnership(nft, marketplace, tokenId, owner.address);
await nft.approve(marketplace.address, tokenId);
await marketplace.listNFT(tokenId, ethers.utils.parseEther("1.5"), ethers.utils.parseEther("3.0"));
console.log("NFT Re-listed with different prices");
const listing2 = await marketplace.getListing(tokenId);
console.log("New Listing:", {
seller: listing2.seller,
minPrice: ethers.utils.formatEther(listing2.minPrice),
listed: listing2.listed
});
const newBid = await marketplace.getHighestBid(tokenId);
console.log("Highest Bid after re-listing:", {
bidder: newBid.bidder,
amount: newBid.bidder === ethers.constants.AddressZero ? "0" : ethers.utils.formatEther(newBid.amount)
});
async function mockResetOwnership(nft, marketplace, tokenId, ownerAddress) {
console.log("Simulating NFT returning to owner through some mechanism");
}
}
This proof of concept demonstrates that:
An NFT can be listed with initial auction parameters
A bidder can place a bid on this NFT
If the original owner somehow regains possession of the NFT, they can re-list it
Re-listing overwrites the original listing parameters but doesn't clear the existing bid
This creates an inconsistent state where bids are associated with a listing that has changed parameters
Recommended Mitigation
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
+ require(!listings[tokenId].listed, "NFT is already listed");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
if (_buyNowPrice > 0) {
require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
}
BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
auctionEnd: 0, // Timer starts only after the first valid bid.
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}
This change:
Prevents Re-listing: Adds a check to ensure NFTs that are already listed cannot be re-listed.
Maintains Auction Integrity: Ensures that ongoing auctions cannot be disrupted by re-listing the same NFT.
Protects Bidders: Prevents scenarios where bidders might lose track of their bids due to re-listing.
Consistent State: Maintains a consistent state where an NFT can only be in one active listing at a time.
This simple addition significantly improves the security and reliability of the marketplace, protecting both sellers and bidders from potential issues caused by re-listing.