Bid Beasts

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

Potential Race Condition in Auction Settlement

Description

  • The settleAuction function enables any user to finalize an auction once the auction end time has passed and the highest bid meets or exceeds the minimum price, by executing the sale which transfers the NFT to the winning bidder, calculates and accumulates fees, pays out the seller, and emits an event.

  • Multiple concurrent calls to settleAuction for the same tokenId can result in only one successful execution while others fail after partial processing if the payout fails, potentially causing inconsistencies in failedTransferCredits additions without completing the full finalization for subsequent calls, as the function lacks reentrancy guards or atomicity checks beyond the isListed modifier.

function settleAuction(uint256 tokenId) external isListed(tokenId) {
Listing storage listing = listings[tokenId];
//@> require(listing.auctionEnd > 0, "Auction has not started (no bids)");
//@> require(block.timestamp >= listing.auctionEnd, "Auction has not ended");
//@> require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");
_executeSale(tokenId);
}

Risk

Likelihood:

  • Multiple transactions mine in the same block targeting the same ended auction.

  • Network congestion delays transaction confirmations allowing overlapping settlement attempts.

Impact:

  • Failed transactions consume gas without effect, wasting user resources.

  • Inconsistent state if payout fails in the successful call, leading to misplaced credits in failedTransferCredits.

Proof of Concept

// Assume auction for tokenId=1 has ended with valid bid
// Attacker or multiple users submit txs to settleAuction(1) simultaneously
// First tx succeeds: executes _executeSale, sets listed=false, transfers NFT, attempts payout
// If payout fails, adds to failedTransferCredits[seller]
// Subsequent txs revert at isListed modifier since listed=false
// No additional deductions or changes occur, but users pay gas for failed txs

Recommended Mitigation

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

Incorrect Address Utilization in 'withdrawAllFailedCredits'

Description

  • The withdrawAllFailedCredits function allows a user to withdraw accumulated failed transfer credits for a specified receiver by transferring the amount to msg.sender if the transfer succeeds, and resetting the credit to zero.

  • The function reads the credit amount from failedTransferCredits[_receiver] but resets failedTransferCredits[msg.sender] to zero, creating a mismatch where credits for _receiver are read but msg.sender's credits are cleared, potentially allowing unauthorized clearing of credits or withdrawal of wrong amounts if _receiver differs from msg.sender.

function withdrawAllFailedCredits(address _receiver) external {
//@> uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
//@> failedTransferCredits[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount} ("");
require(success, "Withdraw failed");
}

Risk

Likelihood:

  • Users specify a _receiver different from msg.sender in the function call.

  • Malicious actors exploit the function by providing arbitrary _receiver addresses.

Impact:

  • Wrong user's credits get withdrawn to msg.sender, leading to fund theft.

  • Legitimate credits for msg.sender remain uncleared while another's are reset.

Proof of Concept

// Assume failedTransferCredits[alice] = 1 ether, failedTransferCredits[bob] = 0
// Bob calls withdrawAllFailedCredits(alice)
// amount = 1 ether, resets failedTransferCredits[bob] = 0 (no effect)
// Transfers 1 ether to bob, but alice's credits remain 1 ether (not reset)

Recommended Mitigation

function withdrawAllFailedCredits(address _receiver) external {
- uint256 amount = failedTransferCredits[_receiver];
+ uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
- failedTransferCredits[msg.sender] = 0;
+ failedTransferCredits[_receiver] = 0;
(bool success, ) = payable(msg.sender).call{value: amount} ("");
require(success, "Withdraw failed");
}

Potential DOS with Failed Auctions

Description

  • Auctions proceed with bidding, and upon ending, can be settled if the highest bid meets the minPrice, or unlisted by the seller if no bids were placed.

  • If an auction receives bids but the highest does not meet minPrice, it remains listed indefinitely without ability to settle (due to require(bids.amount >= minPrice)) or unlist (due to require(bidder == address(0)) in unlistNFT), trapping the NFT in a permanent listed state where the seller cannot recover it.

function unlistNFT(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
//@> require(bids[tokenId].bidder == address(0), "Cannot unlist, a bid has been placed");
// ...
}
function settleAuction(uint256 tokenId) external isListed(tokenId) {
// ...
//@> require(bids[tokenId].amount >= listing.minPrice, "Highest bid did not meet min price");
// ...
}

Risk

Likelihood:

  • Bidders place amounts below minPrice before auction ends.

  • Auction ends without meeting minPrice threshold.

Impact:

  • Seller unable to retrieve or relist the NFT, causing permanent lockup.

  • Marketplace credibility suffers from stuck auctions.

Proof of Concept

// Seller lists NFT with minPrice=1 ether
// Bidder places 0.5 ether bid, auction starts and ends
// settleAuction reverts: bid < minPrice
// unlistNFT reverts: bidder != address(0)
// NFT stuck in contract, listed=true

Recommended Mitigation

+ function cancelFailedAuction(uint256 tokenId) external isSeller(tokenId, msg.sender) {
+ Listing storage listing = listings[tokenId];
+ require(listing.listed, "Not listed");
+ require(listing.auctionEnd > 0 && block.timestamp >= listing.auctionEnd, "Auction not ended");
+ require(bids[tokenId].amount < listing.minPrice, "Can settle instead");
+
+ address bidder = bids[tokenId].bidder;
+ if (bidder != address(0)) {
+ _payout(bidder, bids[tokenId].amount); // Refund invalid bid
+ }
+ delete bids[tokenId];
+ listing.listed = false;
+ listing.auctionEnd = 0;
+ BBERC721.transferFrom(address(this), msg.sender, tokenId);
+ }

Underestimated Condition in Unlisting Logic

Description

  • The unlistNFT function permits the seller to remove a listed NFT from the marketplace if no bids have been placed, by checking if the bidder address is zero and transferring the NFT back.

  • The check solely relies on bids[tokenId].bidder == address(0), which may not account for scenarios where bids were placed but later invalidated or deleted without fully resetting the bid struct, or if tokenId reuse/sequence issues allow interference from prior listings, potentially allowing unlisting with lingering bid data or preventing it erroneously.

function unlistNFT(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
//@> require(bids[tokenId].bidder == address(0), "Cannot unlist, a bid has been placed");
// ...
}

Risk

Likelihood:

  • Bids get partially reset in other functions without clearing bidder.

  • TokenIds get relisted without fully deleting prior bid data.

Impact:

  • Seller unable to unlist despite no active bids, locking NFT.

  • Potential unlisting with unrefunded bids, leading to fund loss for bidders.

Proof of Concept

// Assume bug in another function sets bid.amount=0 but leaves bidder nonzero
// Seller calls unlistNFT, reverts despite no effective bid
// Or vice versa: bidder=0 but amount>0, allows unlist without refund

Recommended Mitigation

function unlistNFT(uint256 tokenId) external isListed(tokenId) isSeller(tokenId, msg.sender) {
- require(bids[tokenId].bidder == address(0), "Cannot unlist, a bid has been placed");
+ Bid storage bid = bids[tokenId];
+ require(bid.bidder == address(0) && bid.amount == 0, "Cannot unlist, bid data present");
}
Updates

Lead Judging Commences

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

BidBeast Marketplace: Unrestricted FailedCredits Withdrawal

withdrawAllFailedCredits allows any user to withdraw another account’s failed transfer credits due to improper use of msg.sender instead of _receiver for balance reset and transfer.

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.