Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

Impossible to win raffle if the winner is a smart contract without a fallback function (selectWinner reverts, funds locked)

Description

The intended behavior is that the selected winner receives the prize pool and the puppy NFT, concluding the raffle.

The specific issue is that selectWinner pays the prize with a low-level call wrapped in require(success). If the winner is a smart contract that has no payable receive/fallback function, the ETH transfer fails, so selectWinner reverts and the raffle can never be concluded.

address winner = players[winnerIndex];
(bool success,) = winner.call{value: prizePool}(""); // @> returns false if winner can't receive ETH
require(success, "PuppyRaffle: Failed to send prize pool to winner"); // @> reverts the whole draw
_safeMint(winner, tokenId);

The winner is chosen from players[], which anyone can enter, including a contract address with no payable fallback. Once such an address is selected, the prize transfer fails and selectWinner reverts; because the winner is fixed by the RNG for that block, the raffle is stuck and the collected funds are locked.

Risk

Likelihood: Medium

  • Any participant can enter as a contract that cannot receive ETH; nothing restricts entrants to EOAs.

Impact: High

  • When such an address wins, selectWinner permanently reverts: no winner is paid, no NFT minted, and all entrants' funds remain locked. A griefer can guarantee this by entering such a contract.

Proof of Concept

// Attacker deploys a contract with NO receive()/fallback() and enters the raffle with it.
// When selectWinner picks that contract as winner: winner.call{value:prizePool}("") returns false,
// require(success) reverts -> selectWinner always reverts -> raffle bricked, funds stuck.

Recommended Mitigation

Use a pull-payment pattern: record the winner's prize and let them withdraw it in a separate call, so a failing transfer to one winner cannot block the draw.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 20 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-03] Impossible to win raffle if the winner is a smart contract without a fallback function

## 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); } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!