refund does not follow the recomended Check-Effect-Interaction pattern to protect functions against reentrancy attacks.
A hacker can create an attacker smart contract, with a malicious fallback function, to re-renter refund as many times as necessary to drain the balance of the raffle. This attack is possible because the players array is updated after transferring the funds and not before.
This is the detailed sequence of the attack:
Hacker deploys an attacker contract
Hacker execute stealFunds function to enter the raffle with the contract as a player and immediately calls refund
refund sends back the funds to the attacker, by making an external call, PuppyRaffle cedes control of the transaction to the attacker
refund cannot update the players array because the fallback in the attacker has not finished its execution
The balance in the raffle is still greater than zero, so it will trigger a new call to refund until the balance is reduced to zero
Balance goes to zero, the fallback finishes its executio and now refund can update the players array
Let's perform an attack to validate the explanation from above.
Create a Reentrant.sol file and paste in it the following contract:
Import Reentrant and then paste the following test in PuppyRaffleTest.t.sol:
Contract drained resulting in complete lost of funds.
VS Code and Foundry.
Update the players array before transferring the funds. This minimal change will protect the function against reentrancy attack and inherit ReentrancyGuard.sol from OpenZeppelin to implement the nonReentrant modifier on all external functions that handle transfer of Ether or make external calls.
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.