Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Predictable Randomness

Summary

The PuppyRaffle contract is vulnerable to predictable randomness attacks. This vulnerability arises due to the contract's relying on block.timestamp and keccak256 for generating random numbers. Since miners have some control over the block timestamp, and keccak256 is deterministic, they could potentially manipulate the outcome of transactions that depend on these pseudo-random numbers and can determine the index of the next winner. Additionally, an external contract or proxy could be used to predict or influence the outcome of the selectWinner function through front running the out come

Vulnerability Details

The timestamp of each block is set by the miner who mines that block. While there are rules that limit how much a miner can skew the timestamp, there is still some room for manipulation. This contract's functionality relies heavily on the block timestamp, a miner could influence the contract's behavior by adjusting the timestamp of the blocks they mine to their own advantage in the raffle.

The keccak256 function is a deterministic hash function, meaning it will always produce the same output for the same input. The inputs to the keccak256 function are predictable and can be influenced by an attacker, thus the output can also be predicted or influenced. An attacker could potentially observe a transaction that is likely to win the raffle, copy this transaction, and submit it with a higher gas price to preempt the original transaction.

Proof of Concept (PoC)

Consider a scenario where the selectWinner function block timestamp and keccak256 to determine the winner of a raffle are manipulated:

function selectWinner() public {
uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender));
uint256 winnerIndex = seed % players.length;
address winner = players[winnerIndex];
// rest of the function logic
}

In this scenario, a miner can influence the outcome of the raffle by adjusting the timestamp of the block in which the selectWinner transaction is included.

.

Front-Running

Another scenario is when an attacker could also use an external contract or proxy to manipulate the outcome. For example, an attacker could create a contract that calls the selectWinner function with a specific timestamp and sender address that would result in the attacker being selected as the winner

Here's an example of how the attacker's transaction could look:

// Attacker's front-running transaction
function frontRunSelectWinner() external {
// Crafted parameters to mimic a winning transaction
uint256 attackerSeed = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender)));
// Determine the winning index
uint256 attackerWinnerIndex = attackerSeed % players.length;
// Address of the attacker as the "winner"
address attackerWinner = players[attackerWinnerIndex];
// Crafted parameters for prize pool and fees
uint256 totalAmountCollected = players.length * entranceFee;
uint256 attackerPrizePool = (totalAmountCollected * 80) / 100;
uint256 attackerFee = (totalAmountCollected * 20) / 100;
// Update totalFees
totalFees = totalFees + uint64(attackerFee);
// Generate a unique token ID for the attacker
uint256 tokenId = totalSupply();
// Determine rarity based on a pseudo-random number
uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
// Set the rarity for the token
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
// Clear the list of players, update raffle start time, and store the previous winner
delete players;
raffleStartTime = block.timestamp;
previousWinner = attackerWinner;
// Send the prize pool to the attacker
(bool success,) = attackerWinner.call{value: attackerPrizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to attacker");
// Mint the winning NFT for the attacker
_safeMint(attackerWinner, tokenId);
}

Here the attacker is mimicking the winning transaction but with parameters under their control. They manipulate the attackerSeed and calculate the "winning" index, claiming the prize for themselves

Impact

If a miner or an attacker can influence or predict the outcome of the raffle, it could lead to unfair results and loss of trust in the contract. This could potentially disrupt the operation of the raffle and lead to a loss of funds.

Recommendations

To mitigate this vulnerability, avoid relying on block.timestamp and keccak256 for generating pseudo-random numbers. Instead, consider using a more secure method for generating random numbers, such as using the Chainlink VRF. This will ensure that the outcome of the raffle cannot be manipulated by miners or predicted by attackers.

Here's how you could implement this change:

import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
contract PuppyRaffle is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
uint256 public randomResult;
constructor()
VRFConsumerBase(
0xf0d54349aDdcf704F77AE15b96510dEA15cb7952, // VRF Coordinator
0x514910771AF9Ca656af840dff83E8264EcF986CA // LINK Token
) public
{
keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4;
fee = 0.1 * 10 ** 18; // 0.1 LINK
}
function selectWinner() public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");
return requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
randomResult = randomness;
uint256 winnerIndex = randomResult % players.length;
address winner = players[winnerIndex];
// rest of the function logic
}
}

This implementation ensures that the randomness used to determine the winner of the raffle is secure and cannot be manipulated by miners or predicted by attackers. The Chainlink VRF provides a provably fair and verifiable source of randomness directly on-chain, which is a significant improvement over using block.timestamp and keccak256.

  • To mitigate front running vulnerability, consider implementing a commit-reveal scheme. In a commit-reveal scheme, users first submit a hashed version of their input (the "commit"), and then reveal their input after all commits have been submitted. This ensures that no user can preempt another user's input.

Here's how you could implement this change:

mapping(address => bytes32) public commits;
function commit(bytes32 _commit) public {
commits[msg.sender] = _commit;
}
function reveal(uint256 _seed) public {
require(keccak256(abi.encodePacked(_seed)) == commits[msg.sender], "Invalid reveal");
uint256 winnerIndex = _seed % players.length;
address winner = players[winnerIndex];
// rest of the function logic
}

In this implementation, users first call the commit function with a hash of their seed. After all users have committed, they call the reveal function with their seed. The contract checks that the revealed seed matches the commit, and then uses the seed to determine the winner of the raffle.

Updates

Lead Judging Commences

Hamiltonite Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

weak-randomness

Root cause: bad RNG Impact: manipulate winner

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!