Using the deterministic inputs for the random number; block.difficulty
and msg.sender
, creates a deterministic rarity
value for the winner's NFT when calling PuppyRaffle::selectWinner
. The value of block.difficulty
is always 0
since the migration from proof-of-work (PoW) to proof-of-stake (PoS) and msg.sender
can be modified by the caller to create the desired NFT rarity. Therefore, rarity
is not random, but deterministic and constant for a given msg.sender
value.
Since the migration from PoW to PoS, the value of block.difficulty
is always 0
(see Etherscan to see a graph of block.difficulty
wrt time). The rarity
of the NFT that is awarded to the winner
of the raffle is determined using the following calculation in the PuppyRaffle
contract on line 139:
Since the block.difficulty
is constant at 0
, the only parameter that varies is msg.sender
. Since an attacker can control the value of msg.sender
, the value of rarity
can be manipulated and is pre-determined and constant based on the value of msg.sender
.
As the msg.sender
is the only variable that impacts which NFT the winner receives, the caller of selectWinner()
can control which NFT the winner gets. The value for rarity
is therefore not random and subject to manipulation.
A malicious attacker could call selectWinner()
, and by controlling the address use, ensure that the winner receives the lowest rarity of NFT.
A prospective winner could call selectWinner()
and again by controlling the address they use, ensure that they receive the highest rarity of NFT.
Since the caller can control who wins the lottery (see issue: "Deterministic, bad randomness in PuppyRaffle::selectWinner
when determining the winner of the raffle"), an attacker can execute the function only when they know they are the winner
and ensure that they are the receiver of the highest rarity NFT.
To calculate which address would be needed to receive a maximum rarity NFT, an attacker can run a script that generates addresses using CREATE2
.
When the script finds the required address, the address can be deployed using the generated salt
. This address can then be used to call selectWinner()
which will send an NFT with the desired rarity to the winner
.
The attacker could modify a script e.g. https://github.com/0age/create2crunch but change the stopping condition to be when the desired rarity
is achieved.
To ensure that the msg.sender
is the winner, given that winner
is dependent on the msg.sender
, block.number
and the block.difficulty
and the msg.sender
is now a constant, the attacker can simulate the transaction and wait to execute until the block.number
dictates that they are the winner of the NFT.
The following test demonstrates that for a given address, the rarity
is deterministic and constant:
Every time the test is run, the rarity
is the same, with a value of 70
:
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.