Root Cause: Effect (state update) occurs after Interaction (external call) -> Impact: Breaks Checks-Effects-Interactions pattern, risks reentrancy if nonReentrant is ever removed or altered, and conflicts with industry best practices - reminiscent of The DAO hack.
The withdrawWinnings() function in Game.sol transfers user funds before zeroing out their pendingWinnings balance. Although it employs the nonReentrant modifier, performing the external call prior to the state update:
Violates the Checks-Effects-Interactions (CEI) pattern, a cornerstone of smart-contract security.
Creates a fragile ordering: if nonReentrant is modified, disabled, or bypassed (e.g., in an inherited override), reentrancy becomes possible.
Can lead to fund lockup or state inconsistency if the call succeeds but the subsequent state update were to fail (e.g., via out-of-gas or unexpected revert).
This anti-pattern directly contributed to the infamous The DAO exploit (2016), where external calls before clearing balances allowed repeated withdrawals. Even with nonReentrant today, adhering to CEI is essential for defense-in-depth.
withdrawWinnings:
Check: Verifies amount > 0. ✅
Interaction: Sends ETH to msg.sender via .call{value: amount}(). ❌
Effect: Only then clears pendingWinnings[msg.sender]. ❌
Likelihood: High
If the nonReentrant guard is ever removed or circumvented (e.g., via inheritance), a malicious contract could reenter withdrawWinnings() during the external call, repeatedly draining funds. Even without reentrancy, the ordering violates secure-by-default principles and obscures auditing.
Impact: None (Reentrancy Guard is employed)
Potential Reentrancy: If nonReentrant is altered, attackers can drain all winnings in a single transaction.
Funds Lockup: In edge cases (out-of-gas after call succeeds), the state update might never execute, leaving user balances non-zero but ETH already sent.
Audit Difficulty: Violating CEI obscures transaction flow, making it harder to reason about safe state transitions.
Historical Precedent: The DAO hack (2016) exploited this exact pattern, stealing >3.6 million ETH (∼$60 M) by recursive withdraws before balance updates.
Severity: Informational (but critical to fix)
Tools Used:
Foundry Test Suite
Chat-GPT AI Assistance (Report Grammar Check & Improvements)
Manual Review
Scenario:
Malicious Recipient Contract deploys a fallback that calls withdrawWinnings() again.
When Game.withdrawWinnings() sends ETH, control transfers to the malicious fallback.
Even though nonReentrant prevents immediate reentry, any future modification (removing the modifier, or a bug in the guard) could reopen the exploit.
The DAO parallel:
The DAO’s withdrawRewardFor() did external transfers before zeroing the user’s balance, enabling recursive calls that drained ~$60M in ETH.
Adhere to CEI: Move the state update before the external call:
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
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.