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

Deterministic value for the `rarity` of the winner's NFT due to bad randomness in `PuppyRaffle::selectWinner`

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.

Vulnerability Details

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:

uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;

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.

Impact

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.

Proof of Concept

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.

Working Test Case

The following test demonstrates that for a given address, the rarity is deterministic and constant:

function test_poc_rarityDependentOnMsgSender() public playersEntered {
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
vm.prank(playerTwo);
puppyRaffle.selectWinner();
uint256 expectedRarity = uint256(keccak256(abi.encodePacked(playerTwo, uint256(0)))) % 100;
if (expectedRarity <= puppyRaffle.COMMON_RARITY()) {
expectedRarity = puppyRaffle.COMMON_RARITY();
} else if (expectedRarity <= puppyRaffle.COMMON_RARITY() + puppyRaffle.RARE_RARITY()) {
expectedRarity = puppyRaffle.RARE_RARITY();
} else {
expectedRarity = puppyRaffle.LEGENDARY_RARITY();
}
uint256 rarity = puppyRaffle.tokenIdToRarity(0);
console.log("expectedRarity: ", expectedRarity);
console.log("rarity: ", rarity);
assertEq(expectedRarity, rarity);
}

Every time the test is run, the rarity is the same, with a value of 70:

$ forge test --mt test_poc_rarityDependentOnMsgSender -vvv
// output
Running 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[PASS] test_poc_rarityDependentOnMsgSender() (gas: 293028)
Logs:
expectedRarity: 70
rarity: 70
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 16.14ms

Recommended Mitigation

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.

Tools Used

Updates

Lead Judging Commences

Hamiltonite Lead Judge almost 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.