This smart contract implements a secure Rock-Paper-Scissors game in Solidity, ready to be deployed on the Ethereum blockchain. The game follows these steps:
- Two players register and place a bet.
- Each participant picks a move and a password. They send the hash of the concatenated string to the contract which stores it.
- When the two players have committed their moves, they reveal what they have played. To do so, they send their move and password in clear. The contract verifies that the hash of the received input matches the one stored.
- When both player have revealed their move, the contract determines the winner and sends him/her the total betting pool. If there is a draw, each player gets their bet back.
- The game resets and can be played again by new players.
This contract is secure in the sense that a player who has access to the blockchain and its content would still not be able to guess another player's move. Indeed the contract never stores any of the players' move in clear, but only the hash of the move salted with a password only known to the player. Since players cannot change their move during the reveal phase (after they have both committed their choice), this effectively ensures that an opponent could not cheat by looking at transaction data and playing accordingly.
This assumes that the hash function used is both preimage resistant and second-preimage resistant. But for the hash used, SHA256, no one has managed to break pre and second image resistance yet.
- Register with function
register()
. You must send a bet greater than or equal to both the minimumBET_MIN
and to the first player's bet (if defined). - Commit a move with function
play(bytes32 encrMove)
. The format of the expected input is"0xHASH"
whereHASH
is the SHA256 hash of a stringmove-password
.move
is an integer ranging from 1 to 3 which correspond to rock, paper and scissors respectively andpassword
is a string that should be kept secret. - Only when you and your opponent have played, you can reveal your move with
reveal(string memory clearMove)
. The format of the expected input is"move-passord"
. - When both players have revealed their move or when the reveal phase has ended, you can get the result and the reward via
getOutcome()
.
In the screenshots, player A and player B (who initially own 100 ethers each) have bet 10 ethers. Player A played rock while player B chose scissors. At the end of the game, player A won the total contract's balance.
A python script inputs.py
is included to help generate expected inputs from an user's choice of a move and a password. The script works only with Python 3+. To use it, run:
python3 inputs.py
Anyone can register provided that they're not already registered and that their bet is greater than a fixed minimum, currently 1 finney.
If a player has already been registered, a second player wishing to register must place a bet greater than or equal to the bet of that previous player. This is to prevent the strategy of always betting a smaller amount than the opponent to minimize risks while maximizing gains. Of course there are no advantages of betting an amount strictly greater than the initial bet, but one should be free to waste his coins however he wants.
When a player has been registered successfully, he/she can play. As described previously, the player provides a SHA256 hash of the concatenation of a move, represented by an integer, and a secret password. The contract stores this hash and nobody except the player has access to the actual move. Once such a hash has been committed, it cannot be modified.
The reveal phase begins when both player have committed their moves. A player reveals what he had played by sending move-password
. The contract then checks if its indeed the move that was played by hashing it and comparing the result to the stored hash. If they're equal, the first character of the string (move
, an integer) is saved and the contract waits for the second player to reveal its move.
They are two ways for the reveal phase to end: either the two players have revealed their moves or the second player fails to submit its move before the phase ends. Currently, the reveal phase begins as soon as one of the player reveals its move and lasts 10 minutes. This is to prevent a deadlock where the second player realizes he has lost (since he can now see his opponent's move) and does not bother to finish the game.
When the reveal phase ends, any of the player can trigger the function getOutcome()
to make the contract sends the rewards. The winner, if there is any, takes it all. In case of a draw, players get their bet back. A player that has not previously revealed its move has automatically lost.
Just before sending the coins, the contract resets the state of the game. This is done in this order to prevent potential reentrancy attack.
At any time, players have access to public state variables and helper functions to get information about the state of the game.
Functions available are:
getContractBalance()
: to see the current value of the betting pool.whoAmI()
: to see their player's ID: 1 or 2 if they are indeed registered or 0 if they're not.bothPlayed()
: returnstrue
if both players have played and that they're now able to go to the reveal phase.bothRevealed()
: returnstrue
if both players have revealed their move and that the contract is ready to send out the rewards.revealTimeLeft()
: returnsREVEAL_TIMEOUT
if the timer of the reveal phase has not started yet, returns the time left otherwise
Public state variables are:
BET_MIN
: the minimal amount to be sent to the contract in order to register. Currently set to 1 finney.REVEAL_TIMEOUT
: the duration of the reveal phase in seconds. Currently set to 10 minutes (600 seconds).initial_bet
: the bet of the first player registered if there is one. The second player must place a bet greater than or equal to that amount.