Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

selectWinner() sends the prize to the winner with a low-level call; if the winner is a smart contract without a payable fallback, the transfer reverts and the raffle is permanently stuck

Root + Impact

Description

  • selectWinner() sends the prize pool to the winner address via winner.call{value: prizePool}(""). If winner is a smart contract that does not implement receive() or fallback() payable, the call reverts — and since selectWinner() has no fallback logic, the entire transaction reverts. No winner is selected, no NFT is minted, and the raffle cannot progress.

// src/PuppyRaffle.sol — selectWinner()
(bool success,) = winner.call{value: prizePool}(""); // @> reverts if winner rejects ETH
require(success, "PuppyRaffle: Failed to send prize pool to winner");
// NFT mint and fee accounting only happen after this line — never reached
  • Any player who entered via a smart contract wallet without a receive() function can permanently block winner selection. The ETH and the ability to draw a winner are locked in the contract until all other players call refund().

Risk

Likelihood:

  • Smart contract wallets (multisigs, Gnosis Safe, custom contracts) are common. A player who enters via such a wallet — intentionally or inadvertently — blocks the raffle if selected as winner.

Impact:

  • All legitimate players are denied their winnings. The raffle must be unwound via refund() calls, and the protocol's core function (selecting a winner and minting an NFT) fails permanently for that round.

Proof of Concept

A NoFallback contract enters the raffle. When selected as winner, selectWinner() reverts because the prize transfer fails.

contract NoFallback {
// No receive() or fallback() — rejects ETH
}
function test_smartContractWinnerBlocksRaffle() public {
NoFallback nf = new NoFallback();
address[] memory players = new address[](1);
players[0] = address(nf);
puppyRaffle.enterRaffle{value: entranceFee}(players);
vm.warp(block.timestamp + duration + 1);
// selectWinner sends ETH to nf which has no receive() — reverts
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
// Raffle is permanently stuck — no winner, no NFT, no fee
}

The revert confirms a non-payable contract winner permanently blocks raffle completion.

Recommended Mitigation

Restrict enterRaffle() to EOAs only, or implement a pull-payment pattern so the winner claims their prize rather than having it pushed:

+ function claimPrize() external {
+ require(msg.sender == pendingWinner, "Not the winner");
+ uint256 amount = pendingPrize;
+ pendingWinner = address(0);
+ pendingPrize = 0;
+ (bool success,) = msg.sender.call{value: amount}("");
+ require(success, "Transfer failed");
+ }

With pull-payment, a winner who cannot receive ETH simply never claims — the raffle still completes and the NFT is minted.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 5 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!