Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Invalid

Treat cost manipulation

Summary

Description: The User in the SpookySwap contract can call trickOrTreat function to take part in the random game. If the random number is more than 2 the user will pay the full price of the treat, and the contract mints the treat to the user account. But if the random number is less than 2 the user will pay half of the price of the treat, and the contract mints the treat to the user account. The random number equals 2 - the user will pay double the price of the treat.
If the user did not pass the double price amount to the smart contract. The smart contract mint NFT to itself and the User can top up the balance of the smart contract to get the NFT in the resolveTrick function. However, the issue is that the resolveTrick function checks the current price of the treat. So if the admin changes this price between the trickOrTreat and resolveTrick function calls, the user will pay a new price for the treat.

Line of code: TrickOrTreat.sol#L124

Vulnerability Details

This behavior can be used by the attacker to manipulate the price of the treat. Or even free mint of NFT. For example, if the admin sets the treat price equal to zero, the user can withdraw already spent funds while calling the resolveTrick function.
Let's consider the following scenario:

  1. On line number 124 the resolveTrick function checks the current price of the treat and double it.

  2. If the treat price is equal to zero, the totalPaid would be more than requiredCost, and it triggers the if conditional on the line number 139

  3. The user will receive the full price he already spends in the first call of the trickOrTreat function.

Proof of Code: Let's proof the following logic with the following test case:

function test_trickOrTreat_randomIsTwo() public {
vm.startPrank(user1);
vm.prevrandao(bytes32(uint256(1)));
vm.warp(61);
// User1 buys a treat with double price
trick.trickOrTreat{value: 1 ether}("Candy");
(string memory name, uint256 cost, ) = trick.treatList("Candy");
uint256 expectedCost = cost * 2;
uint256 requiredCost = (cost * 2) / 1;
assertEq(expectedCost, requiredCost, "Double-price cost did not apply as expected");
vm.stopPrank();
// Owner sets the cost of the treat to 0
vm.startPrank(owner);
trick.setTreatCost("Candy", 0);
vm.stopPrank();
uint256 balanceUser1Before = address(user1).balance;
vm.startPrank(user1);
trick.resolveTrick{value: 1 ether}(1);
uint256 balanceUser1After = address(user1).balance;
vm.stopPrank();
// User1 should receive 1 ether back from the contract
assertEq(balanceUser1After - balanceUser1Before, 1 ether, "+1 ether expected");
}

Impact

  • The owner can manipulate the price of the treat.

  • The user can withdraw already spent funds.

  • The user can mint NFT for free.

  • The user can spend more than the initial price of the treat that he won.

Tools Used

  • Manual code analysis

  • Foundry for testing

Recommendations

The resolveTrick function should not check the current price of the treat. The smart contract should also save the price of the treat at the moment of the trickOrTreat function call.

Updates

Appeal created

bube Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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