Puppy Raffle

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

Medium 1

Root + Impact

Description

enterRaffle() (line 79) pushes every submitted address into players with no validation against address(0):

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++) {
@> players.push(newPlayers[i]);
}

A zero-address entry is uniquely toxic because it can never be removed: refund() requires playerAddress == msg.sender, and msg.sender can never be address(0). The slot persists for the whole round.

In selectWinner(), if winnerIndex lands on the zero slot, winner is address(0). The prize transfer winner.call{value: prizePool} to address(0) succeeds (burning the ETH), but _safeMint(address(0), tokenId) (line 153) reverts with ERC721: mint to the zero address — rolling back the entire selectWinner() call. Every draw attempt that picks the zero slot fails, delaying the raffle and wasting callers' gas. Combined with the predictable randomness (H-3), the griefing can even be targeted at specific rounds.

Risk

Likelihood: Medium — anyone can plant the zero address for the cost of one entrance fee; no privileged access needed.

Impact: Medium — raffle draws that pick the zero slot revert, griefing the round and burning gas; the entry itself can never be refunded or removed.

Proof of Concept

The Foundry test below demonstrates the griefing:

  1. Four players enter, including address(0) at index 2, paying 4 ETH.

  2. Time warps past the raffle duration.

  3. Timestamps are scanned to find one where the public winner formula (keccak256(caller, timestamp, difficulty) % 4) lands on index 2.

  4. A caller invokes selectWinner() at that timestamp — it reverts on the _safeMint(address(0)) path.

function test_PoC_R3_AddressZeroPlayerBricksRaffle() public {
address[] memory p = new address[](4);
p[0] = playerOne; p[1] = playerTwo; p[2] = address(0); p[3] = playerFour;
puppyRaffle.enterRaffle{value: 4 ether}(p);
assertEq(puppyRaffle.players(2), address(0));
​
// find a timestamp where winnerIndex == 2 (the zero slot)
uint256 base = block.timestamp + duration + 1;
address caller = address(0xCA11);
for (uint256 i = 0; i < 2000; i++) {
vm.warp(base + i);
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(caller, block.timestamp, block.difficulty))) % 4;
if (winnerIndex == 2) break;
}
​
vm.prank(caller);
vm.expectRevert(); // ERC721: mint to the zero address
puppyRaffle.selectWinner();
}

Test assertions and measured result (forge test --match-test test_PoC_R3_AddressZeroPlayerBricksRaffle):

  • address(0) is accepted as a player at index 2,

  • selectWinner() reverts when the zero slot is picked as winner.

Recommended Mitigation

Reject the zero address at entry. With no address(0) ever in the array, every player is a real account that can receive the NFT, so the _safeMint zero-address revert path becomes unreachable and the unrefundable-slot problem disappears.

for (uint256 i = 0; i < newPlayers.length; i++) {
+ require(newPlayers[i] != address(0), "PuppyRaffle: Invalid player");
players.push(newPlayers[i]);
}
Updates

Lead Judging Commences

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