Bid Beasts

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

Auction end-time bypass via equality edge (sniping at block.timestamp == auctionEnd)

Root + Impact

  • Root: placeBid enforces block.timestamp < auctionEnd and only extends when auctionEnd > block.timestamp. At == auctionEnd, bids are rejected outright and never trigger extension.

  • Impact: A malicious bidder can engineer inclusion in the boundary block (timestamp == auctionEnd), preventing all rival bids from being accepted and blocking extension, allowing them to snipe auctions cheaply.


Description

  • The auction is designed to extend by 15 minutes if bids arrive shortly before the deadline, preventing last-second sniping and ensuring fair competition.

  • Due to strict < checks, any bid mined exactly at the deadline second (block.timestamp == auctionEnd) reverts. Because extension logic also checks auctionEnd > block.timestamp, these bids never trigger an extension. An attacker can exploit miner/builder timestamp flexibility to ensure competitor bids fall in this boundary window, winning unfairly.

// In placeBid
require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
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);
}
// @> Strict '<' makes equality bids revert
// @> Extension logic never runs on equality, breaking soft-close behavior

Risk

Likelihood:

  • Likelihood:

    • Reason 1 // Competitive auctions routinely cluster bids at the deadline, making equality-second bids common.

    • Reason 2 // Builders/validators have limited control over block.timestamp, enabling attackers to manipulate equality timing.


  • Impact:

    • Impact 1 // Attacker can reliably snipe auctions by ensuring rivals’ bids land at the boundary second and revert.

    • Impact 2 // Auction anti-sniping design is broken; seller revenue reduced and fairness compromised.


Proof of Concept

Drop this in test/AuctionEndEdge.t.sol, replace the market deployment line with your actual market deployment, then run forge test --match-path test/AuctionEndEdge.t.sol.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
interface IMarket {
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external;
function placeBid(uint256 tokenId) external payable;
function getListing(uint256 tokenId) external view returns (
address seller,
uint256 minPrice,
uint256 buyNowPrice,
uint256 auctionEnd,
bool listed
);
}
contract MockERC721 {
mapping(uint256 => address) internal _owners;
function mint(address to, uint256 id) external { _owners[id] = to; }
function ownerOf(uint256 id) external view returns (address) { return _owners[id]; }
function transferFrom(address from, address to, uint256 id) external { require(_owners[id] == from, "not owner"); _owners[id] = to; }
}
contract AuctionEndEdgePoC is Test {
MockERC721 nft;
IMarket market;
address seller = address(0xBEEF);
address bidder1 = address(0x1111);
address bidder2 = address(0x2222);
function setUp() public {
nft = new MockERC721();
// --- REPLACE this with your actual market deployment ---
// market = IMarket(address(new BidBeastsNFTMarket(address(nft))));
// --------------------------------------------------------
// Placeholder to let compile succeed if you don't replace:
market = IMarket(address(0x100));
}
function testBidAtExactAuctionEndReverts() public {
vm.deal(seller, 1 ether);
vm.deal(bidder1, 1 ether);
vm.deal(bidder2, 1 ether);
uint256 tokenId = 1;
uint256 minPrice = 0.05 ether;
// mint & list
nft.mint(seller, tokenId);
vm.prank(seller);
market.listNFT(tokenId, minPrice, 0);
// first bid to start auction
vm.prank(bidder1);
market.placeBid{value: minPrice + 0.01 ether}(tokenId);
(, , , uint256 auctionEnd, ) = market.getListing(tokenId);
assertGt(auctionEnd, 0);
// warp to exact auctionEnd
vm.warp(auctionEnd);
// bidder2 tries to bid at equality -> should revert with "Auction ended"
vm.prank(bidder2);
vm.expectRevert(bytes("Auction ended"));
market.placeBid{value: minPrice + 0.02 ether}(tokenId);
}
}

Recommended Mitigation

  1. Accept equality-second bids:

- require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
+ require(listing.auctionEnd == 0 || block.timestamp <= listing.auctionEnd, "Auction ended");
Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months 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.

Give us feedback!