Puppy Raffle

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

`selectWinner()` reverts if winner is a smart contract without `receive()` or `fallback()` function

Description

  • Normal: The protocol should handle the case where the winner is a smart contract that cannot receive ETH.

  • Bug: selectWinner() sends ETH to the winner via winner.call{value: prizePool}(""). If the winner is a smart contract that doesn't implement a payable receive() or fallback() function, the call silently fails (returns false), and the subsequent require(success) reverts the entire transaction.

// src/PuppyRaffle.sol:151-152
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");

Risk

Likelihood:

  • Smart contracts participate in protocols routinely (multisigs, DAOs, aggregators)

  • Many contracts intentionally omit receive() to prevent accidental ETH receipts

  • No EOA check is performed in enterRaffle()

Impact:

  • selectWinner() permanently reverts if a contract without fallback wins

  • Raffle is bricked — no winner can be selected, no new raffle starts

  • Players can still call refund() to recover funds, but the protocol is effectively dead

  • Core functionality (selecting a winner) is completely blocked

Proof of Concept

contract NoFallbackContract {
// No receive(), no fallback() — cannot receive ETH
function enterRaffle(PuppyRaffle raffle) external payable {
address[] memory players = new address[](1);
players[0] = address(this);
raffle.enterRaffle{value: msg.value}(players);
}
}

If NoFallbackContract is selected as winner, winner.call{value: prizePool}("") returns false, and the require reverts. The raffle cannot proceed.

Recommended Mitigation

- (bool success,) = winner.call{value: prizePool}("");
- require(success, "PuppyRaffle: Failed to send prize pool to winner");
+ // Use pull-over-push: let winner claim their prize
+ // OR restrict entrants to EOAs:
+ require(tx.origin == msg.sender, "Contracts not allowed");
+ // Better: use a pull pattern
+ mapping(address => uint256) public pendingWithdrawals;
+
+ function selectWinner() external {
+ // ... select winner ...
+ pendingWithdrawals[winner] += prizePool;
+ _safeMint(winner, tokenId);
+ }
+
+ function claimPrize() external {
+ uint256 amount = pendingWithdrawals[msg.sender];
+ pendingWithdrawals[msg.sender] = 0;
+ (bool success,) = msg.sender.call{value: amount}("");
+ require(success, "Failed to send");
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 36 minutes 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!