Bid Beasts

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

Potential Reentrancy in NFT Transfer and Payment

Description

The _executeSale function transfers the NFT to the winning bidder before sending the payment to the seller, which could allow a malicious bidder to execute a reentrancy attack.

Expected Behavior

State changes should be completed before external calls to prevent reentrancy attacks.

Actual Behavior

The NFT is transferred to the bidder before the ETH is sent to the seller, allowing a malicious bidder to potentially reenter the contract and manipulate state.

Root Cause

The function performs an external call (BBERC721.transferFrom) before updating critical state and making another external call (_payout):

function _executeSale(uint256 tokenId) internal {
Listing storage listing = listings[tokenId];
Bid memory bid = bids[tokenId];
listing.listed = false;
delete bids[tokenId];
BBERC721.transferFrom(address(this), bid.bidder, tokenId); // External call
uint256 fee = (bid.amount * S_FEE_PERCENTAGE) / 100;
s_totalFee += fee;
uint256 sellerProceeds = bid.amount - fee;
_payout(listing.seller, sellerProceeds); // Another external call
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}

Risk Assessment

Impact

If exploited, a malicious bidder could potentially manipulate the contract state during the reentrancy, leading to theft of funds or other unexpected behavior.

Likelihood

The likelihood is medium because it requires the bidder to be a malicious contract capable of executing reentrancy attacks, and the attack vector depends on the specific implementation of the NFT contract.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/BidBeastsNFTMarketPlace.sol";
import "../src/BidBeasts_NFT_ERC721.sol";
// Malicious bidder contract
contract MaliciousBidder {
BidBeastsNFTMarket market;
uint256 tokenId;
constructor(address _market, uint256 _tokenId) {
market = BidBeastsNFTMarket(_market);
tokenId = _tokenId;
}
// Function to place a bid
function bid() external payable {
market.placeBid{value: msg.value}(tokenId);
}
// Callback function when receiving the NFT
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
// Perform reentrancy attack here
// For example, try to manipulate state or withdraw funds
return this.onERC721Received.selector;
}
receive() external payable {}
}
contract ExploitTest is Test {
BidBeastsNFTMarket market;
BidBeasts nft;
address seller = address(0x1);
MaliciousBidder attacker;
uint256 tokenId = 0;
function setUp() public {
// Deploy contracts
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
// Mint NFT to seller
vm.prank(address(nft.owner()));
nft.mint(seller);
// List NFT
vm.startPrank(seller);
nft.approve(address(market), tokenId);
market.listNFT(tokenId, 1 ether, 5 ether);
vm.stopPrank();
// Deploy attacker contract
attacker = new MaliciousBidder(address(market), tokenId);
vm.deal(address(attacker), 2 ether);
}
function testExploit() public {
// Place bid from attacker
attacker.bid{value: 1.1 ether}();
// Fast forward time to end auction
vm.warp(block.timestamp + market.S_AUCTION_EXTENSION_DURATION() + 1);
// Settle auction - this will trigger the reentrancy attack
market.settleAuction(tokenId);
// Check results
// The specific assertions would depend on what the reentrancy attack is trying to achieve
}
}

Recommended Mitigation

Implement the checks-effects-interactions pattern by updating all state before making external calls:

// Before: Vulnerable code
function _executeSale(uint256 tokenId) internal {
Listing storage listing = listings[tokenId];
Bid memory bid = bids[tokenId];
listing.listed = false;
delete bids[tokenId];
BBERC721.transferFrom(address(this), bid.bidder, tokenId);
uint256 fee = (bid.amount * S_FEE_PERCENTAGE) / 100;
s_totalFee += fee;
uint256 sellerProceeds = bid.amount - fee;
_payout(listing.seller, sellerProceeds);
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}
// After: Fixed code
function _executeSale(uint256 tokenId) internal {
Listing storage listing = listings[tokenId];
Bid memory bid = bids[tokenId];
address bidder = bid.bidder;
address seller = listing.seller;
uint256 amount = bid.amount;
// Update all state first
listing.listed = false;
delete bids[tokenId];
uint256 fee = (amount * S_FEE_PERCENTAGE) / 100;
s_totalFee += fee;
uint256 sellerProceeds = amount - fee;
// Then make external calls
BBERC721.transferFrom(address(this), bidder, tokenId);
_payout(seller, sellerProceeds);
emit AuctionSettled(tokenId, bidder, seller, amount);
}

Explanation

The fixed implementation follows the checks-effects-interactions pattern by updating all state variables before making any external calls, which prevents reentrancy attacks.

Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months 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.

Give us feedback!