withdrawWinnings()
Creates Reentrancy RiskThe root cause is the violation of the Checks-Effects-Interactions pattern. The "Interaction" (sending ETH) happens before the "Effect" (updating the pendingWinnings
balance). The @>
markers highlight this incorrect ordering.
The withdrawWinnings()
function does not adhere to the widely accepted Checks-Effects-Interactions (CEI) security pattern. The function transfers ETH to the user via an external .call()
before it updates the user's internal accounting by setting their pendingWinnings
to zero. Although the function is protected by a nonReentrant
modifier, relying solely on a mutex while violating the CEI pattern is considered bad practice and can be risky in complex integrations. Should the nonReentrant
guard ever be removed or contain a flaw, this incorrect ordering would immediately open a classic reentrancy attack vector.
Likelihood:
The presence of the nonReentrant
guard prevents a simple, direct reentrancy attack. However, the underlying pattern is flawed. Exploitation would require a sophisticated scenario that bypasses the mutex, or a future code change where a developer might remove the guard without realizing the severity of the incorrect state update order.
Impact:
If the reentrancy guard were bypassed, a malicious contract acting as a winner could repeatedly call withdrawWinnings()
within a single transaction before its balance is cleared. This would allow the attacker to drain more funds than they are entitled to, potentially stealing the entire contract balance, including the prize pot and accumulated platform fees.
An attacker could deploy a malicious contract that, upon receiving ETH, immediately calls back into the Game
contract to withdraw funds again.
The attacker deploys the Attacker
contract and uses it to win the game.
The attacker calls Attacker.startAttack()
, which in turn calls Game.withdrawWinnings()
.
The Game
contract sends ETH to the Attacker
contract.
The Attacker
contract's receive()
function is triggered, which immediately calls Game.withdrawWinnings()
again.
If the nonReentrant
guard were absent, this second call would succeed because pendingWinnings
for the attacker has not yet been set to 0, allowing for a second withdrawal.
This PoC demonstrates the flawed structure that the nonReentrant
guard is currently mitigating. The robust, long-term solution is to fix the underlying pattern.
Refactor the withdrawWinnings()
function to strictly follow the Checks-Effects-Interactions pattern. The state must be updated (Effect
) before the external call to transfer ETH (Interaction
).
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.