Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

Refunded Player Slot Can Be Selected as Winner, Blocking Raffle

Scope: src/PuppyRaffle.sol

Root + Impact

When a player refunds, their position is set to address(0). If this player's index is selected as the winner, the selectWinner() function fails because ETH cannot be sent to address(0), permanently blocking the raffle.

Description

  • Normal behavior: The raffle should select an active player as winner and send them the prize.

  • The issue: The selectWinner() function doesn't check if the selected winner address is address(0) (a refunded slot). Attempting to send ETH to address(0) fails, reverting the entire transaction.

function selectWinner() external {
uint256 winnerIndex = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
// @> winner could be address(0) if that player refunded
address winner = players[winnerIndex];
uint256 prizePool = (totalAmountCollected * 80) / 100;
// @> Fails if winner is address(0) - cannot send ETH to zero address
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
}

Risk

Likelihood:

  • Any player can refund before winner selection

  • With weak randomness, attacker can predict if address(0) slot will win

  • Multiple refunds increase the probability

Impact:

  • Raffle becomes permanently stuck - no winner can be selected

  • All remaining players' funds are locked in the contract

  • DoS attack with minimal cost (enter then refund)

Proof of Concept

Explanation: The test enters 4 players, then has playerOne refund (making index 0 = address(0)). It finds a caller that would select index 0 as winner. When that caller calls selectWinner(), the transaction reverts because you cannot send ETH to address(0), proving the raffle is stuck.

function testVuln7_ZeroAddressWinner() public {
address[] memory players = new address[](4);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
// Player refunds - slot becomes address(0)
vm.prank(playerOne);
puppyRaffle.refund(0);
assertEq(puppyRaffle.players(0), address(0));
vm.warp(block.timestamp + duration + 1);
// Find caller that selects index 0
address winningCaller;
for (uint160 i = 1; i < 1000; i++) {
address testCaller = address(i);
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(testCaller, block.timestamp, block.difficulty))
) % 4;
if (winnerIndex == 0) {
winningCaller = testCaller;
break;
}
}
// Selecting address(0) as winner causes revert - raffle stuck
vm.prank(winningCaller);
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
}

Recommended Mitigation

Explanation: Skip address(0) slots when selecting a winner, or maintain a count of active players and require minimum active players instead of total players.

function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
- require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
+ require(activePlayerCount >= 4, "PuppyRaffle: Need at least 4 active players");
+ // Keep selecting until we find a non-zero address
+ address winner;
+ do {
+ uint256 winnerIndex = /* random calculation */;
+ winner = players[winnerIndex];
+ } while (winner == address(0));
+ require(winner != address(0), "PuppyRaffle: No valid winner");
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 5 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] Potential Loss of Funds During Prize Pool Distribution

## Description In the `selectWinner` function, when a player has refunded and their address is replaced with address(0), the prize money may be sent to address(0), resulting in fund loss. ## Vulnerability Details In the `refund` function if a user wants to refund his money then he will be given his money back and his address in the array will be replaced with `address(0)`. So lets say `Alice` entered in the raffle and later decided to refund her money then her address in the `player` array will be replaced with `address(0)`. And lets consider that her index in the array is `7th` so currently there is `address(0)` at `7th index`, so when `selectWinner` function will be called there isn't any kind of check that this 7th index can't be the winner so if this `7th` index will be declared as winner then all the prize will be sent to him which will actually lost as it will be sent to `address(0)` ## Impact Loss of funds if they are sent to address(0), posing a financial risk. ## Recommendations Implement additional checks in the `selectWinner` function to ensure that prize money is not sent to `address(0)`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!