The intended behavior is that during prize-pool distribution the winner receives 80% of what the raffle actually collected and the remaining 20% is retained as fees, so the sum distributed never exceeds the funds held.
The specific issue is that selectWinner computes the distributed amounts from players.length, but refund removes a player's ETH from the contract while leaving their slot counted in players.length. The distribution is therefore based on funds that are no longer in the contract, so the prize pool pays out more than was collected and the fee portion becomes unbacked — funds are lost during prize-pool distribution.
Because totalAmountCollected is padded by refunded slots, prizePool can exceed the ETH contributed by active players. The winner is paid from the single contract balance (which also holds accumulated totalFees), so the overpayment drains the fee reserve. The protocol loses funds during distribution, and withdrawFees (which needs balance == totalFees) can no longer succeed.
Likelihood: Medium
refund is a normal user action and always leaves a counted address(0) slot; any raffle with at least one refund distributes an inflated prize.
Impact: High
Loss of funds during prize-pool distribution: the winner is overpaid relative to funds collected, the fee reserve backing totalFees is drained, and fee withdrawal is permanently blocked.
Base the distribution on funds actually held, and remove refunded players (swap-and-pop) so players.length reflects only active players: uint256 totalAmountCollected = address(this).balance;.
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.