Bid Beasts

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

Infinite Auction Extension Due to Last-Minute Bidding

Root + Impact

  • Root Cause: The absence of a maximum duration cap or limit on auction extensions in the placeBid function allows the auction end time (auctionEnd) to be repeatedly extended by S_AUCTION_EXTENSION_DURATION (15 minutes) whenever a bid is placed within the last 15 minutes. The current implementation lacks a mechanism to enforce a total auction duration, enabling continuous extensions. This behavior deviates from the README’s claim that auctions end after 3 days, especially when no buyNowPrice is set to terminate the auction early.

  • Impact: This vulnerability enables a Denial of Service (DoS) attack, where malicious bidders can perpetually extend the auction by placing last-minute bids, locking the seller’s NFT and funds indefinitely. Legitimate winners are prevented from settling the auction, leading to delayed payouts, reduced user trust, and potential financial losses due to decreased bidder participation. Additionally, repeated extensions incur unnecessary gas costs, further impacting usability.

Description:

The BidBeastsNFTMarket::placeBid function allows the auction duration (auctionEnd) to be extended by 15 minutes (S_AUCTION_EXTENSION_DURATION) whenever a new bid is placed within the last 15 minutes of the auction. If bidders continuously place bids in the final minutes, the auction can theoretically extend indefinitely, as there is no maximum duration cap. This behavior is particularly problematic when a seller lists an NFT with only a minPrice and no buyNowPrice, as there is no immediate purchase option to terminate the auction early. This violates the expected behavior outlined in the README, which claims auctions end after 3 days, and introduces a potential Denial of Service (DoS) attack vector.

// In placeBid (extension logic):
uint256 timeLeft = 0;
if (listing.auctionEnd > block.timestamp) {
timeLeft = listing.auctionEnd - block.timestamp;
}
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}

Risk:

  • Likelihood: High. The vulnerability is exploitable whenever an auction nears its end, particularly with automated bots monitoring the mempool for auctionEnd proximity. High-value auctions with competitive bidding increase the likelihood, as attackers have greater incentive to disrupt.

  • Impact: High. Indefinite auction extensions lock NFTs and funds, denying sellers and winners access to their assets. This can lead to significant financial losses, erode marketplace trust, and enable griefing or DoS attacks. Gas costs from repeated extensions also burden participants.

Proof of Concept:

This test demonstrates that repeated last-minute bids can indefinitely extend the auction, effectively creating a DoS vector.

  • An NFT is listed with a minPrice and no buyNowPrice.

  • Multiple bids are placed 5 seconds before the auction ends, each triggering the configured 15-minute extension.

  • The test confirms that the auction end time continues to increase with each bid, with no upper limit.

Results:

  • First bid at timestamp 1000 sets auctionEnd = 1900 (15 minutes).

  • Each subsequent bid (e.g., at 1895, 2795, etc.) extends auctionEnd by 900 seconds (15 minutes).

  • After 100 bids, auctionEnd = 91000 (1000 + 100 × 900), a duration of 25 hours.

  • Console logs confirm auctionEnd increases by 900 seconds per bid, with no upper limit, enabling a DoS attack.
    → Malicious bidders can place repeated last-minute bids to extend the auction indefinitely, preventing settlement and locking the NFT and funds, which disrupts the platform and deters legitimate bidders.

Add the following to the BidBeastsNFTMarketTest.t.sol test file.

