Bid Beasts

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

Precision loss in subsequent bid calculation

Root + Impact

Description

  • The auction’s minimum increment logic suffers from integer division precision loss because the contract divides before multiplying:

  • This means bidders can submit underpriced bids that the contract accepts, undermining the auction’s increment protection and systematically reducing seller revenue.

requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
require(msg.value >= requiredAmount, "Bid not high enough");

Risk

Likelihood: Medium

  • The bug triggers whenever bids are not divisible.

  • Attackers can deliberately exploit this to save ETH while still winning.

Impact: Low

  • Sellers consistently lose value with each accepted underpriced bid.

  • Auction integrity is broken, as increment rules are not enforced correctly.

Proof of Concept

This test demonstrates a precision loss vulnerability in the bidding system's minimum increment calculation. The test proves the vulnerability exists by comparing the correct calculation with the contract's flawed implementation.

function test_precisionLoss() public {
_mintNFT();
_listNFT();
// Fund bidders
vm.deal(BIDDER_1, 1 ether);
vm.deal(BIDDER_2, 1 ether);
// BIDDER_1 places first bid of 0.011 ETH (> 0.01 ETH min price)
vm.prank(BIDDER_1);
market.placeBid{value: 0.011 ether}(TOKEN_ID);
// Pull increment percentage
uint256 incPct = market.S_MIN_BID_INCREMENT_PERCENTAGE();
// Current highest bid
BidBeastsNFTMarket.Bid memory current = market.getHighestBid(TOKEN_ID);
// Correct next min (multiply-first)
uint256 expectedNext = (current.amount * (100 + incPct)) / 100;
// Buggy contract calc (divide-first)
uint256 contractCalc = (current.amount / 100) * (100 + incPct);
// Logs for clarity
emit log_named_uint("Current bid", current.amount);
emit log_named_uint("Expected next min", expectedNext);
emit log_named_uint("Contract-calculated next min", contractCalc);
// Make sure contractCalc < expectedNext
assertTrue(contractCalc < expectedNext, "Bug not triggered");
// BIDDER_2 exploits by bidding contractCalc (cheaper than expectedNext)
vm.prank(BIDDER_2);
market.placeBid{value: contractCalc}(TOKEN_ID);
// Verify BIDDER_2 is now highest bidder despite underpaying
BidBeastsNFTMarket.Bid memory highest = market.getHighestBid(TOKEN_ID);
assertEq(highest.bidder, BIDDER_2);
assertEq(highest.amount, contractCalc);
assertTrue(highest.amount < expectedNext, "Accepted bid was less than true minimum increment");
}

Recommended Mitigation

Replace the vulnerable calculation with the mathematically correct version that performs multiplication before division:

- requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
+ requiredAmount = (previousBidAmount * (100 + S_MIN_BID_INCREMENT_PERCENTAGE)) / 100;
Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeasts Marketplace: Integer Division Precision Loss

Integer division in requiredAmount truncates fractions, allowing bids slightly lower than intended.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!