Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

NFT is permanently locked if the auction fails to meet the minimum price.

Root + Impact

Description

  • Normal Behavior: If an auction ends without meeting the minPrice, the listed NFT should be returned to the original seller.

  • Specific Issue: The marketplace logic only provides a way to finalize a successful auction (settleAuction). The settleAuction function contains a strict check: require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");. If the highest bid is below minPrice, the function reverts. Since no alternative function exists to handle an unsuccessful auction, the NFT becomes permanently locked in the marketplace contract.

// Root cause in the codebase with @> marks to highlight the relevant section
function settleAuction(uint256 tokenId) external isListed(tokenId) {
// ...
require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
require(bids[tokenId].amount @> >= <@ listing.minPrice, "Highest bid did not meet min price");
// ... no logic for returning NFT if the require fails
_executeSale(tokenId);
}

Risk

Likelihood:

  • This will occur in every auction that receives bids but fails to reach the minimum price.

  • The current implementation makes asset recovery impossible via contract logic.


Impact:

  • Permanent loss of seller's NFT access.

  • Breach of core marketplace business logic, preventing the seller from relisting or reclaiming their asset.Proof of Concept

Proof of Concept

This PoC simulates an auction where a bid is placed below the MIN_PRICE. The subsequent call to settleAuction reverts, leaving the NFT locked with the contract.

function test_fail_settleAuction_belowMinPrice_locksNFT() public {
// Setup: Mint and List NFT (MIN_PRICE is 1 ether)
_mintNFT();
_listNFT();
// 1. Place a bid below minPrice (e.g., 0.5 ether)
vm.prank(BIDDER_1);
market.placeBid{value: 0.5 ether}(TOKEN_ID);
// 2. Advance time past the auction end
uint256 auctionEnd = market.getListing(TOKEN_ID).auctionEnd;
vm.warp(auctionEnd + 1);
// 3. Anyone calls settleAuction, which reverts
vm.prank(address(123));
vm.expectRevert("Highest bid did not meet min price");
market.settleAuction(TOKEN_ID);
// 4. Assert: NFT is still owned by the marketplace
assertEq(nft.ownerOf(TOKEN_ID), address(market), "NFT should be locked in the contract");
}

Recommended Mitigation

A new function, retrieveUnsoldNFT, must be implemented. This function should be restricted to the original seller and allow the NFT to be reclaimed if the auction deadline has passed and the minimum price was not met.

+ /**
+ * @notice Allows the seller to reclaim an NFT if the auction ended without meeting the minPrice.
+ */
+ function retrieveUnsoldNFT(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
+ Listing storage listing = listings[tokenId];
+ // Must be ended, and either no bid or bid was too low
+ require(listing.auctionEnd > 0 && block.timestamp >= listing.auctionEnd, "Auction not ended");
+ // New check to ensure the auction was unsuccessful
+ require(bids[tokenId].amount < listing.minPrice, "Auction was successful, call settleAuction");
+
+ // Clean up and return NFT
+ listing.listed = false;
+ delete bids[tokenId];
+
+ BBERC721.transferFrom(address(this), msg.sender, tokenId);
+ emit NftUnlisted(tokenId);
+ }
Updates

Lead Judging Commences

cryptoghost Lead Judge 29 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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