The joinGameWithEth and joinGameWithToken functions lack a check to ensure game.playerB == address(0). This omission allows the playerB slot to be overwritten under certain conditions, introducing a potential for front-running, griefing, or participant replacement.
Although the game.state == GameState.Created check is enforced, there is no explicit safeguard to prevent overwriting of playerB. This opens up critical edge cases:
A user joins a game, becoming playerB, but before any action is taken, another transaction front-runs and replaces them by calling joinGame... again.
The overwritten user may have already committed a move or was about to win, leading to an unfair replacement.
This replacement invalidates the original playerB’s participation and undermines the fairness of the protocol.
PoC Scenario:
1. Game is created with ETH.
2. Legitimate user B sends a joinGameWithEth() tx.
3. Attacker front-runs or races the tx and also calls joinGameWithEth().
4. Due to missing game.playerB == address(0) check, attacker overwrites playerB.
5. Game logic now operates under a false assumption about who the actual second player is.
This results in:
Critical integrity failure of the game's player structure.
Potential financial and strategic loss to the first user.
Exploitable front-running vector with mempool monitoring tools.
Manuel review
Immediately include this check in both joinGameWithEth and joinGameWithToken:
require(game.playerB == address(0), "Game already joined");
This enforces single-assignment of playerB and mitigates overwrite attacks, ensuring fairness and consistency in game execution.
Game state remains Created after a player joins
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.