Puppy Raffle

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

[M-02] Smart Contract Winner Can Block Raffle Completion

Root + Impact

Description

  • When a smart contract without a receive() or fallback() function wins the raffle, the ETH transfer fails and the entire selectWinner() transaction reverts.

  • Since the winner is determined by on-chain data that doesn't change between calls, the raffle becomes permanently stuck.

// Root cause in the codebase with @> marks to highlight the relevant section
function selectWinner() external {
// ...
(bool success,) = winner.call{value: prizePool}("");
@> require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}

Risk

Likelihood: Medium

  • Reason 1 // Requires smart contract entry (intentional or accidental)

  • Reason 2 // Some legitimate users may enter via multisigs or smart wallets

Impact: High

  • Impact 1 // Raffle permanently stuck, cannot select winner

  • Impact 2 // All player funds locked in contract

  • Impact 3 // Only resolution is contract redeployment

Proof of Concept

The following test demonstrates how a contract that rejects ETH can permanently block the raffle.

function testSmartContractWinnerBlocksRaffle() public {
NoReceiveContract blocker = new NoReceiveContract();
address[] memory players = new address[](4);
players[0] = address(blocker);
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
vm.warp(block.timestamp + duration + 1);
// Find prevrandao where blocker wins
for (uint256 i = 0; i < 10000; i++) {
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(address(this), block.timestamp, i))
) % 4;
if (winnerIndex == 0) {
vm.prevrandao(bytes32(i));
// Raffle permanently stuck
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
// Subsequent calls also fail with same winner
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
puppyRaffle.selectWinner();
return;
}
}
}
contract NoReceiveContract {
// No receive() or fallback() - cannot accept ETH
}

Recommended Mitigation

Use a pull-over-push pattern where winners claim their prize instead of receiving it automatically.

+ mapping(address => uint256) public pendingWithdrawals;
function selectWinner() external {
// ...
- (bool success,) = winner.call{value: prizePool}("");
- require(success, "PuppyRaffle: Failed to send prize pool to winner");
+ pendingWithdrawals[winner] = prizePool;
_safeMint(winner, tokenId);
}
+ function claimPrize() external {
+ uint256 amount = pendingWithdrawals[msg.sender];
+ require(amount > 0, "No prize to claim");
+ pendingWithdrawals[msg.sender] = 0;
+ (bool success,) = msg.sender.call{value: amount}("");
+ require(success, "Transfer failed");
+ }
Updates

Lead Judging Commences

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