Root + Impact: Missing checks-effects-interactions in refund lets a contract entrant re-enter and withdraw entranceFee repeatedly, draining the whole contract.
A player calls refund(playerIndex) to reclaim their entranceFee; the contract should send the ETH and mark the slot address(0) so it cannot be refunded again. Here the external transfer runs before the slot is zeroed, so the guarding requires still pass during re-entry.
Likelihood:
Occurs the first time any contract-based entrant calls refund on its own index; needs no privilege and no other player's action.
Impact:
The attacker drains the entire contract balance (all other entrants' fees), not just their own.
The attacker contract enters the raffle once, then calls refund on its own index. Because sendValue executes before the slot is zeroed, the ETH transfer invokes the attacker's receive(), which calls refund again with the same index; both require checks still pass, so the call recurses until the contract is empty. Net result: the attacker deposits a single entranceFee and withdraws the entire prize pool.
Following checks-effects-interactions — zeroing players[playerIndex] and emitting the event before the external sendValue — makes the require(playerAddress != address(0)) check fail on re-entry, so the recursive refund reverts. Adding OpenZeppelin's nonReentrant modifier gives the same guarantee as defense in depth.
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.