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 21 days 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.