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.
Risk
Likelihood: High
Impact: High
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");
vm.deal(ATTACKER, 100 ether);
vm.startPrank(OWNER);
nft.mint(SELLER);
vm.stopPrank();
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
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);
vm.warp(listing.auctionEnd - 1 minutes);
console.log("Auction current period due: ", listing.auctionEnd);
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);
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);
+ }
+ }