Bid Beasts

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

Invariant Protocol Break, anyone cannot finalize auction after 3 days

Invariant Protocol Break, anyone cannot finalize auction after 3 days

Description

  • Anyone can finalize the auction after 3 days, according to the contest detail "After 3 days, anyone can call endAuction(tokenId) to finalize the auction." is designed to prevent the auction from taking too long even listing.auctionEnd > 3 days.

  • Unfortunately, in function BidBeastsNFTMarket::settleAuction() there is no checks that if an auction has been ongoing for 3 days. The function only checks for listing.auctionEnd which is its dynamic deadline and it can exceed more than 3 days if there's many bidders come in.

function settleAuction(uint256 tokenId) external isListed(tokenId) {
@> // no checks if auction is already going on for 3 days or not
// according to the contest detail "After 3 days, anyone can call `endAuction(tokenId)` to finalize the auction."
// auction could exceeds more than 3 days and never finalize
Listing storage listing = listings[tokenId];
require(listing.auctionEnd > 0, "Auction has not started (no bids)");
require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");
_executeSale(tokenId);
}

Risk

Likelihood:

  • When auction has been ongoing for 3 days

Impact:

  • Invariant Protocol Break

  • It can confuse whoever call the function BidBeastsNFTMarket::settleAuction() to finalized the auction

Proof of Concept

Simplified for testing:

uint256 constant public S_AUCTION_EXTENSION_DURATION = 4 days;

copy and paste the PoC to BidBeastMarketPlaceTest.t.sol

function test_auctionCannotFinalizeAfter3days() public {
_mintNFT();
_listNFT();
// BIDDER_1 place the frist bid
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1}(TOKEN_ID);
// simplified S_AUCTION_EXTENSION_DURATION = 4 days;
BidBeastsNFTMarket.Listing memory list = market.getListing(TOKEN_ID);
assertEq(4 days + 1, list.auctionEnd);
vm.warp(block.timestamp + 3 days);
// will revert due to "Auction has not ended" even its already more than 3 days
vm.expectRevert();
vm.prank(BIDDER_2);
market.settleAuction(TOKEN_ID);
}

the log:

[332591] BidBeastsNFTMarketTest::test_auctionCannotFinalizeAfter3days()
├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [74067] BidBeasts::mint(SHA-256: [0x0000000000000000000000000000000000000002])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SHA-256: [0x0000000000000000000000000000000000000002], tokenId: 0)
│ ├─ emit BidBeastsMinted(to: SHA-256: [0x0000000000000000000000000000000000000002], tokenId: 0)
│ └─ ← [Return] 0
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(SHA-256: [0x0000000000000000000000000000000000000002])
│ └─ ← [Return]
├─ [25508] BidBeasts::approve(BidBeastsNFTMarket: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 0)
│ ├─ emit Approval(owner: SHA-256: [0x0000000000000000000000000000000000000002], approved: BidBeastsNFTMarket: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 0)
│ └─ ← [Stop]
├─ [128588] BidBeastsNFTMarket::listNFT(0, 1000000000000000000 [1e18], 5000000000000000000 [5e18])
│ ├─ [1094] BidBeasts::ownerOf(0) [staticcall]
│ │ └─ ← [Return] SHA-256: [0x0000000000000000000000000000000000000002]
│ ├─ [29510] BidBeasts::transferFrom(SHA-256: [0x0000000000000000000000000000000000000002], BidBeastsNFTMarket: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 0)
│ │ ├─ emit Transfer(from: SHA-256: [0x0000000000000000000000000000000000000002], to: BidBeastsNFTMarket: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 0)
│ │ └─ ← [Stop]
│ ├─ emit NftListed(tokenId: 0, seller: SHA-256: [0x0000000000000000000000000000000000000002], minPrice: 1000000000000000000 [1e18], buyNowPrice: 5000000000000000000 [5e18])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::prank(RIPEMD-160: [0x0000000000000000000000000000000000000003])
│ └─ ← [Return]
├─ [72627] BidBeastsNFTMarket::placeBid{value: 1000000000000000001}(0)
│ ├─ emit AuctionSettled(tokenId: 0, winner: RIPEMD-160: [0x0000000000000000000000000000000000000003], seller: SHA-256: [0x0000000000000000000000000000000000000002], price: 1000000000000000001 [1e18])
│ ├─ emit AuctionExtended(tokenId: 0, newDeadline: 345601 [3.456e5])
│ ├─ emit BidPlaced(tokenId: 0, bidder: RIPEMD-160: [0x0000000000000000000000000000000000000003], amount: 1000000000000000001 [1e18])
│ └─ ← [Stop]
├─ [2122] BidBeastsNFTMarket::getListing(0) [staticcall]
│ └─ ← [Return] Listing({ seller: 0x0000000000000000000000000000000000000002, minPrice: 1000000000000000000 [1e18], buyNowPrice: 5000000000000000000 [5e18], auctionEnd: 345601 [3.456e5], listed: true })
├─ [0] VM::warp(259201 [2.592e5])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [0] VM::prank(Identity: [0x0000000000000000000000000000000000000004])
│ └─ ← [Return]
├─ [1311] BidBeastsNFTMarket::settleAuction(0)
│ └─ ← [Revert] Auction has not ended
└─ ← [Stop]

Recommended Mitigation

so track auctionStart in BidBeastsNFTMarket::listNFT() function and in BidBeastsNFTMarket::settleAuction() checks if block.timestamp >= auctionStart + 3 days, anyone can finalize, even though auctionEnd is still in the future due to extension.

append auctionStart in struct Listing:

struct Listing {
address seller;
uint256 minPrice;
uint256 buyNowPrice;
+ uint256 auctionStart;
uint256 auctionEnd;
bool listed;
}

create new constant variable:

+ uint256 constant public S_MAX_AUCTION_DURATION = 3 days;

update BidBeastsNFTMarket::listNFT() function:

function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
....
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
+ auctionStart: block.timestamp,
auctionEnd: 0, // Timer starts only after the first valid bid.
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}

update BidBeastsNFTMarket::settleAuction() function:

function settleAuction(uint256 tokenId) external isListed(tokenId) {
Listing storage listing = listings[tokenId];
require(listing.auctionEnd > 0, "Auction has not started (no bids)");
- require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
+ bool reachedDeadline = block.timestamp >= listing.auctionEnd;
+ bool reachedMaxDuration = block.timestamp >= (listing.auctionStart + S_MAX_AUCTION_DURATION);
+ require(reachedDeadline || reachedMaxDuration, "Auction has not ended");
require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");
_executeSale(tokenId);
}
Updates

Lead Judging Commences

cryptoghost Lead Judge
about 2 months ago
cryptoghost Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeasts Marketplace: Improper Documentation

Documentation for BidBeasts Marketplace is incomplete or inaccurate, potentially leading to misconfigurations or security misunderstandings.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.