Proof of Code
function test_InfiniteAuctionExtensionWith100Bidders() public {
uint256 minPrice = 1 ether;
uint256 initialTimestamp = 1000;
uint256 extensionDuration = 15 minutes;
uint256 bidIncrementPercentage = 5;
uint256 buyNowPrice = 0 ether; // No buy now price for auction
vm.startPrank(OWNER);
nft.mint(SELLER);
vm.stopPrank();
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, minPrice, buyNowPrice);
vm.stopPrank();
// Generate 50 unique bidder addresses
address[] memory bidders = new address[](100);
for (uint256 i = 0; i < 100; i++) {
string memory label = string(abi.encodePacked("bidder", uint2str(i)));
bidders[i] = makeAddr(label);
vm.deal(bidders[i], 150 ether);
// Verify uniqueness
for (uint256 j = 0; j < i; j++) {
assertTrue(bidders[i] != bidders[j], string(abi.encodePacked("Duplicate bidder at index ", uint2str(i))));
}
}
// First bid
vm.prank(bidders[0]);
vm.warp(initialTimestamp);
market.placeBid{value: minPrice}(TOKEN_ID);
uint256 auctionEnd = market.getListing(TOKEN_ID).auctionEnd;
assertEq(auctionEnd, initialTimestamp + extensionDuration, "First bid should set auction end to 15 minutes");
// Simulate 49 additional bids
uint256 currentTime = initialTimestamp;
uint256 previousBid = minPrice;
for (uint256 i = 1; i < 100; i++) {
currentTime = auctionEnd - 5;
vm.prank(bidders[i]);
vm.warp(currentTime);
uint256 bidAmount = previousBid * (100 + bidIncrementPercentage) / 100;
require(bidders[i].balance >= bidAmount, string(abi.encodePacked("Insufficient funds for bidder ", uint2str(i))));
market.placeBid{value: bidAmount}(TOKEN_ID);
auctionEnd = market.getListing(TOKEN_ID).auctionEnd;
assertEq(
auctionEnd,
(initialTimestamp + extensionDuration * (i + 1)),
"Auction should extend by 15 minutes per bid"
);
previousBid = bidAmount;
}
// Verify total duration after 50 bids
uint256 expectedEnd = initialTimestamp + extensionDuration * 100;
assertEq(auctionEnd, expectedEnd, "Auction end should reflect 50 extensions");
// Verify auction can be settled
vm.warp(auctionEnd + 1);
market.settleAuction(TOKEN_ID);
assertEq(market.getListing(TOKEN_ID).listed, false, "Auction should be settled");
assertEq(nft.ownerOf(TOKEN_ID), bidders[99], "NFT should transfer to highest bidder");
}
function uint2str(uint256 _i) internal pure returns (string memory) {
if (_i == 0) return "0";
uint256 j = _i;
uint256 length;
while (j != 0) {
length++;
j /= 10;
}
bytes memory bstr = new bytes(length);
uint256 k = length;
j = _i;
while (j != 0) {
bstr[--k] = bytes1(uint8(48 + j % 10));
j /= 10;
}
return string(bstr);
}

Recommended Mitigation:

Introduce a maximum auction duration cap to prevent indefinite extensions. For example, enforce a total auction duration (e.g., 7 days) from the first bid or listing. Alternatively, limit the number of extensions allowed. Additionally, align the codebase with the README’s 3-day duration expectation or update the documentation to reflect the current behavior.

For example, to implement a 7-day maximum duration:

+ uint256 constant public S_MAX_AUCTION_DURATION = 7 days; // 604800 seconds
// In placeBid (extension logic):
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
+ require(listing.auctionEnd + S_AUCTION_EXTENSION_DURATION <= listing.startTime + S_MAX_AUCTION_DURATION, "Max auction duration exceeded");
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
  • Add a startTime field to the Listing struct to track when the auction begins (set in listNFT or first placeBid).

  • Alternatively, limit the number of extensions:

    uint256 constant public S_MAX_EXTENSIONS = 10;
    mapping(uint256 => uint256) public extensionCount;
    if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
    require(extensionCount[tokenId] < S_MAX_EXTENSIONS, "Max extensions reached");
    extensionCount[tokenId]++;
    listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
    emit AuctionExtended(tokenId, listing.auctionEnd);
    }
  • Update the README to clearly document the auction duration and extension behavior to avoid user confusion.

Updates

Lead Judging Commences

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

BidBeast Marketplace: Auction Duration Miscalculation

BidBeast marketplace contains a flaw in its auction timing mechanism. This causes the contract to miscalculate the actual end time of an auction, resulting in auctions that either conclude prematurely or run longer than specified.

Support

FAQs

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