Bid Beasts

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

The takeHighestBid function is a risk that the seller will terminate the auction early

Root + Impact

Description

  • The takeHighestBid function has a design fairness issue.

// @notice The takeHighestBid function has a design fairness issue
function takeHighestBid(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
Bid storage bid = bids[tokenId];
require(bid.amount >= listings[tokenId].minPrice, "Highest bid is below min price");
_executeSale(tokenId);
}

Risk

Likelihood:

  • Sellers can call this function before the auction ends, directly ending the auction and completing the transaction

Impact:

  • This undermines auction fairness: new bidders might be ready to bid during the extended period, only to have their bids "closed early" by the seller.

Proof of Concept

* @notice Tests a more extreme fairness issue: the seller ends the auction just as the extension begins.
*/
function test_auctionFairnessIssue_SellerEndsAuctionImmediatelyAfterExtension() public {
_mintNFT();
_listNFT();
// The first bidder places a bid, triggering an extension of the auction time
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE}(TOKEN_ID);
uint256 auctionEndTime = market.getListing(TOKEN_ID).auctionEnd;
console.log("Auction extended to:", auctionEndTime);
console.log("Extension duration:", market.S_AUCTION_EXTENSION_DURATION());
// Immediately let the seller end the auction (at the beginning of the extended time)
vm.prank(SELLER);
market.takeHighestBid(TOKEN_ID);
// This completely deprives other bidders of the opportunity to bid within the extended time
// The extended time mechanism becomes meaningless
assertEq(nft.ownerOf(TOKEN_ID), BIDDER_1, "NFT transferred to first bidder");
assertFalse(market.getListing(TOKEN_ID).listed, "Auction ended immediately");
console.log("CRITICAL ISSUE: Seller can end auction immediately after extension starts");
console.log("This makes the extension mechanism completely unfair to other bidders");
}
/**
* @notice Tests the situation where a bidder is about to bid but the seller ends the auction first.
*/
function test_auctionFairnessIssue_BidderPreparingBidButSellerEndsFirst() public {
_mintNFT();
_listNFT();
// First bidder places a bid
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE}(TOKEN_ID);
uint256 auctionEndTime = market.getListing(TOKEN_ID).auctionEnd;
// Most of the simulation time has passed, and BIDDER_2 is ready to bid
uint256 almostEndTime = auctionEndTime - 1;
vm.warp(almostEndTime);
console.log("BIDDER_2 preparing to bid with 1 second left");
console.log("Current time:", block.timestamp);
console.log("Auction ends at:", auctionEndTime);
// But the seller ended the auction firs
vm.prank(SELLER);
market.takeHighestBid(TOKEN_ID);
// BIDDER_2 lost the opportunity to bid
assertEq(nft.ownerOf(TOKEN_ID), BIDDER_1, "First bidder got NFT despite BIDDER_2 wanting to bid");
console.log("UNFAIR: Seller ended auction before BIDDER_2 could place their bid");
console.log("BIDDER_2 had valid time remaining but was denied the opportunity");
}

Recommended Mitigation

//Sellers can only close the deal after the auction ends
function takeHighestBid(uint256 tokenId)
external
isListed(tokenId)
isSeller(tokenId, msg.sender)
{
Listing storage listing = listings[tokenId];
Bid storage bid = bids[tokenId];
require(bid.amount >= listing.minPrice, "Highest bid is below min price");
require(listing.auctionEnd > 0, "Auction not started");
require(block.timestamp >= listing.auctionEnd, "Auction not ended yet");
_executeSale(tokenId);
}
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.