Normal behavior: After the raffle duration has elapsed, anyone can call selectWinner() to finalize the round.
The function selects a winner, transfers the prize pool (80% of collected funds) to the winner, mints the NFT, resets the raffle state, and allows the protocol to proceed to the next round.
Issue: The raffle completion logic relies on a push-based ETH transfer to the selected winner and requires this transfer to succeed.
If the winner is a contract that rejects ETH (e.g., by reverting in receive or fallback), the ETH transfer fails and causes selectWinner() to revert entirely.
Because all state changes occur after the ETH transfer, the raffle round cannot be finalized, and the protocol becomes stuck in the same state.
As a result, a single non-payable winner can permanently prevent the raffle from completing, causing a liveness failure of the core protocol functionality.
Likelihood:
The raffle allows contract-based participants, and selectWinner() can be called by any address once the raffle duration has elapsed, making it possible for a non-payable contract to be selected as the winner during normal protocol operation.
The protocol does not provide any alternative completion path or recovery mechanism if the selected winner cannot receive ETH, so a single malicious or incompatible winner is sufficient to block progress.
Impact:
The raffle round cannot be finalized, as selectWinner() will consistently revert when attempting to transfer the prize pool to a non-payable winner.
Core protocol liveness is broken: no winner can be selected, no NFT can be minted, and the raffle cannot progress to the next round, resulting in a permanent denial of service of the primary protocol functionality.
src/RejectETH.sol
PuppyRaffleTest.t.sol
The PoC fixes block inputs to deterministically select a contract that rejects ETH as the winner for demonstration purposes. The vulnerability itself does not rely on controlling randomness, as the raffle’s progress depends on the winner successfully receiving ETH.
An attacker may increase the likelihood of this scenario by registering multiple contract-based participants that intentionally reject ETH, causing the raffle round to become non-finalizable once such a participant is selected.
Avoid push-based ETH transfers during raffle finalization. Instead of requiring the prize payout to succeed within selectWinner(), record the winner and the prize amount, and allow the winner to claim the prize via a separate pull-based withdrawal function.
Alternatively, store the prize in escrow and decouple raffle completion from ETH transfer success, so that a non-payable or reverting winner cannot block protocol progress. This ensures that the raffle round can always be finalized, preserving core protocol liveness regardless of winner behavior.
The fix decouples raffle finalization from ETH transfer success by introducing a pull-based prize withdrawal mechanism, ensuring that a non-payable or reverting winner cannot block protocol progress.
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.