Bid Beasts

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

Precision Loss in Bid Increment Calculation

Medium: Precision Loss in Bid Increment Calculation

Description

  • The minimum bid increment calculation ensures each new bid is at least 5% higher than the previous bid to maintain competitive bidding.

  • The calculation performs division before multiplication, causing precision loss for bid amounts not perfectly divisible by 100.

requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE); // @> Division before multiplication loses precision

Risk

Likelihood:

  • Occurs for any bid amount not perfectly divisible by 100 wei

  • Happens frequently since most ETH amounts in wei are large odd numbers

Impact:

  • Bidders can place lower bids than intended due to rounding down

  • For small bids (< 100 wei), the required increment becomes 0

  • Undermines the minimum increment mechanism

Proof of Concept

This test demonstrates how the division-before-multiplication pattern causes severe precision loss, especially for smaller bid amounts. The issue allows bidders to place much lower bids than the intended 5% increment.

function test_PrecisionLossInBidCalculation() public {
// Test Case 1: Bid of 199 wei
uint256 previousBid = 199 wei;
uint256 expectedRequired = (199 * 105) / 100; // 209 wei (correct)
uint256 actualRequired = (199 / 100) * 105; // 105 wei (buggy)
assertEq(expectedRequired, 209);
assertEq(actualRequired, 105); // Lost 104 wei due to precision loss!
// Test Case 2: Bid of 99 wei results in 0 required amount
previousBid = 99 wei;
expectedRequired = (99 * 105) / 100; // 103 wei (correct)
actualRequired = (99 / 100) * 105; // 0 wei (buggy)
assertEq(expectedRequired, 103);
assertEq(actualRequired, 0); // Complete loss - any bid accepted!
// Test Case 3: Real scenario with 1.5 ETH bid
previousBid = 1.5 ether;
expectedRequired = (previousBid * 105) / 100; // 1.575 ether
actualRequired = (previousBid / 100) * 105; // Also 1.575 ether (works for round numbers)
// But with 1.99 ETH:
previousBid = 1.99 ether;
expectedRequired = (previousBid * 105) / 100; // 2.0895 ether
actualRequired = (previousBid / 100) * 105; // 2.079 ether (loses 0.0105 ether)
}

Recommended Mitigation

Perform multiplication before division to maintain precision. This ensures the full value is preserved during calculation and only rounds at the final step.

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

Alternatively, for even better precision with large numbers:

// Use basis points (10000 = 100%) for finer granularity
uint256 constant S_MIN_BID_INCREMENT_BPS = 500; // 5% = 500 basis points
requiredAmount = previousBidAmount + (previousBidAmount * S_MIN_BID_INCREMENT_BPS) / 10000;
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.

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!