Using deterministic inputs for the random number block.difficulty, block.number and msg.sender creates a pseudo-random number when calculating the winner in PuppyRaffle::selectWinner. The value of block.number can be determined and block.difficulty is always 0 since the migration of proof-of-work (PoW) to proof-of-stake (PoS). By executing a transaction in the same block and modifying msg.sender, the desired winner index can be selected and the winner controlled.
In PuppyRaffle:selectWinner on line 128-130, the winner is determined using the following calculation:
The block.timestamp, msg.sender, and block.difficulty have been used as seed values for the random index for the players array. Since using block-related properties lead to pseudo-random numbers (see the "Bad Randomness Is Even Dicier than You Think" Medium article for more information), the winnerIndex and therefore the winner are deterministic and subject to manipulation.
msg.sender can be controlled by the selectWinner() caller and since the migration from PoW to PoS, block.difficulty is always 0, the only value which varies is block.number. Since block.number can be determined by including a transaction in the same block, the required value of msg.sender can be calculated for a given block.number to yield the desired winnerIndex and manipulate the winner of the lottery.
The impact of bad randomness used to determine the winner of a lottery is that an attacker can control who wins the lottery by either simulating a transaction and waiting until the block.number yields the desired winnerIndex for a given msg.sender address or by calculating the required msg.sender for a given block.number. This means that the lottery is not truly random or fair and is therefore a high-severity vulnerability as the fairness of a lottery is a fundamental capability.
There are two ways to manipulate the winnerIndex:
Perform the winnerIndex calculation and wait for the block.number to yield the desired winnerIndex for a constant msg.sender address.
Calculate the msg.sender address required to yield the desired winnerIndex for a given block.number.
To calculate which address would be needed to yield the desired winnerIndex, an attacker can run a script that generates addresses using CREATE2. When the script finds the required address, that satisfies the conditions of yielding the required winnerIndex for a given block.number, the address can be deployed using the generated salt. This address can then be used to call selectWinner() which will determine the winner to be the address the attacker desires.
The attacker could modify a scrip e.g. https://github.com/0age/create2crunch but change the stopping condition to be when the desired index is calculated for a given block.number.
Block-related properties, when used as seed values to create random numbers, can be viewed and the random values exploited. Therefore, they should not be used as seed values for random number generation as they are deterministic and are therefore pseudo-random.
Instead, random numbers should be calculated off-chain where possible, or verifiably random functions e.g. Chainlink VRF, should be used to generate random values. VRF uses an Oracle network to generate random numbers and verify their randomness before posting on-chain. VRF uses open-source code and cryptography to create a tamper-proof source of randomness that users can verify as fair and unbiased. The proof is published and verified on-chain before any consuming application can use it. For more information, visit the Chainlink documentation.
Root cause: bad RNG Impact: manipulate winner
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.