Bid Beasts

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

NFT Can Be Permanently Locked in Contract Due to Missing Emergency Recovery Mechanism

Root + Impact

Description

The BidBeastsNFTMarket contract lacks any emergency recovery mechanism to retrieve NFTs that become stuck in the contract due to edge cases, bugs, or unforeseen circumstances. While the normal auction flow works as intended, there is no safety net for situations where:

  • A critical bug prevents normal settlement

  • The contract enters an invalid state

  • External dependencies fail (e.g., NFT contract issues)

Risk

Likelihood: Medium

While the normal flow works correctly, several realistic scenarios can trigger this issue:

  • Multiple state transitions increase bug probability

  • Relies on BidBeasts NFT contract behavior

Impact: High

  • NFTs can be irreversibly locked in the contract

  • Neither sellers, buyers, nor contract owner can retrieve stuck NFTs

Proof of Concept

function test_griefing_attack_nft_stuck() public {
vm.prank(OWNER);
nft.mint(SELLER);
// Seller lists valuable NFT
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, 0);
vm.stopPrank();
// Griefer places minimum bid (minPrice + 1 wei)
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1 wei}(TOKEN_ID);
// Seller wants to cancel listing (changed mind, market crashed, etc.)
vm.startPrank(SELLER);
// ❌ Cannot unlist - bid exists
vm.expectRevert("Cannot unlist, a bid has been placed");
market.unlistNFT(TOKEN_ID);
vm.stopPrank();
// Wait for auction to end
vm.warp(block.timestamp + 16 minutes);
// Seller doesn't want to sell for this price
vm.startPrank(SELLER);
// ✅ takeHighestBid works (bid meets minPrice)
// But what if seller refuses?
market.takeHighestBid(TOKEN_ID);
vm.stopPrank();
// In reality, if seller refuses to call takeHighestBid,
// anyone can call settleAuction and force the sale
// But this shows seller has no control after first bid
}

Recommended Mitigation

  • Implement multiple layers of emergency recovery mechanisms:

// Add new event
event EmergencyWithdrawal(uint256 indexed tokenId, address indexed seller, address indexed bidder, uint256 refundAmount);
/**
* @notice Emergency function to recover stuck NFTs
* @dev Only callable by contract owner after significant delay
* @param tokenId The token ID to recover
*/
function emergencyWithdrawNFT(uint256 tokenId) external onlyOwner {
Listing storage listing = listings[tokenId];
require(listing.listed, "NFT not listed");
// Require significant time passage to prevent abuse
// Option 1: Require 30 days after auction end
require(
listing.auctionEnd > 0 &&
block.timestamp > listing.auctionEnd + 30 days,
"Emergency withdrawal: too early"
);
address seller = listing.seller;
address bidder = bids[tokenId].bidder;
uint256 bidAmount = bids[tokenId].amount;
// Clear listing
listing.listed = false;
delete bids[tokenId];
// Return NFT to original seller
BBERC721.transferFrom(address(this), seller, tokenId);
// Refund bidder if bid exists
if (bidder != address(0) && bidAmount > 0) {
_payout(bidder, bidAmount);
}
emit EmergencyWithdrawal(tokenId, seller, bidder, bidAmount);
}
Updates

Lead Judging Commences

cryptoghost Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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