refund() blanks the player's slot (players[playerIndex] = address(0)) but selectWinner() computes the pot from players.length, which still counts refunded slots as paying players. The prize pool and the fee are therefore inflated by players who already took their money back.
Likelihood: High — refunding is a documented feature, so this triggers in normal use; no attacker is needed.
Impact: High — the raffle breaks in two ways. First, selectWinner() reverts when refunded players are more than 25% of active players: the prize pool (80% of players.length) exceeds the real balance, so the winner payment fails. In a minimum 4-player raffle, a single refund bricks winner selection (prizePool 3.2 ETH vs 3 ETH balance). If the winner index lands on a refunded (address(0)) slot, the prize is sent to address(0) and _safeMint reverts. Second, withdrawFees() is permanently broken after ANY refund: the fee is inflated by refunded players, so totalFees never equals the real balance and the balance check can never pass — protocol fees stuck forever.
The first test shows one refund bricking winner selection in a 4-player raffle: after the refund the balance is 3 ETH but the prize pool is 3.2 ETH, so selectWinner() reverts with "Failed to send prize pool to winner". The second test shows the fee side: 8 players, 1 refund, selectWinner() succeeds, but the balance is 0.6 ETH vs totalFees 1.6 ETH, so withdrawFees() reverts forever.
Track the number of active players (or the real collected amount) separately from players.length, and compute the prize and fee from active players only. Alternatively, remove refunded players from the array (swap-and-pop) instead of leaving address(0) holes.
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.