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