refund() blanks the player's slot (players[playerIndex] = address(0), line 103) but never shrinks the players array. selectWinner() then trusts the stale players.length for both winner selection and prize accounting (lines 128-136):
This creates two failure modes after a single honest refund:
There is no recovery: withdrawFees() (line 158) requires address(this).balance == totalFees, which no longer holds after a refund, so the leftover ETH is locked forever. No privileged rescue function exists.
Likelihood: High — a single honest refund by any player triggers it; no attacker skill or special conditions are required.
Impact: High — one refund permanently bricks the raffle and locks all remaining player funds with no recovery path.
The Foundry test below demonstrates the full bricking flow:
Test assertions and measured result (forge test --match-test test_PoC_H2_RefundBricksSelectWinner):
Track the true active-player count (or swap-and-pop the refunded slot) and use it — not players.length — in selectWinner() accounting, while ensuring address(0) can never be selected as winner. With accounting matching actual funds, the prize pool can never exceed the balance, and with no blank slots the _safeMint revert path disappears — selectWinner() can always complete and withdrawFees()' balance check holds.
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.