Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

withdrawWinnings() Does Not Follow the Checks-Effects-Interactions (CEI) Pattern, Leading to Potential Re-Entrancy Risk

withdrawWinnings() Does Not Follow the Checks-Effects-Interactions (CEI) Pattern, Leading to Potential Re-Entrancy Risk

Description

  • In WithdrawWinning() ,the line that resets pendingWinnings[msg.sender] to 0 is placed after the external call.

  • This violates the Checks-Effects-Interactions (CEI) pattern and introduces a potential re-entrancy vulnerability. If a malicious contract receives ETH via the low-level call, it could re-enter the withdrawWinnings() function before its pendingWinnings is cleared.

  • As a result, the attacker can bypass the amount > 0 check and repeatedly drain the contract’s balance.

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

Likelihood:High

  • The function was calling the external call to another address,when the recipient is a contract, the fallback or receive function is triggered, allowing it to perform arbitrary logic, including re-entrant calls.

Impact:High

  • Since the require(amount > 0) is useless , the attacker can repeatedly call the withdrawWinnings() till the balance of the Game contract is drained. so the attacker is successfully to steal all the ether which does not belong to him.

  • Note: While nonReentrant provides protection against direct re-entrancy in the current context, relying solely on modifiers is not always sufficient.

    For example, if future changes introduce internal calls, delegatecalls, or access through another entry point, the low-level call may still become a re-entrancy vector.

    In addition, the fallback or receive function of the recipient may contain side effects that interfere with contract logic or gas usage, even without re-entrancy.

Proof of Concept

A malicious contract can exploit the low-level call as follows:

//fallback function in malicious contract
function fallback() external {
address GameContractAddress = 0x....aaa;
// Re-enter the Game contract
address(GameContractAddress).withdrawWinnings();
}

In this example, when the Game contract sends ETH to the malicious contract, it triggers the fallback function, which re-enters the vulnerable withdrawWinnings() logic before the previous withdrawal finalizes.And while the pendingWinnings[msg.sender] was not be set to zero, so it can still bypass the amount > 0 check until the contract’s entire balance is drained.

Recommended Mitigation

  • Following the Checks-Effects-Interactions (CEI) pattern will protect contracts from re-entrancy issues.

  • Placed the line that resets the variable pendingWinner before the external call.

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);
}
Updates

Appeal created

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Game::withdrawWinnings reentrancy

We only audit the current code in scope. We cannot make speculation with respect to how this codebase will evolve in the future. For now there is a nonReentrant modifier which mitigates any reentrancy. CEI is a good practice, but it's not mandatory. Informational

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!