Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

M02. Inconsistent auction duration

Root + Impact

Description

  • Normal behavior: After a fixed 3-day auction, anyone should be able to call endAuction(tokenId) to finalize the auction.

    • If the highest bid ≥ minimum price: transfer the NFT to the winner and send payment minus 5% fee.

    • If no valid bids were made: return the NFT to the seller.

  • Issue: The current implementation uses a rolling 15-minute extension that starts only after the first bid.

    • settleAuction requires auctionEnd > 0, so third parties cannot finalize auctions where bids occurred but the rolling deadline keeps extending indefinitely.

    • This prevents the auction from ever ending naturally if last-minute bids keep extending the timer.

// Root cause in the codebase with @> marks to highlight the relevant section
uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
...
if (previousBidAmount == 0) {
// Timer only starts on first bid
@> listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
} else {
...
// Rolling extension logic
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
}
}
}
function settleAuction(uint256 tokenId) external isListed(tokenId) {
Listing storage listing = listings[tokenId];
- require(listing.auctionEnd > 0, "Auction has not started (no bids)"); // @> prevents settlement if auction keeps extending
- require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
- require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");
-
- _executeSale(tokenId);
}

Risk

Likelihood:

  • Occurs when last-minute bids repeatedly arrive near the auction deadline, triggering the rolling extension logic.

  • Happens every time an active auction experiences bids close to the timer threshold (15 minutes before auctionEnd).

Impact:

  • Auctions may never naturally end, preventing the marketplace from finalizing NFT transfers and payments.

  • Breaks the specification guarantee that anyone can finalize the auction after 3 days, degrading user trust and platform reliability.


Proof of Concept

function test_poc_rollingExtensionPreventsSettlement() public {
// Mint and list NFT
vm.startPrank(OWNER);
nft.mint(SELLER);
vm.stopPrank();
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
// Place first bid to start auction
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE}(TOKEN_ID);
// Fast-forward to just before auctionEnd
vm.warp(market.getListing(TOKEN_ID).auctionEnd() - 10 seconds);
// Place a new bid to trigger extension
vm.prank(BIDDER_2);
market.placeBid{value: MIN_PRICE * 105 / 100}(TOKEN_ID);
// Attempt to settle auction before new auctionEnd → revert
vm.prank(BIDDER_1);
vm.expectRevert("Auction has not ended");
market.settleAuction(TOKEN_ID);
// AuctionEnd keeps extending with each new last-minute bid
}

Recommended Mitigation

Use a fixed auction deadline of three days once the first bid has been submitted instead of rolling extension

  • The timer starts after the first bid

  • Rolling extensions are removed to avoid indefinite auction duration.

  • If no bids are ever placed, the seller can still call unlistNFT to reclaim the NFT.

+ // Keep auction timer starting after the first bid, but use a fixed duration (no rolling extension)
+ if (previousBidAmount == 0) {
+ listing.auctionEnd = block.timestamp + 3 days; // fixed 3-day timer starting now
+ } else {
+ // Remove rolling extension logic entirely
+ }
+
+ function settleAuction(uint256 tokenId) external isListed(tokenId) {
+ Listing storage listing = listings[tokenId];
+ require(listing.auctionEnd > 0, "Auction has not started (no bids)");
+ require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
+
+ // Execute sale if highest bid meets minPrice
+ if (bids[tokenId].amount >= listing.minPrice) {
+ _executeSale(tokenId);
+ } else {
+ // No sale: NFT remains listed? Optionally return to seller (only seller can unlist)
+ }
+ }
- // Remove rolling-extension code in placeBid()
Updates

Lead Judging Commences

cryptoghost Lead Judge
about 1 month ago
cryptoghost Lead Judge 29 days ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeasts Marketplace: Improper Documentation

Documentation for BidBeasts Marketplace is incomplete or inaccurate, potentially leading to misconfigurations or security misunderstandings.

Support

FAQs

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