Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: medium
Invalid

Missing upper Bound for `RookPaperScissors::createGameWithEth::_uint256 timeoutInterval`

Summary

The RockPaperScissor::createGameWithEth function lacks an upper bound for the uint256 _timeoutInterval parameter, allowing arbitrarily large values (e.g., 10 years). This could lock games because the RockPaperScissor::Game::revealDeadline may become unreachable, preventing players from resolving the game.

Vulnerability Details

function createGameWithEth(uint256 _totalTurns, uint256 _timeoutInterval) external payable returns (uint256) {
require(msg.value >= minBet, "Bet amount too small");
require(_totalTurns > 0, "Must have at least one turn");
require(_totalTurns % 2 == 1, "Total turns must be odd");
require(_timeoutInterval >= 5 minutes, "Timeout must be at least 5 minutes"); //this line
function commitMove(uint256 _gameId, bytes32 _commitHash) external {
// ... (commit logic)
// If both players have committed, set the reveal deadline
if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
game.revealDeadline = block.timestamp + game.timeoutInterval; //this line
}
}

The createGameWithEth function in the RockPaperScissor contract accepts a _timeoutInterval parameter to set the RockPaperScissor::Game::timeoutInterval variable, which determines the time allowed to play a game. This variable is later used to set the RockPaperScissor::Game::revealDeadline, which specifies the deadline for players to reveal their moves. The revealDeadline is set in the RockPaperScissor::commitMove function when both players have committed their moves.

The createGameWithEth function enforces a minimum _timeoutInterval of 5 minutes (_timeoutInterval >= 5 minutes) but does not impose an upper bound. This allows users to specify excessively large values for _timeoutInterval (e.g., 315,360,000 seconds or 10 years). When timeoutInterval is used to set revealDeadline in commitMove, an excessively large timeoutInterval results in a revealDeadline far in the future, making it practically unreachable.

Relevant GitHub Links

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L100
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L219

Impact

Locked Games: A revealDeadline set far in the future prevents players from revealing moves and resolving the game, locking any staked ETH (e.g., msg.value) until the deadline passes.

User Experience Issues: Accidental large inputs by users could render games unplayable, reducing trust in the contract.

// Here is my proof of code
// Set TIMEOUT to 10 years to demonstrate the vulnerability of a missing upper bound for _timeoutInterval in createGameWithEth. Log timeoutInterval and revealDeadline to show an unreachable deadline.
uint256 constant TIMEOUT = 10 * 365 days; // Equivalent to 10 years (10 * 365 * 24 * 3600 seconds)
// Test committing moves
function testCommitMoves() public {
gameId = createAndJoinGame();
// Player A commits
bytes32 saltA = keccak256(abi.encodePacked("salt for player A"));
bytes32 commitA = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Rock), saltA));
vm.prank(playerA);
vm.expectEmit(true, true, false, true);
emit MoveCommitted(gameId, playerA, 1);
game.commitMove(gameId, commitA);
// Player B commits
bytes32 saltB = keccak256(abi.encodePacked("salt for player B"));
bytes32 commitB = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Paper), saltB));
vm.prank(playerB);
vm.expectEmit(true, true, false, true);
emit MoveCommitted(gameId, playerB, 1);
game.commitMove(gameId, commitB);
// Verify game state
(,,,uint256 timeoutInterval,uint256 revealDeadline,,,,, bytes32 storedCommitA, bytes32 storedCommitB,,,,, RockPaperScissors.GameState state) =
game.games(gameId);
console.log("Timeout interval: ", timeoutInterval);
console.log("Reveal deadline: ", revealDeadline);
assertEq(storedCommitA, commitA);
assertEq(storedCommitB, commitB);
assertEq(uint256(state), uint256(RockPaperScissors.GameState.Committed));
}

Before add an upper bound check

forge test --match-test testCommitMoves -vvv 1
[⠊] Compiling...
[⠑] Compiling 2 files with Solc 0.8.20
[⠘] Solc 0.8.20 finished in 49.72s
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to view
--> test/RockPaperScissorsTest.t.sol:866:5:
|
866 | function testOwnershipFunctions() public {
| ^ (Relevant source part starts here and spans across multiple lines).
Ran 1 test for test/RockPaperScissorsTest.t.sol:RockPaperScissorsTest
[PASS] testCommitMoves() (gas: 349436)
Logs:
Timeout interval: 315360000
Reveal deadline: 315360001
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.09ms (3.34ms CPU time)

After

forge test --match-test testCommitMoves -vvv
[⠊] Compiling...
[⠰] Compiling 2 files with Solc 0.8.20
[⠔] Solc 0.8.20 finished in 39.37s
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to view
--> test/RockPaperScissorsTest.t.sol:866:5:
|
866 | function testOwnershipFunctions() public {
| ^ (Relevant source part starts here and spans across multiple lines).
Ran 1 test for test/RockPaperScissorsTest.t.sol:RockPaperScissorsTest
[FAIL: revert: Timeout must be at least 5 minutes and at most 7 days] testCommitMoves() (gas: 18581)
Traces:
[18581] RockPaperScissorsTest::testCommitMoves()
├─ [0] VM::prank(playerA: [0x23223AC37AC99a1eC831d3B096dFE9ba061571CF])
│ └─ ← [Return]
├─ [954] RockPaperScissors::createGameWithEth{value: 100000000000000000}(3, 315360000 [3.153e8])
│ └─ ← [Revert] revert: Timeout must be at least 5 minutes and at most 7 days
└─ ← [Revert] revert: Timeout must be at least 5 minutes and at most 7 days
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 39.68ms (2.35ms CPU time)
Ran 1 test suite in 321.99ms (39.68ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/RockPaperScissorsTest.t.sol:RockPaperScissorsTest
[FAIL: revert: Timeout must be at least 5 minutes and at most 7 days] testCommitMoves() (gas: 18581)

Relevant Github links

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/test/RockPaperScissorsTest.t.sol#L34
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/test/RockPaperScissorsTest.t.sol#L242

Tools Used

  • Foundry: Used to test the contract and verify the vulnerability by simulating large _timeoutInterval values.

Recommendations

To prevent arbitrarily large _timeoutInterval values, add an upper bound check to the createGameWithEth function. A reasonable maximum, such as 7 days (604,800 seconds), balances flexibility with practicality, ensuring games can be resolved within a manageable timeframe.

- require(_timeoutInterval >= 5 minutes, "Timeout must be at least 5 minutes");
+ require(_timeoutInterval >= 5 minutes && _timeoutInterval <= 7 days, "Timeout must be between 5 minutes and 7 days");
Updates

Appeal created

m3dython Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational

Code suggestions or observations that do not pose a direct security risk.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.