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.