Bid Beasts

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

[M-1] The `BidBeastsNFTMarket::takeHighestBid` function allow seller to settle the auction before the auction ends accepting the highest bid, creating an MEV attack opportunity for frontrunning.

The BidBeastsNFTMarket::takeHighestBid function allow seller to settle the auction before the auction ends accepting the highest bid, creating an MEV attack opportunity for frontrunning.

Description

  • The BidBeastsNFTMarket::takeHighestBid function allows the seller to settle the auction before the auction ends accepting the highest bid, allowing an attacker to frontrun the seller’s transaction to take the highest bid by placing a higher bid just before the seller’s transaction is mined.

  • This vulnerability creates a Miner Extractable Value (MEV) attack opportunity for frontrunning the seller’s transaction to accept the highest bid, disrupting the protocol’s intended functionality and allowing attackers to manipulate auction results.

function takeHighestBid(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
//@>
Bid storage bid = bids[tokenId];
require(bid.amount >= listings[tokenId].minPrice, "Highest bid is below min price");
_executeSale(tokenId);
}

Risk

Likelihood: Medium

  • It might occur under specific conditions

  • This attack may be executed only if seller decides to use ``takeHighestBid`` which is a part of the main flow.

Impact: Medium

  • 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.

This test is designed to demonstrate the Miner Extractable Value (MEV) attack opportunity for frontrunning the seller’s transaction to take the highest bid in the `BidBeastsNFTMarket::takeHighestBid` function.

function testFrontRunningTakeHighestBid() public {
// Arrange
uint256 S_MIN_BID_INCREMENT_PERCENTAGE = 5;
// attacker address
address ATTACKER = makeAddr("attacker");
console.log("\n Arrange");
console.log("ATTACKER address: ", ATTACKER);
console.log("BIDDER_1 address: ", BIDDER_1);
console.log("BIDDER_2 address: ", BIDDER_2);
// 1. Contract owner mints NFT to seller
vm.startPrank(OWNER);
nft.mint(SELLER);
vm.stopPrank();
// 2. Seller lists NFT
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
// 3. Pre defined Bidders 1 places a bid
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1}(TOKEN_ID);
BidBeastsNFTMarket.Bid memory firstHighestBid = market.getHighestBid(TOKEN_ID);
console.log("\n Auction started\n");
console.log("First bid has been placed\n");
console.log("BIDDER_1 bid: ", firstHighestBid.amount);
console.log("BIDDER_1 bidder: ", firstHighestBid.bidder);
uint256 secondBidAmount =
firstHighestBid.amount + (firstHighestBid.amount * S_MIN_BID_INCREMENT_PERCENTAGE) / 100;
vm.prank(BIDDER_2);
market.placeBid{value: secondBidAmount}(TOKEN_ID);
BidBeastsNFTMarket.Bid memory secondHighestBid = market.getHighestBid(TOKEN_ID);
console.log("\n");
console.log("Second bid has been placed\n");
console.log("BIDDER_2 bid: ", secondHighestBid.amount);
console.log("BIDDER_2 bidder: ", secondHighestBid.bidder);
console.log("\n");
console.log("Seller decide to take highest bid");
console.log("Seller's transaction has been placed to mempool");
uint256 attackerBidAmount =
secondHighestBid.amount + (secondHighestBid.amount * S_MIN_BID_INCREMENT_PERCENTAGE) / 100;
hoax(ATTACKER, 100 ether);
market.placeBid{value: attackerBidAmount}(TOKEN_ID);
BidBeastsNFTMarket.Bid memory attackerHighestBid = market.getHighestBid(TOKEN_ID);
console.log("\n");
console.log("Attacker's bid has been placed, with a higher preority fee");
console.log("Attacker's bid: ", attackerHighestBid.amount);
console.log("Attacker's bidder: ", attackerHighestBid.bidder);
console.log("\n");
console.log("Seller's transaction has been included in the block");
vm.prank(SELLER);
market.takeHighestBid(TOKEN_ID);
console.log("Supposed winner of the NFT auction is BIDDER_2: ", secondHighestBid.bidder);
console.log("Actual winner of the NFT auction is ATTACKER: ", attackerHighestBid.bidder);
assertEq(attackerHighestBid.bidder, nft.ownerOf(TOKEN_ID), "Attacker has won the NFT auction");
}

Recommended Mitigation

Possible mitigations are:

1. remove this function as it is not crutial for the protocol functionality.

2. inforce a cooldown after every bid, except the bid that is equal or greater than the `buyNowPrice`, to allow seller to accept the highest bid before the auction ends.

...
// --- Constants ---
uint256 public constant S_AUCTION_EXTENSION_DURATION = 15 minutes;
uint256 public constant S_MIN_NFT_PRICE = 0.01 ether;
uint256 public constant S_FEE_PERCENTAGE = 5;
uint256 public constant S_MIN_BID_INCREMENT_PERCENTAGE = 5;
+ uint256 public constant S_COOLDOWN_DURATION = 1 minutes;
// --- State Variables ---
uint256 public s_totalFee;
+ uint256 public s_cooldown; // reset after every bid
mapping(uint256 => Listing) public listings;
mapping(uint256 => Bid) public bids;
mapping(address => uint256) public failedTransferCredits;
...
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
...
+ require(block.timestamp > s_cooldown, "Cooldown period not passed");
require(msg.sender != previousBidder, "Already highest bidder");
...
// EFFECT: update highest bid
bids[tokenId] = Bid(msg.sender, msg.value);
+ s_cooldown = block.timestamp + S_COOLDOWN_DURATION;
...
}
...
function takeHighestBid(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
+ require(listings[tokenId].auctionEnd > block.timestamp, "Auction has ended");
Bid storage bid = bids[tokenId]; // creates a link to bids[tokenId] value, why ?
require(bid.amount >= listings[tokenId].minPrice, "Highest bid is below min price");
_executeSale(tokenId);
}
...
Updates

Lead Judging Commences

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

BidBeast Marketplace: Auction Duration Miscalculation

BidBeast marketplace contains a flaw in its auction timing mechanism. This causes the contract to miscalculate the actual end time of an auction, resulting in auctions that either conclude prematurely or run longer than specified.

Support

FAQs

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