Puppy Raffle

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

Push Payment in `PuppyRaffle::selectWinner` Leads to Permanent Denial of Service if Winner Rejects Ether

[M-01] Push Payment in PuppyRaffle::selectWinner Leads to Permanent Denial of Service if Winner Rejects Ether

Description

  • When the raffle completes, selectWinner() determines the winner and automatically pushes 80% of the total prize pool to the winning address.

  • The payout is performed via winner.call{value: prizePool}("") followed by require(success). When the selected winner is a contract that lacks a payable receive/fallback function or intentionally reverts upon incoming value transfers, selectWinner() reverts, freezing the raffle.

function selectWinner() external {
...
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);
}

Risk

Likelihood:

  • Smart contract accounts (e.g. multisigs, proxy wallets without fallback, or malicious griefing contracts) enter the raffle.

  • The pseudo-random selection algorithm picks the non-payable contract address as the winner.

Impact:

  • Permanent Denial of Service preventing the completion of the current raffle round.

  • Entire prize pool and protocol fees remain locked in the contract with no ability to restart or re-draw.

Proof of Concept

contract RevertingEntrant {
PuppyRaffle raffle;
constructor(PuppyRaffle _raffle) {
raffle = _raffle;
}
function enter() external payable {
address[] memory p = new address[](1);
p[0] = address(this);
raffle.enterRaffle{value: msg.value}(p);
}
// Reverts on any incoming ETH
receive() external payable {
revert("Rejecting ETH");
}
}
function test_dosWhenWinnerRejectsEth() public {
RevertingEntrant revertingContract = new RevertingEntrant(puppyRaffle);
address[] memory players = new address[](3);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
puppyRaffle.enterRaffle{value: entranceFee * 3}(players);
vm.deal(address(revertingContract), 1 ether);
revertingContract.enter{value: 1 ether}();
vm.warp(block.timestamp + duration + 1);
// If revertingContract is selected, selectWinner reverts
// vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
// puppyRaffle.selectWinner();
}

Recommended Mitigation

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

Updates

Lead Judging Commences

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