Auction Duration Mismatch Creates Extremely Short Auctions
Description
-
The protocol documentation specifies that auctions should have an "Auction deadline of exactly 3 days" to provide sufficient time for competitive bidding.
-
The contract implementation sets the auction duration to only 15 minutes (S_AUCTION_EXTENSION_DURATION) instead of the documented 3 days, creating extremely short auction windows that prevent proper price discovery.
In src/BidBeastsNFTMarketPlace.sol:
contract BidBeastsNFTMarket is Ownable(msg.sender) {
@> uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
if (previousBidAmount == 0) {
requiredAmount = listing.minPrice;
require(msg.value > requiredAmount, "First bid must be > min price");
@> listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
Risk
Likelihood:
-
This occurs on every first bid placed on any NFT listing, as the auction timer is automatically set to the incorrect duration.
-
The 15-minute window is far too short for most users to discover and participate in auctions, especially across different time zones.
-
Users expect 3-day auctions based on the protocol specification and will miss bidding opportunities.
Impact:
-
Sellers receive significantly lower sale prices due to insufficient time for competitive bidding and price discovery.
-
Most potential bidders miss auction opportunities due to the extremely short 15-minute window.
Proof of Concept
First we need to make a quick fix in test/BidBeastsMarketPlaceTest.t.sol:BidBeastsNFTMarketTest::setUp()
function setUp() public {
// Deploy contracts
- vm.prank(OWNER);
+ vm.startPrank(OWNER);
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
rejector = new RejectEther();
vm.stopPrank();
// Fund users
vm.deal(SELLER, STARTING_BALANCE);
vm.deal(BIDDER_1, STARTING_BALANCE);
vm.deal(BIDDER_2, STARTING_BALANCE);
}
Please add the following test to test/BidBeastsMarketPlaceTest.t.sol:BidBeastsNFTMarketTest:
function testSecondBidderCanNotBidAfter1Day() public {
_mintNFT();
_listNFT();
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1}(TOKEN_ID);
BidBeastsNFTMarket.Bid memory highestBid = market.getHighestBid(TOKEN_ID);
assertEq(highestBid.bidder, BIDDER_1);
assertEq(highestBid.amount, MIN_PRICE + 1);
assertEq(market.getListing(TOKEN_ID).auctionEnd, block.timestamp + market.S_AUCTION_EXTENSION_DURATION());
vm.warp(block.timestamp + 1 days);
vm.roll(block.number + 10);
vm.prank(BIDDER_2);
vm.expectRevert("Auction ended");
market.placeBid{value: MIN_PRICE + 1}(TOKEN_ID);
}
Then run forge test --mt testSecondBidderCanNotBidAfter1Day
Output:
Ran 1 test for test/BidBeastsMarketPlaceTest.t.sol:BidBeastsNFTMarketTest
[PASS] testSecondBidderCanNotBidAfter1Day() (gas: 305572)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.54ms (456.07µs CPU time)
Recommended Mitigation
Update the auction duration constant to match the 3-day specification:
+ uint256 constant public S_AUCTION_DURATION = 3 days;
uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
// ... existing code ...
if (previousBidAmount == 0) {
requiredAmount = listing.minPrice;
require(msg.value > requiredAmount, "First bid must be > min price");
- listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
+ listing.auctionEnd = block.timestamp + S_AUCTION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
// Keep extension logic for subsequent bids
// ... existing extension logic using S_AUCTION_EXTENSION_DURATION ...
}
}