Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

[H-3] The `BidBeastsNFTMarket` contract is missing the `endAuction(tokenId)` function described in the documentation, creating a direct MEV attack opportunity for backrunning.

The BidBeastsNFTMarket contract is missing the endAuction(tokenId) function described in the documentation, direct MEV attack opportunity for backrunning.

Description

  • The current implementation is missing the `BidBeastsNFTMarket::endAuction(tokenId)` function described in the documentation under the `Auction Completion` section. This omission creates a backrunning opportunity for bid manipulation, as the contract does not enforce the end of the auction after three days, allowing the auction period to be extended until a bid meets the buyNowPrice.

  • This vulnerability creates a Miner Extractable Value (MEV) attack opportunity for backrunning due to the possible extension of the auction beyond three days. Attackers can manipulate transaction ordering to place bids until one meets the `buyNowPrice` or bidding ceases, undermining the auction’s integrity and potentially causing financial losses for legitimate bidders and sellers.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood: High

  • It's highly probable to happen.

  • Direct path for attack execution.

Impact: High

  • There’s severe disruption to protocol functionality.

  • Negative impact on protocol trustworthiness.

Proof of Concept

Add the following code snippet to the `BidBeastsMarketPlaceTest.t.sol` test file.

function testBackrunningToManipulateBid() public {
uint256 S_MIN_BID_INCREMENT_PERCENTAGE = 5;
address ATTACKER = makeAddr("attacker"); // posibly the saller second account
vm.deal(ATTACKER, 100 ether);
// Owner mints NFT to the user `SELLER`
vm.startPrank(OWNER);
nft.mint(SELLER);
vm.stopPrank();
// The `SELLER` lists the NFT
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
// BIDDER_1 places a bid
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1}(TOKEN_ID);
BidBeastsNFTMarket.Bid memory firstHighestBid = market.getHighestBid(TOKEN_ID);
BidBeastsNFTMarket.Listing memory listing = market.getListing(TOKEN_ID);
// ATTACKER:
// waits for end of extention auction perioud
vm.warp(listing.auctionEnd - 1 minutes);
console.log("Auction current period due: ", listing.auctionEnd);
// places a bid to increas latest bid value and extend auction period
uint256 secondBidAmount =
firstHighestBid.amount + (firstHighestBid.amount * S_MIN_BID_INCREMENT_PERCENTAGE) / 100;
vm.prank(ATTACKER);
market.placeBid{value: secondBidAmount}(TOKEN_ID);
BidBeastsNFTMarket.Listing memory listingAfter = market.getListing(TOKEN_ID);
console.log("Auction extended period due: ", listingAfter.auctionEnd);
assertTrue(listingAfter.auctionEnd >= block.timestamp);
// Attacker repeats the process until the bid is meat or exceeds the buyNowPrice
BidBeastsNFTMarket.Listing memory listingBuyNow = market.getListing(TOKEN_ID);
vm.prank(BIDDER_2);
vm.expectEmit(true, true, true, true, address(market));
emit AuctionSettled(TOKEN_ID, BIDDER_2, SELLER, listingBuyNow.buyNowPrice);
market.placeBid{value: listingBuyNow.buyNowPrice}(TOKEN_ID);
}

Recommended Mitigation

Possible mitigation is to implement the missing `BidBeastsNFTMarket::endAuction(tokenId)` function to allow anyone to finalize the auction after a three-day duration, as designed, to limit the auction period.

// --- Structs ---.
struct Listing {
address seller;
uint256 minPrice;
uint256 buyNowPrice;
+ uint256 listedDate;
bool listed;
}
...
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
if (_buyNowPrice > 0) {
require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
}
BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
+ listedDate: block.timestamp,
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}
+ function endAuction(uint256 tokenId) external isListed(tokenId) {
+ Listing memory tokenListing = listings[tokenId];
+ Bid memory tokenBid = bids[tokenId];
+ require(
+ block.timestamp >= tokenListing.auctionEnd + 3 days || block.timestamp >= tokenListing.listedDate + 3 days,
+ "Auction did not exceed 3 days period"
+ );
+
+ if (tokenBid.amount >= tokenListing.minPrice) {
+ _executeSale(tokenId);
+ } else {
+ Listing storage listing = listings[tokenId];
+ listing.listed = false;
+
+ BBERC721.transferFrom(address(this), tokenListing.seller, tokenId);
+
+ emit NftUnlisted(tokenId);
+ }
+ }
Updates

Lead Judging Commences

cryptoghost Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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