Root + Impact
Description
The BidBeastsNFTMarket contract does not enforce a fixed auction deadline of exactly 3 days, despite the claim that it supports an "auction deadline of exactly 3 days." Instead, the contract implements an auction extension mechanism that can indefinitely extend the auction duration with each new bid.
In this implementation, the contract sets the auction end time to only 15 minutes after the first bid and then keeps extending the auction by another 15 minutes if a bid arrives close to the deadline. There is no hard 3-day cutoff, meaning the auction can continue indefinitely as long as new bids keep arriving within the extension window.
if (previousBidAmount == 0) {
listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
Risk
Likelihood:
Occurs on every auction that receives bids.
Triggered whenever new bids arrive near the auction’s end.
Impact:
Auction does not end in 3 days, contrary to expectations.
Auctions can be prolonged indefinitely, delaying settlement and payouts.
Bidders and sellers cannot rely on a predictable closing time, which can harm user trust.
Proof of Concept
function test_AuctionExtendsWithNewBids() external {
_mintNFT();
_listNFT();
vm.prank(BIDDER_1);
market.placeBid{value: 2e18}(TOKEN_ID);
uint256 initialEnd = market.getListing(TOKEN_ID).auctionEnd;
assertEq(initialEnd, block.timestamp + 15 minutes, "Auction should start with 15-minute duration");
vm.warp(block.timestamp + 5 minutes);
vm.prank(BIDDER_2);
uint256 secondBidAmount = ( 2e18 / 100) * (100 + 5);
market.placeBid{value: secondBidAmount}(TOKEN_ID);
uint256 newEnd = market.getListing(TOKEN_ID).auctionEnd;
assertEq(newEnd, initialEnd + 15 minutes, "Auction should extend by 15 minutes");
vm.expectRevert("Auction has not ended");
market.settleAuction(TOKEN_ID);
}
}
Recommended Mitigation
- listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
+ listing.auctionEnd = block.timestamp + 3 days;
- 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) {
+ uint256 maxEnd = listing.auctionStart + 3 days;
+ uint256 newEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
+ listing.auctionEnd = newEnd > maxEnd ? maxEnd : newEnd;
+ emit AuctionExtended(tokenId, listing.auctionEnd);
+ }
Alternatively, enforce a strict 3-day auction deadline without extensions, you can modify the contract to set auctionEnd to block.timestamp + 3 days on the first bid and remove the extension logic.
+ uint256 constant public S_AUCTION_DURATION = 3 days;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
Listing storage listing = listings[tokenId];
address previousBidder = bids[tokenId].bidder;
//other codes
+ require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
}