Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Refunded players remain counted and can DoS winner selection

Root + Impact

Description

Refunded players should no longer participate in the raffle and should no longer be counted as funds available for the prize pool.

refund replaces a refunded player's slot with address(0) but does not reduce players.length. Later, selectWinner calculates totalAmountCollected as players.length * entranceFee, treating refunded slots as paid active entries. When enough players refund, the contract calculates a prize pool larger than the ETH it actually holds, causing winner payout to revert.

function refund(uint256 playerIndex) public {
...
payable(msg.sender).sendValue(entranceFee);
// @> The array length is unchanged, leaving a blank slot.
players[playerIndex] = address(0);
emit RaffleRefunded(playerAddress);
}
function selectWinner() external {
...
// @> Refunded slots are still counted as paid entries.
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
...
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
}

Risk

Likelihood:

  • Refunds intentionally leave blank spots in the players array.

  • selectWinner always uses players.length instead of counting active nonzero players or actual contract balance.

Impact:

  • Winner selection can revert because the calculated prize pool exceeds the contract balance.

  • A participant can make the raffle round difficult or impossible to settle after refunds occur.

Proof of Concept

function testRefundedPlayersCanDosSelectWinner() public {
address[] memory players = new address[](4);
players[0] = address(1);
players[1] = address(2);
players[2] = address(3);
players[3] = address(4);
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
for (uint256 i = 0; i < 3; i++) {
vm.prank(players[i]);
puppyRaffle.refund(i);
}
assertEq(address(puppyRaffle).balance, entranceFee);
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
}

After three refunds, players.length is still 4, so selectWinner tries to pay 3.2 ETH even though the contract only has 1 ETH.

Recommended Mitigation

Track active paid entries separately or calculate rewards from actual available ETH. Also ensure address(0) cannot win.

+ uint256 public activePlayerCount;
function enterRaffle(address[] memory newPlayers) public payable {
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle");
for (uint256 i = 0; i < newPlayers.length; i++) {
players.push(newPlayers[i]);
+ activePlayerCount++;
}
...
}
function refund(uint256 playerIndex) public {
...
players[playerIndex] = address(0);
+ activePlayerCount--;
payable(msg.sender).sendValue(entranceFee);
}
function selectWinner() external {
...
- uint256 totalAmountCollected = players.length * entranceFee;
+ uint256 totalAmountCollected = activePlayerCount * entranceFee;
...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!