Puppy Raffle

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

selectWinner() uses _safeMint, so if the randomly selected winner is a contract that does not implement onERC721Received, the entire draw reverts and the raffle cannot conclude

Root + Impact

Description

  • selectWinner() mints the puppy NFT to the winner using _safeMint, which calls onERC721Received on contract recipients and reverts if it is absent or returns the wrong value.

  • Because the winner is chosen pseudo-randomly from all players, a participating contract that does not implement the ERC721 receiver hook can be selected. When that happens, _safeMint reverts, reverting the whole selectWinner() transaction. Since this is the only way to conclude the round, the raffle becomes stuck.

address winner = players[winnerIndex];
...
@> _safeMint(winner, tokenId); // reverts if winner is a non-receiver contract

Risk

Likelihood:

  • Occurs when a participating contract address that lacks onERC721Received is selected as winner; contract entrants are allowed and selection is uniform, so the probability scales with how many such contracts enter.

Impact:

  • The round cannot be finalized: no winner is paid, no fees are bankable, and there is no separate refund path once the raffle period has ended; funds are stranded.

Proof of Concept

The test below enters a contract with no onERC721Received hook as a player; when the pseudo-random selection lands on that address, _safeMint reverts and selectWinner() reverts with it, leaving the round unable to conclude. The commented loop shows how to brute-force msg.sender/block.timestamp until index 0 is selected, then assert the revert.

contract NonReceiver {} // no onERC721Received
function test_safeMint_to_non_receiver_stalls_draw() public {
NonReceiver bad = new NonReceiver();
address[] memory players = new address[](4);
players[0] = address(bad);
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
// if the RNG selects index 0 (the NonReceiver), selectWinner reverts.
// brute-force msg.sender / warp in a loop to land on it, then:
// vm.expectRevert();
// puppyRaffle.selectWinner();
}

Recommended Mitigation

Either mint with _mint (accepting the recipient's responsibility) or decouple prize delivery from winner selection via a pull-based claim. (Note: a pull pattern (winner calls claimPrize() separately) is the more robust fix, as it also avoids the ETH-transfer failure path stalling the draw).

- _safeMint(winner, tokenId);
+ _mint(winner, tokenId);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 22 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!