[High] Missing 3-Day Auction Duration Requirement
Description
The contract documentation explicitly states that auctions must last exactly 3 days before settlement. However, the placeBid function sets the auction duration to only 15 minutes (S_AUCTION_EXTENSION_DURATION) when the first bid is placed, and extends by 15 minutes for subsequent bids. This creates a critical deviation from the specified business logic.
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);
} else {
}
@> uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
Risk
Likelihood:
-
Affects every single auction in the marketplace
-
Deviation is guaranteed for all listings
-
The issue is baked into the core constant definition
Impact:
-
Business Logic Violation: Directly contradicts documented auction mechanics
-
Unfair Price Discovery: 15-minute auctions prevent proper market evaluation
-
Reduced Competition: Insufficient time for bidders to discover and participate in auctions
-
Seller Disadvantage: Premature settlement may result in significantly lower sale prices
-
Trust Erosion: Users expecting 3-day auctions will lose confidence in the platform
-
Manipulation Risk: Malicious actors can exploit short windows to win auctions before legitimate bidders participate
Documentation Reference
The contract documentation explicitly requires:
Auction Completion:
The contract also supports:
Proof of Concept
Current behaviour demonstrates the vulnerability:
function test_AuctionDurationTooShort() public {
_mintNFT();
_listNFT();
vm.prank(BIDDER_1);
market.placeBid{value: 1.1 ether}(TOKEN_ID);
uint256 auctionEnd = market.getListing(TOKEN_ID).auctionEnd;
uint256 actualDuration = auctionEnd - block.timestamp;
assertEq(actualDuration, 900, "Auction duration is 15 minutes");
assertNotEq(
actualDuration,
3 days,
"Auction should last 3 days per docs"
);
vm.warp(block.timestamp + 901 seconds);
market.settleAuction(TOKEN_ID);
}
Attack Scenario:
Seller lists valuable NFT at 0.01 ETH minimum
Malicious bidder monitors mempool for new listings
Immediately bids 0.011 ETH to start 15-minute timer
After 15 minutes, attacker settles and wins NFT far below the market value
Seller receives drastically undervalued payment
Recommended Mitigation
Update the initial auction duration to match Business logic:
If the auction is set to end after 3 Days with no extension whatsoever:
- event AuctionExtended(uint256 tokenId, uint256 newDeadline);
- uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
+ uint256 constant public S_INITIAL_AUCTION_DURATION = 3 days;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
// ... existing checks ...
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_INITIAL_AUCTION_DURATION;
- emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
require(msg.value >= requiredAmount, "Bid not high enough");
- 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);
}
}
// ... rest of function ...
}
If the Initial Auction is set to end after 3 Days, but will be extended by 15 minutes by up to 3 Days upon bids:
- uint256 constant public S_AUCTION_EXTENSION_DURATION = 15 minutes;
+ uint256 constant public S_INITIAL_AUCTION_DURATION = 3 days;
+ uint256 constant public S_BID_EXTENSION_DURATION = 15 minutes;
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
// ... existing checks ...
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_INITIAL_AUCTION_DURATION;
- emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
require(msg.value >= requiredAmount, "Bid not high enough");
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;
+ if (timeLeft < S_INITIAL_AUCTION_DURATION) {
+ listing.auctionEnd = listing.auctionEnd + S_BID_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
// ... rest of function ...
}