Game::withdrawWinnings function does not follow CEI method
Description
function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
@> (bool success, ) = payable(msg.sender).call{value: amount}("");
@> require(success, "Game: Failed to withdraw winnings.");
@> pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}
Risk
Proof of Concept
Run the command forge test --mt test_reentrancy_protection_works_but_CEI_violated -vvvv
to check the test result
The test is created just to proove that, function implements nonReentrant modifier
still does not follow CEI method. Reentrancy is still possible which will eventually fail.
contract MaliciousReentrancy {
Game public game;
uint256 public attackCount;
constructor(Game _game) {
game = _game;
}
receive() external payable {
attackCount++;
if (attackCount < 3 && game.pendingWinnings(address(this)) > 0) {
game.withdrawWinnings();
}
}
function attack() external {
game.withdrawWinnings();
}
}
function test_reentrancy_protection_works_but_CEI_violated() public {
MaliciousReentrancy malicious = new MaliciousReentrancy(game);
vm.deal(address(malicious), 10 ether);
vm.startPrank(address(malicious));
game.claimThrone{value: 2 ether}();
vm.stopPrank();
uint256 graceTime = game.gracePeriod();
vm.warp(block.timestamp + graceTime + 1);
vm.startPrank(user2);
game.declareWinner();
vm.stopPrank();
uint256 winnings = game.pendingWinnings(address(malicious));
assert(winnings > 0);
vm.startPrank(address(malicious));
vm.expectRevert("Game: Failed to withdraw winnings.");
malicious.attack();
vm.stopPrank();
}
Recommended Mitigation
function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
+ pendingWinnings[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Game: Failed to withdraw winnings.");
- pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}