Root + Impact
The selectWinner function uses msg.sender, block.timestamp, and block.difficulty for random number generation. These values are publicly known or can be influenced by miners/validators, allowing an attacker to predict and manipulate the winner and NFT rarity.
Description
The normal behavior is for the protocol to select a random winner and assign a random rarity to their NFT after the raffle duration expires.
The issue is that the random number is derived from msg.sender (known), block.timestamp (predictable by miners), and block.difficulty (known or influenceable), making the outcome deterministic and predictable.
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 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;
}
}
Risk
Likelihood:
An attacker can call selectWinner at a specific block where they know they will win
Miners/validators can influence block.timestamp and block.difficulty to ensure a specific outcome
The attacker can also enter the raffle multiple times at indices they control to guarantee they are at the winning index
Impact:
Attacker can guarantee themselves a win and potentially a legendary NFT
Unfair raffle defeats the intended purpose of randomness
Economic impact from predetermined winners claiming the prize pool
Proof of Concept
Recommended Mitigation
// Use Chainlink VRF for verifiable randomness
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract PuppyRaffle is ERC721, Ownable, VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
uint64 s_subscriptionId;
uint256 public s_requestId;
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
s_requestId = COORDINATOR.requestRandomWords(
keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
uint256 winnerIndex = randomWords[0] % players.length;
// Use randomWords[1] for rarity selection
// Complete winner selection logic...
}
}