Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Valid

Precision Loss in Price Calculations

Summary

When calculating half-price treats (1/1000 chance), the contract performs division before multiplication, leading to precision loss due to Solidity's integer division rounding down.

Vulnerability Details

Location: src/TrickOrTreat.sol:trickOrTreat()

uint256 requiredCost = (treat.cost * costMultiplierNumerator) / costMultiplierDenominator;

Proof of Concept:

function testPrecisionLossInPriceCalculation() public {
// Set a treat cost that will demonstrate precision loss when halved
vm.prank(owner);
spookySwap.setTreatCost("Candy", 3);
// Fund user1 with enough ETH
vm.deal(user1, 3);
// Control all random number inputs
uint256 mockTimestamp = 1234567;
vm.warp(mockTimestamp); // Set block.timestamp
vm.prevrandao(bytes32(uint256(1))); // Set prevrandao
// Calculate what the random number will be
uint256 expectedRandom = uint256(keccak256(abi.encodePacked(
mockTimestamp,
user1,
spookySwap.nextTokenId(),
uint256(1)
))) % 1000 + 1;
console.log("Expected random number:", expectedRandom);
// If the random number isn't 1 (treat case), adjust the timestamp until it is
while (expectedRandom != 1) {
mockTimestamp++;
vm.warp(mockTimestamp);
expectedRandom = uint256(keccak256(abi.encodePacked(
mockTimestamp,
user1,
spookySwap.nextTokenId(),
uint256(1)
))) % 1000 + 1;
}
// Attempt to buy at half price (should be 1.5 wei)
vm.prank(user1);
spookySwap.trickOrTreat{value: 2}("Candy");
// Check if the user received the NFT
uint256 tokenId = spookySwap.nextTokenId() - 1;
assertEq(spookySwap.ownerOf(tokenId), user1, "User should have received NFT");
// Log the actual cost and the amount paid
(, uint256 actualCost,) = spookySwap.treatList("Candy");
console.log("Original cost:", actualCost);
console.log("Half price (with precision loss):", actualCost / 2);
}

Impact

When treat costs are not even numbers, the half-price calculation will round down, potentially leading to incorrect pricing. For example, with a cost of 3 wei, half price should be 1.5 wei but will be calculated as 1 wei due to integer division. This affects not only the half-price scenario but could also impact refunds when users overpay, potentially leading to trapped dust amounts in the contract.

Tools Used

Forge

Recommendations

Consider using a fixed-point arithmetic library or restructuring the calculations to avoid precision loss. Alternatively, ensure all treat costs are even numbers.

Updates

Appeal created

bube Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Very small cost

In case of treat, if the cost of the treat is very small, the user can get NFT for zero `requiredCost` due to rounding. Also, if the cost is small and odd, the user may get a given NFT at a lower price than intended.

Support

FAQs

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