Bid Beasts

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

Precision loss

Root + Impact

Description

Calaculating the minimum Bid increese must me exact 5% from the previous.

Currently there is loss of precision issue due to not correct calculations of requiredAmount in placeBid function

function placeBid(uint256 tokenId) external payable isListed(tokenId) {
More code...
if (previousBidAmount == 0) {
requiredAmount = listing.minPrice;
require(msg.value > requiredAmount, "First bid must be > min price");
listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
@> requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
require(msg.value >= requiredAmount, "Bid not high enough");
uint256 timeLeft = 0;
if (listing.auctionEnd > block.timestamp) {
timeLeft = listing.auctionEnd - block.timestamp;
}
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
More code...
}

Risk

Likelihood:

It will happen every time there is when there is decimal that solidity cannot handle
In the lifetime of a Auction it can stack multiple times


Impact:

consistent loss of percision in some cases making the bidding possible with less that exactly 5% increment.

Proof of Concept

The test shows how the wrong calculation affect the price of the bid

The problematic way of calculating compared to the correct way.
Added comments so it can be easier to read and understand.

function test_takeHighestBidPrecisionPoC() public {
uint256 minBidIncrementPercentage = market.S_MIN_BID_INCREMENT_PERCENTAGE();
_mintNFT();
_listNFT();
// required to prove the calculations are not rounding properly
uint256 newBid = MIN_PRICE + 199999999999999999; // 1.199999999999999 ether
vm.prank(BIDDER_1);
market.placeBid{value: newBid}(TOKEN_ID);
// 105 Exact MIN_BID_INCREMENT_PERCENTAGE
uint256 secondBidAmount = (newBid * (100 + minBidIncrementPercentage)) / 100;
vm.prank(BIDDER_2);
market.placeBid{value: secondBidAmount}(TOKEN_ID);
/*//////////////////////////////////////////////////////////////
CALCULATIONS
//////////////////////////////////////////////////////////////*/
BidBeastsNFTMarket.Bid memory highestBid = market.getHighestBid(TOKEN_ID);
// reusing the logic from the contract to show the difference
// requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
uint256 wrongCalculation = (highestBid.amount / 100) * (100 + minBidIncrementPercentage);
uint256 correctCalculation = (highestBid.amount * (100 + minBidIncrementPercentage)) / 100;
console.log("Wrong Calculation:", wrongCalculation); // 1322999999999999895
console.log("Correct Calculation:", correctCalculation); // 1322999999999999997
// 1322999999999999997 - 1322999999999999895 = 102
console.log("Difference:", correctCalculation - wrongCalculation); // 102
// I can simply subtract 102 wei to make it pass the require in `placeBid()`
/*//////////////////////////////////////////////////////////////
CALCULATIONS
//////////////////////////////////////////////////////////////*/
// Subtracting 102 wei to make it revert, but its does not
uint256 thirdBidAmount = (secondBidAmount * (100 + minBidIncrementPercentage) / 100) - 102 wei;
vm.prank(BIDDER_3);
market.placeBid{value: thirdBidAmount}(TOKEN_ID);
}

Recommended Mitigation

Multiplication must happent before division.
After replacing the line the PoC test should fail! And it needs to remove the - 102 wei part to run correctly just like the new calculations.

function placeBid(uint256 tokenId) external payable isListed(tokenId) {
More code...
if (previousBidAmount == 0) {
requiredAmount = listing.minPrice;
require(msg.value > requiredAmount, "First bid must be > min price");
listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
} 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");
uint256 timeLeft = 0;
if (listing.auctionEnd > block.timestamp) {
timeLeft = listing.auctionEnd - block.timestamp;
}
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
More code...
}
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!