selectWinner() combines an ETH push payment to the winner and an NFT mint into one all-or-nothing transaction. Both steps must succeed or the entire round (including delete players) is rolled back.
If the drawn "winner" is a contract with no receive()/fallback(), the low-level ETH .call fails and require(success) reverts the whole call.
Even if the contract can receive ETH but has not implemented onERC721Received, the ETH push succeeds inside the call frame but the following _safeMint() reverts via OpenZeppelin's ERC721Receiver check - which still unwinds the entire transaction, including the ETH transfer that "already happened" in that same call.
Either way, enterRaffle() performs no validation on the type of address entered, so any player (attacker or otherwise) can seed the players array with a poisoned entrant, and the round becomes permanently stuck whenever that entrant is drawn.
Likelihood:
Reason 1 // Entering a poisoned contract address costs nothing extra beyond the normal entrance fee and requires no special permission - enterRaffle() accepts any address.
Reason 2 // Once a poisoned entrant is in players, the round is stuck any time the deterministic winner-selection formula lands on that index - which, given the separately-reported predictable-randomness issue, an attacker can also actively steer toward.
Impact:
Impact 1 // The round's prize pool becomes stuck and selectWinner() reverts deterministically and repeatedly for every caller, at every later timestamp, as long as the poisoned entrant remains in players.
Impact 2 // Honest players can recover their own entrance fee via refund(), but cannot forcibly evict the poisoned entrant's slot (only that entrant's own address can call refund() on its own slot, and a bare/malicious contract has no incentive or ability to do so) - so the round itself stays stuck even though individual funds are not permanently lost.
Ran with forge test --match-path "test/PoC_6.t.sol" -vv: all 3 tests pass. test_A_EthPushDoS_BricksSelectWinner shows 4 entrants that are plain contracts with no receive() deterministically brick selectWinner() (reverts on two independent attempts, with all state - including the poisoned players array - unchanged). test_B_NftMintDoS_BricksSelectWinnerEvenThoughEthPushWouldSucceed shows a contract that can receive ETH but lacks onERC721Received still bricks the round via the mint step, with the balance proven unchanged (the ETH push was rolled back too). test_C_HonestPlayersCanSelfRescueViaRefund_RoundThenResolvesNormally confirms honest players can refund() their own entrance fee, but cannot evict the poisoned slot themselves.
Decoupling payment/minting from round-resolution (pull over push) means a single incompatible winner address can no longer hold the whole raffle hostage - at worst they simply forfeit their own prize.
## Description If a player submits a smart contract as a player, and if it doesn't implement the `receive()` or `fallback()` function, the call use to send the funds to the winner will fail to execute, compromising the functionality of the protocol. ## Vulnerability Details The vulnerability comes from the way that are programmed smart contracts, if the smart contract doesn't implement a `receive() payable` or `fallback() payable` functions, it is not possible to send ether to the program. ## Impact High - Medium: The protocol won't be able to select a winner but players will be able to withdraw funds with the `refund()` function ## Recommendations Restrict access to the raffle to only EOAs (Externally Owned Accounts), by checking if the passed address in enterRaffle is a smart contract, if it is we revert the transaction. We can easily implement this check into the function because of the Adress library from OppenZeppelin. I'll add this replace `enterRaffle()` with these lines of code: ```solidity function enterRaffle(address[] memory newPlayers) public payable { require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle"); for (uint256 i = 0; i < newPlayers.length; i++) { require(Address.isContract(newPlayers[i]) == false, "The players need to be EOAs"); players.push(newPlayers[i]); } // Check for duplicates for (uint256 i = 0; i < players.length - 1; i++) { for (uint256 j = i + 1; j < players.length; j++) { require(players[i] != players[j], "PuppyRaffle: Duplicate player"); } } emit RaffleEnter(newPlayers); } ```
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.