Trick or Treat

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

Vulnerability in Random Price Mechanism

Summary

The SpookySwap contract’s randomized pricing mechanism in the trickOrTreat function can be manipulated, enabling users to mint NFT treats for free or at a reduced cost. The pseudo-random number generation method used is vulnerable to user manipulation, which can result in a loss of funds for the contract and compromise the integrity of the random pricing model.

Severity

High

This vulnerability has significant financial implications. Malicious users are able to mint NFTs for free or at a reduced cost, leading to:

  • Potential Financial Losses: The contract owner could lose substantial revenue due to the exploit, as attackers bypass the intended minting price.

  • Unfair Advantage: Attackers gain an unfair edge over legitimate users, enabling them to secure NFTs at a lower cost and potentially disrupting the NFT's intended scarcity and market value.

Vulnerability Details

The vulnerability stems from the reliance on predictable variables (block.timestamp, block.prevrandao, msg.sender, and nextTokenId) to generate a pseudo-random outcome in the trickOrTreat function. By controlling these inputs, users can influence the outcome of the "trick" or "treat" probabilities, allowing them to:

  • Receive treats at a reduced cost when the random value favors "treat."

  • Bypass payment requirements entirely by manipulating the random mechanism to bypass payment conditions.

Code Snippet

The following code is from the TrickOrTreat contract:

function trickOrTreat(string memory _treatName) public payable nonReentrant {
Treat memory treat = treatList[_treatName];
require(treat.cost > 0, "Treat cost not set.");
uint256 costMultiplierNumerator = 1;
uint256 costMultiplierDenominator = 1;
// Generate a pseudo-random number between 1 and 1000
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, nextTokenId, block.prevrandao))) % 1000 + 1;
if (random == 1) {
// 1/1000 chance of half price (treat)
costMultiplierNumerator = 1;
costMultiplierDenominator = 2;
} else if (random == 2) {
// 1/1000 chance of double price (trick)
costMultiplierNumerator = 2;
costMultiplierDenominator = 1;
}
// Else, normal price (multiplier remains 1/1)
uint256 requiredCost = (treat.cost * costMultiplierNumerator) / costMultiplierDenominator;
if (costMultiplierNumerator == 2 && costMultiplierDenominator == 1) {
// Double price case (trick)
if (msg.value >= requiredCost) {
// User sent enough ETH
mintTreat(msg.sender, treat);
} else {
// User didn't send enough ETH
// Mint NFT to contract and store pending purchase
uint256 tokenId = nextTokenId;
_mint(address(this), tokenId);
_setTokenURI(tokenId, treat.metadataURI);
nextTokenId += 1;
pendingNFTs[tokenId] = msg.sender;
pendingNFTsAmountPaid[tokenId] = msg.value;
tokenIdToTreatName[tokenId] = _treatName;
emit Swapped(msg.sender, _treatName, tokenId);
// User needs to call fellForTrick() to finish the transaction
}
} else {
// Normal price or half price
require(msg.value >= requiredCost, "Insufficient ETH sent for treat");
mintTreat(msg.sender, treat);
}
// Refund excess ETH if any
if (msg.value > requiredCost) {
uint256 refund = msg.value - requiredCost;
(bool refundSuccess,) = msg.sender.call{value: refund}("");
require(refundSuccess, "Refund failed");
}
}

Exploit Details

An attacker can manipulate the block.timestamp or repeatedly call the function to find favorable random values.
By doing so, the attacker can repeatedly mint NFTs at a reduced or zero cost by forcing the contract to enter the "treat" condition.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/TrickOrTreat.sol";
contract SpookySwapTest is Test {
SpookySwap public spookySwap;
// Declare an array of Treat structs
SpookySwap.Treat[] treats;
function setUp() public {
spookySwap = new SpookySwap(treats);
spookySwap.addTreat("CandyCorn", 1 ether, "ipfs://example-metadata-uri");
spookySwap.setTreatCost("CandyCorn", 1 ether);
}
function testTrickOrTreatBelowThreshold() public {
address sender = address(0x123);
vm.deal(sender, 2 ether);
vm.startPrank(sender);
// Mock the random outcome to be below the threshold (e.g., 0)
vm.mockCall(address(spookySwap), abi.encodeWithSignature("getRandomOutcome()"), abi.encode(0));
// Update block.timestamp and block.prevrandao
vm.warp(block.timestamp + 1); // Move the timestamp forward by 1 second
vm.roll(block.number + 1); // Move to the next block
(string memory name, uint256 cost, string memory metadataURI) = spookySwap.treatList("CandyCorn");
uint256 normalCost = cost;
(bool success,) = address(spookySwap).call{value: normalCost}(
abi.encodeWithSignature("trickOrTreat(string)", "CandyCorn")
);
assertTrue(success, "Transaction should succeed when random number is below threshold");
assertEq(spookySwap.balanceOf(sender), 1, "Should own 1 NFT after purchase");
vm.stopPrank();
}
function testTrickOrTreatAboveThreshold() public {
address sender = address(0x123);
vm.deal(sender, 2 ether);
vm.startPrank(sender);
// Mock the random outcome to be above the threshold (e.g., 1001)
vm.mockCall(address(spookySwap), abi.encodeWithSignature("getRandomOutcome()"), abi.encode(1001));
// Update block.timestamp and block.prevrandao
vm.warp(block.timestamp + 1); // Move the timestamp forward by 1 second
vm.roll(block.number + 1); // Move to the next block
(string memory name, uint256 cost, string memory metadataURI) = spookySwap.treatList("CandyCorn");
uint256 normalCost = cost;
(bool success,) = address(spookySwap).call{value: normalCost}(
abi.encodeWithSignature("trickOrTreat(string)", "CandyCorn")
);
assertTrue(success, "Transaction should succeed when random number is above threshold");
assertEq(spookySwap.balanceOf(sender), 1, "Should own 1 NFT after purchase");
vm.stopPrank();
}
}

Impact

This vulnerability allows malicious users to mint NFTs at no cost or at a significantly reduced rate, resulting in potential financial losses for the contract and unfair advantages for users who can manipulate the random outcome.

Tools Used

  • Foundry: Used to write and test the contract in a controlled test environment.

  • Mocking Functions: Mocked block values to simulate manipulation and confirm exploitability.

Recommendations

  1. Use Chainlink VRF or Similar Oracle Solutions: Integrate a secure randomness provider such as Chainlink VRF to ensure unbiased, tamper-proof random outcomes.

  2. Minimize Predictable Variables: Avoid using predictable variables like block.timestamp or block.prevrandao directly in pseudo-random logic, as these can be manipulated by users.

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.