The refund(uint256 playerIndex) function does not implement the Checks-Effects-Interactions pattern, and as a result is vulnerable to reentrancy, allowing all funds to be drained from the contract.
Once a player has entered the raffle, they can call PuppyRaffle:refund(uint256 playerIndex) to get a refund of their entrance fee. However, the function sends the refund before it updates the status of the player. A malicious contract HackDrainFunds can repeatedly call the PuppyRaffle:refund() function from its HackDrainFunds:receive() function, until all the stored ETH in the raffle contract has been transferred to the malicious contract.
All funds can be drained from the contract by a player who has only entered the raffle once.
See the Foundry/Forge PoC below - note that the HackDrainFunds contract has no withdraw functionality to avoid cluttering the code. The test testDrainContractViaRefund() should be copied into the PuppyRaffleTest contract, and makes use of the PuppyRaffleTest:playersEntered() function.
Foundry/Forge
Apply the Checks-Effects-Interactions pattern to PuppyRaffle:refund() and update the players[playerIndex] value before transferring the entrance fee to the caller.
reentrancy in refund() function
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.