From 934c8c9f626be0b3396c191c40a955688f22bffb Mon Sep 17 00:00:00 2001 From: Jimmy Chu <898091+jimmychu0807@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:58:42 +0800 Subject: [PATCH] Completed the smart contract side (#5) * updated * Completed the smart contract side --- apps/contracts/contracts/GuessingGame.sol | 104 ++++++++++++++---- apps/contracts/contracts/base/Constants.sol | 2 +- .../contracts/interfaces/IGuessingGame.sol | 12 +- apps/contracts/hardhat.config.ts | 20 +++- 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/apps/contracts/contracts/GuessingGame.sol b/apps/contracts/contracts/GuessingGame.sol index cf984af..d3fa7d0 100644 --- a/apps/contracts/contracts/GuessingGame.sol +++ b/apps/contracts/contracts/GuessingGame.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IGuessingGame} from "./interfaces/IGuessingGame.sol"; -import {MIN_NUM, MAX_NUM} from "./base/Constants.sol"; +import {MIN_NUM, MAX_NUM, ROUND_TO_WIN} from "./base/Constants.sol"; contract GuessingGame is IGuessingGame, Ownable { Game[] public games; @@ -30,18 +30,10 @@ contract GuessingGame is IGuessingGame, Ownable { _; } - modifier gameStateIn2(uint32 gameId, GameState[2] memory gss) { - Game storage game = games[gameId]; - if (game.state != gss[0] && game.state != gss[1]) { - revert GuessingGame__UnexpectedGameState(game.state); - } - _; - } - modifier oneOfPlayers(uint32 gameId) { Game storage game = games[gameId]; bool found = false; - for (uint8 i = 0; i < game.players.length; i++) { + for (uint8 i = 0; i < game.players.length; ++i) { if (game.players[i] == msg.sender) { found = true; break; @@ -70,6 +62,13 @@ contract GuessingGame is IGuessingGame, Ownable { _; } + modifier BidInRange(uint8 bid) { + if (bid < MIN_NUM || bid > MAX_NUM) { + revert GuessingGame__BidOutOfRange(msg.sender, bid); + } + _; + } + // Helper functions function _updateGameState(uint32 gameId, GameState state) internal validGameId(gameId) nonEndState(gameId) { Game storage game = games[gameId]; @@ -95,12 +94,10 @@ contract GuessingGame is IGuessingGame, Ownable { emit NewGame(gameId, msg.sender); } - // IMPROVE: the gameStateIn() modifier code is bad. It is restricted to take - // two params. function joinGame(uint32 gameId) external override validGameId(gameId) gameStateEq(gameId, GameState.GameInitiated) { Game storage game = games[gameId]; // check the player has not been added to the game - for (uint8 i = 0; i < game.players.length; i++) { + for (uint8 i = 0; i < game.players.length; ++i) { if (game.players[i] == msg.sender) { revert GuessingGame__PlayerAlreadyJoin(msg.sender); } @@ -110,9 +107,9 @@ contract GuessingGame is IGuessingGame, Ownable { emit PlayerJoinGame(gameId, msg.sender); } - function startRound( + function startGame( uint32 gameId - ) external override validGameId(gameId) byGameHost(gameId) gameStateIn2(gameId, [GameState.GameInitiated, GameState.RoundEnd]) { + ) external override validGameId(gameId) byGameHost(gameId) gameStateEq(gameId, GameState.GameInitiated) { _updateGameState(gameId, GameState.RoundBid); emit GameStarted(gameId); } @@ -130,7 +127,7 @@ contract GuessingGame is IGuessingGame, Ownable { // If all players have submitted bid, update game state bool notYetBid = false; - for (uint i = 0; i < game.players.length; i++) { + for (uint i = 0; i < game.players.length; ++i) { address p = game.players[i]; if (game.bids[round][p].bid_null_hash == bytes32(0)) { notYetBid = true; @@ -143,11 +140,80 @@ contract GuessingGame is IGuessingGame, Ownable { } } - function revealBid() { + function _verifyBidProof(bytes32 proof, uint8 bid, uint256 nullifier) internal pure returns (bool) { + /** + * TODO: verify proof + **/ + proof; + bid; + nullifier; + return true; + } + + function revealBid( + uint32 gameId, + bytes32 proof, + uint8 bid, + uint256 nullifier + ) + external + override + validGameId(gameId) + oneOfPlayers(gameId) + gameStateEq(gameId, GameState.RoundReveal) + BidInRange(bid) + { + Game storage game = games[gameId]; + // each player reveal a bid. The last player that reveal a bid will change the game state + bool proofVerified = _verifyBidProof(proof, bid, nullifier); + if (!proofVerified) { + revert GuessingGame__BidProofRejected(msg.sender, gameId, game.currentRound); + } + + uint8 round = game.currentRound; + game.revelations[round][msg.sender] = bid; + emit BidRevealed(gameId, round, msg.sender); + + // If all players have submitted revelation, update game state + bool notYetReveal = false; + for (uint i = 0; i < game.players.length; ++i) { + address p = game.players[i]; + if (game.revelations[round][p] == 0) { + notYetReveal = true; + break; + } + } + + if (!notYetReveal) { + _updateGameState(gameId, GameState.RoundEnd); + } } - function endRound() { - // the average will be cmoputed, the winner will be determined. Update the game state. + function endRound( + uint32 gameId + ) external override validGameId(gameId) byGameHost(gameId) gameStateEq(gameId, GameState.RoundEnd) { + Game storage game = games[gameId]; + + /** + * TODO: calc the average of all bids, determine the winnder + **/ + + // Assume the game host is winner for now + address roundWinner = game.players[0]; + + // Notice we also update the game.currentRound here + uint8 round = game.currentRound++; + ++game.roundWon[roundWinner]; + + // update the game.state or end the game + if (game.roundWon[roundWinner] == ROUND_TO_WIN) { + game.finalWinner = roundWinner; + emit GameWinner(gameId, roundWinner); + _updateGameState(gameId, GameState.GameEnd); + } else { + emit RoundWinner(gameId, round, roundWinner); + _updateGameState(gameId, GameState.RoundBid); + } } } diff --git a/apps/contracts/contracts/base/Constants.sol b/apps/contracts/contracts/base/Constants.sol index 2970edf..5d29093 100644 --- a/apps/contracts/contracts/base/Constants.sol +++ b/apps/contracts/contracts/base/Constants.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -uint8 constant MIN_NUM = 0; +uint8 constant MIN_NUM = 1; uint8 constant MAX_NUM = 100; uint8 constant ROUND_TO_WIN = 5; diff --git a/apps/contracts/contracts/interfaces/IGuessingGame.sol b/apps/contracts/contracts/interfaces/IGuessingGame.sol index 054497f..9728828 100644 --- a/apps/contracts/contracts/interfaces/IGuessingGame.sol +++ b/apps/contracts/contracts/interfaces/IGuessingGame.sol @@ -10,11 +10,12 @@ interface IGuessingGame { struct Game { // game players. The first player is the game host address[] players; - mapping(address => uint8[]) winning; + mapping(address => uint8) roundWon; uint8 currentRound; GameState state; // player bid list mapping(uint8 => mapping(address => Bid)) bids; + mapping(uint8 => mapping(address => uint8)) revelations; address finalWinner; uint256 startTime; uint256 lastUpdate; @@ -37,6 +38,8 @@ interface IGuessingGame { error GuessingGame__PlayerAlreadyJoin(address p); error GuessingGame__SenderIsNotGameHost(); error GuessingGame__SenderNotOneOfPlayers(); + error GuessingGame__BidProofRejected(address, uint32, uint8); + error GuessingGame__BidOutOfRange(address, uint8); // Emitted Events event NewGame(uint32 indexed gameId, address indexed sender); @@ -44,10 +47,15 @@ interface IGuessingGame { event GameStarted(uint32 gameId); event GameStateUpdated(uint32 gameId, GameState state); event BidSubmitted(uint32 gameId, uint8 round, address sender); + event BidRevealed(uint32 gameId, uint8 round, address sender); + event RoundWinner(uint32 gameId, uint8 round, address winner); + event GameWinner(uint32 gameId, address winner); // External Functions function newGame() external returns (uint32 gameId); function joinGame(uint32 gameId) external; - function startRound(uint32 gameId) external; + function startGame(uint32 gameId) external; function submitBid(uint32 gameId, bytes32 bid_nullifier_hash, bytes32 nullifier_hash) external; + function revealBid(uint32 gameId, bytes32 proof, uint8 bid, uint256 nullifier) external; + function endRound(uint32 gameId) external; } diff --git a/apps/contracts/hardhat.config.ts b/apps/contracts/hardhat.config.ts index cfb11c7..d684368 100644 --- a/apps/contracts/hardhat.config.ts +++ b/apps/contracts/hardhat.config.ts @@ -52,7 +52,25 @@ function getNetworks(): NetworksUserConfig { } const hardhatConfig: HardhatUserConfig = { - solidity: "0.8.23", + solidity: { + version: "0.8.23", + settings: { + // We hit the "Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent + // `viaIR: true` (standard JSON) while enabling the optimizer. Otherwise, try removing + // local variables." problem. So we enable it. + // ref: https://hardhat.org/hardhat-runner/docs/reference/solidity-support#support-for-ir-based-codegen + viaIR: true + // optimizer: { + // enabled: true, + // runs: 77, + // details: { + // yulDetails: { + // optimizerSteps: "u", + // }, + // }, + // }, + } + }, defaultNetwork: process.env.DEFAULT_NETWORK || "localhost", networks: { hardhat: {