Puppy Raffle

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

# Refunded slots inflate the prize calculation, reverting `selectWinner` and stranding the round

Refunded slots inflate the prize calculation, reverting selectWinner and stranding the round

Severity: Medium

Description

  • refund marks a player's slot as address(0) but leaves it in the players array, so players.length does not shrink.

  • selectWinner computes the collected amount and prize from the stale players.length, which still counts refunded slots. The resulting prizePool exceeds the ETH actually held, so the prize transfer fails and the whole call reverts. The winner index can also land on a refunded (address(0)) slot.

address winner = players[winnerIndex]; // may be address(0)
@> uint256 totalAmountCollected = players.length * entranceFee; // counts refunded slots
@> uint256 prizePool = (totalAmountCollected * 80) / 100; // overstated vs real balance
...
(bool success,) = winner.call{value: prizePool}(""); // fails: not enough balance
require(success, "PuppyRaffle: Failed to send prize pool to winner");

Risk

Likelihood:

  • Occurs whenever at least one player refunds before the draw: the array keeps the zeroed slot, so totalAmountCollected overstates the balance and the prize transfer reverts on every selectWinner call.

  • Occurs on the zero-address path as well — when the RNG selects a refunded slot, the winner is address(0).

Impact:

  • selectWinner reverts indefinitely, so the round can never be settled and the collected ETH is stranded in the contract.

  • On the zero-address path the prize is sent to address(0) and _safeMint(address(0), ...) reverts.

Proof of Concept

Save as test/RefundInflatesPoC.t.sol and run forge test --mt testRefundedSlotRevertsSelectWinner. Four players enter (4 ETH), one refunds (balance → 3 ETH, length still 4), and selectWinner computes a 3.2 ETH prize against a 3 ETH balance, reverting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
contract RefundInflatesPoC is Test {
PuppyRaffle puppyRaffle;
uint256 entranceFee = 1e18;
uint256 duration = 1 days;
function setUp() public {
puppyRaffle = new PuppyRaffle(entranceFee, address(99), duration);
}
function testRefundedSlotRevertsSelectWinner() public {
address[] memory players = new address[](4);
players[0] = address(101);
players[1] = address(102);
players[2] = address(103);
players[3] = address(104);
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
// player 4 refunds: balance -> 3 ETH, players.length stays 4 (slot 3 = address(0))
vm.prank(address(104));
puppyRaffle.refund(3);
assertEq(address(puppyRaffle).balance, 3 * entranceFee);
vm.warp(block.timestamp + duration + 1);
// land the draw on an active player (index 0) -> prize (3.2 ETH) > balance (3 ETH)
uint256 ts;
for (uint256 t = block.timestamp; t < block.timestamp + 5000; t++) {
if (uint256(keccak256(abi.encodePacked(address(this), t, block.difficulty))) % 4 == 0) {
ts = t;
break;
}
}
vm.warp(ts);
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
}
}

Recommended Mitigation

Base the payout on the number of active players, not players.length. Track an active count (decremented in refund), compact the array on refund, or disallow refunds once the draw is imminent.

- uint256 totalAmountCollected = players.length * entranceFee;
+ uint256 activePlayers;
+ for (uint256 i = 0; i < players.length; i++) {
+ if (players[i] != address(0)) activePlayers++;
+ }
+ uint256 totalAmountCollected = activePlayers * entranceFee;

Also ensure the winner selection skips address(0) slots (or removes them), so the prize is never sent to the zero address.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 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!