(HIGH) Denial of Service on Prize/Refund - Prevents players from receiving ETH prizes/refunds by using a malicious contract, potentially leaving funds locked.
The contract sends ETH prizes and refunds to players using low-level call calls with a success check (require(success, "Transfer failed");).
A malicious player can use a smart contract address that is designed to revert whenever it receives ETH. When the game attempts to send the prize or refund to this malicious address, the call will revert, which in turn causes the entire calling function (_finishGame, _handleTie, or _cancelGame) to revert.
This prevents the game from finalizing correctly, potentially locking the ETH bets within the contract indefinitely for that specific game.
The RockPaperScissors contract manages ETH bets. At the end of a game (win, lose, or tie) or upon cancellation, it attempts to send ETH back to the players. This is done using (bool success,) = playerAddress.call{value: amount}("")
; followed by require(success, "Transfer failed");
.
If playerAddress belongs to a contract whose receive()
or fallback()
function reverts when called with value, the success variable will be false, and the require check will cause the entire function (_finishGame, _handleTie, or _cancelGame)
to revert.
In _finishGame
, if the transfer to the winner fails, the game state will not be marked as Finished
, and the prize is not distributed.
In _handleTie
, if either refund transfer fails , the game state will not be marked as Finished, and neither player receives their refund.
In _cancelGame
, if either refund transfer fails, the game state will not be marked as Cancelled, and neither player receives their refund.
File: src/RockPaperScissors.sol
(_finishGame function)
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L467-L505
File: src/RockPaperScissors.sol
(_handleTie function)
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L511-L530
File: src/RockPaperScissors.sol
(_cancelGame function)
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L547-L561
Deploy a simple smart contract that has a receive() or fallback() function which simply reverts.
Use the address of this malicious contract as playerA or playerB when creating or joining an ETH-based game (createGameWithEth or joinGameWithEth).
Participate in the game until it reaches a state where ETH payout/refund is triggered (i.e., the game finishes due to scores or timeout, or is cancelled).
Wait for the contract to attempt to send ETH to the malicious player address. This transaction will revert, causing the game finalization/cancellation logic to fail entirely.
Impact A malicious player (either player A or player B) can grief a game by using a contract address that prevents the final ETH distribution. This leads to:
Locked Funds: The ETH deposited as bets for that game remains trapped in the RockPaperScissors contract.
Game Bricking: The game's state may not transition correctly to Finished or Cancelled if the payout is mandatory within the finalization logic. This particular contract sets the state before the transfer, but the require(success) causes the entire transaction to revert, including the state change. Thus, the state remains stuck in the previous phase (e.g., Committed if timed out reveal, Created if join timed out or manually cancelled before join).
Reduced Protocol Reliability: Users may lose confidence if games get stuck and funds are inaccessible.
Manual code review
AI for impact analysis and report finessing
Change the payment pattern from push (sending ETH to players automatically) to pull (requiring players to call a function to claim their prize/refund).
When a game finishes or is cancelled, instead of sending ETH directly, record the amount owed to each player in a mapping (e.g., mapping(uint256 => mapping(address => uint256)) public pendingPayouts;).
Provide a new function, claimPayout(uint256 _gameId), that players can call.
This function checks pendingPayouts[_gameId][msg.sender], sends the recorded amount to msg.sender using call{value: amount}(""), and sets the pending amount to zero regardless of the call's success. This way, a malicious receiver only prevents themselves from receiving funds and does not break the game finalization logic for the other player or lock funds for everyone.
Malicious player wins a game using a contract that intentionally reverts when receiving ETH, the entire transaction will fail
Malicious player wins a game using a contract that intentionally reverts when receiving ETH, the entire transaction will fail
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.