Puppy Raffle

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

Smart Contract Winners Cannot Receive NFT Prize, Breaking Raffle

Root + Impact

Root Cause: selectWinner() uses _safeMint() which requires contract recipients to implement onERC721Received(), but this isn't validated during raffle entry.

Impact: If a smart contract wallet without ERC721Receiver interface wins, the entire selectWinner() transaction reverts, blocking raffle completion and locking all funds.

Description

Normal Behavior: When a winner is selected, they should receive both the ETH prize and the NFT.

Issue: The _safeMint() function requires contract recipients to implement onERC721Received(). If a smart contract wallet or multisig wins and doesn't implement this interface, the entire selectWinner() transaction reverts, blocking the raffle.
function selectWinner() external {
// ... winner selection ...
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
// @> This will revert if winner is a contract without onERC721Received
_safeMint(winner, tokenId);
}

Risk

Likelihood:MEDIUM

  • Reason 1 : Many users operate through smart contract wallets (Gnosis Safe, Argent, etc.)

  • Reason 2 : Some older multisig wallets don't implement ERC721Receiver

Impact:

  • Impact 1: Raffle becomes permanently stuck if such a wallet wins

  • Impact 2: No winner can be selected until that wallet is removed

Proof of Concept

ERC-721's _safeMint() function was designed to prevent NFTs from being sent to contracts that can't handle them (which would lock the NFT forever). It does this by calling onERC721Received() on the recipient.

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
// This contract can enter the raffle but cannot receive ERC721 tokens
contract NonERC721Receiver {
PuppyRaffle public puppyRaffle;
constructor(address _puppyRaffle) {
puppyRaffle = PuppyRaffle(_puppyRaffle);
}
function enterRaffle() external payable {
address[] memory players = new address[](1);
players[0] = address(this);
puppyRaffle.enterRaffle{value: msg.value}(players);
}
// Intentionally does NOT implement onERC721Received
// This will cause _safeMint to revert
receive() external payable {}
}

Recommended Mitigation

selectWinner() determines the winner but doesn't transfer the NFT
Winner calls claimPrize() to receive their NFT
+ mapping(address => uint256) public pendingPrizes;
+ mapping(address => uint256) public pendingTokenIds;
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
// ... winner selection logic ...
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
- _safeMint(winner, tokenId);
+ // Store pending prize instead of minting directly
+ pendingTokenIds[winner] = tokenId + 1; // +1 to distinguish from default 0
+ tokenIdToRarity[tokenId] = rarity; // Store rarity
}
+ function claimPrize() external {
+ uint256 tokenId = pendingTokenIds[msg.sender];
+ require(tokenId != 0, "PuppyRaffle: No prize to claim");
+
+ pendingTokenIds[msg.sender] = 0;
+ _safeMint(msg.sender, tokenId - 1); // -1 to get actual tokenId
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 6 days 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!