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

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.