A confidential Rock Paper Scissors decentralized application using a commit-reveal scheme for move confidentiality. Players commit their moves as hashes, then reveal within a 2-minute window after both players have committed.
Deployed Sepolia Contract: 0xDa929CFa4E076d9928674Ba4a3adf5E02E71f64C
- Confidential Moves: Uses commit-reveal scheme - players can see commitments but not actual moves until reveal
- Stake-based Gameplay: Each game requires 0.001 ETH stake from both players
- Automatic Resolution: Winner receives full stake (0.002 ETH total)
- Tie Resolution: Players can choose rematch or split funds
- Game Discovery: Games discoverable via on-chain events
- Time Limits: 2-minute reveal phase, 2-minute tie resolution phase
- Foundry installed
- Node.js 18+ and Yarn
- Git installed
- For testnet tests: Sepolia ETH in your wallet
- Clone the repository:
git clone https://github.com/LyonSsS/rps_tests.git
cd rps_tests- Install dependencies:
# Install forge-std test library
forge install foundry-rs/forge-std@v1.9.5
# Install Node.js dependencies (for TypeScript tests)
yarn install
# Build contracts (generates ABIs needed for TS tests)
forge build- Create
.envfile:
cp .env.example .env- Configure
.envfile:
# Required for all tests
PRIVATE_KEY=0x... # First wallet (player1)
PRIVATE_KEY_2=0x... # Second wallet (player2) - optional
# Required for Sepolia tests
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/your_key
CONTRACT_ADDRESS=0xDa929CFa4E076d9928674Ba4a3adf5E02E71f64C # Pre-deployed Sepolia contract
# Optional
ANVIL_RPC_URL=http://127.0.0.1:8545 # Default if not set
ETHERSCAN_API_KEY=your_etherscan_keyNote: Use 0x-prefixed hex private keys (e.g., 0x0123abcd...).
All test commands are available via Yarn scripts in package.json. Test reports are automatically saved to the reports/ folder.
Run all Solidity tests:
yarn test:solThis runs all test suites:
- Unit Tests (
sol_tests/RockPaperScissors.t.sol): Function-level logic, revert reasons, edge cases - Integration Tests (
sol_tests/RockPaperScissorsIntegration.t.sol): Full contract flows and state transitions - E2E Tests (
sol_tests/RockPaperScissorsE2E.t.sol): Complete user journeys with events and balance assertions
Anvil (Local):
# Terminal 1: Start Anvil with zero fees for exact assertions
yarn anvil:zero
# Terminal 2: Run tests
yarn test:ts:anvilSepolia (Testnet):
yarn test:ts:sepoliaAnvil (Local):
# Terminal 1: Start Anvil with zero fees
yarn anvil:zero
# Terminal 2: Run tests
yarn test:ethers:anvilSepolia (Testnet):
yarn test:ethers:sepolia- Anvil Tests: Zero-fee setup for exact balance assertions (perfect math)
- Sepolia Tests:
- Real-time event listening and polling
- Dynamic gas estimation with 1.3x multiplier
- Automatic gas fee calculation for balance assertions
- Time-based waiting for tie resolution deadlines
All test output is automatically logged to the reports/ folder with timestamps:
- Format:
{testName}_{environment}_{timestamp}.log - Examples:
run_viem_anvil_2025-11-04T18-02-30.logrun_ethers_sepolia_2025-11-04T18-08-19.logforge_test_solidity_2025-11-04T18-10-45.log
.
├── src/
│ └── RockPaperScissors.sol # Main smart contract
├── sol_tests/ # Solidity test suites (Foundry)
│ ├── RockPaperScissors.t.sol # Unit tests
│ ├── RockPaperScissorsIntegration.t.sol # Integration tests
│ └── RockPaperScissorsE2E.t.sol # E2E tests
├── ts_tests/ # TypeScript E2E tests
│ ├── run.ts # Viem-based test runner
│ ├── run_ethers.ts # Ethers.js-based test runner
│ ├── runForgeTest.ts # Foundry test wrapper
│ ├── reportLogger.ts # Test report generator
│ └── client/
│ └── utils.ts # Test utilities
├── reports/ # Generated test reports (gitignored)
├── script/
│ └── Deploy.s.sol # Deployment script
├── docs/
│ └── TEST_STRATEGY.md # Testing strategy document
├── .github/workflows/
│ └── ci.yml # CI/CD pipeline
├── foundry.toml # Foundry configuration
├── package.json # Node.js dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md # This file
-
Game Creation: Player 1 creates a game with a hashed move commitment and stakes 0.001 ETH
- Game emits
GameCreatedevent (discoverable by other players) - Game stays open indefinitely until someone joins
- Game emits
-
Game Join: Player 2 joins with their own move commitment and stakes 0.001 ETH
- Emits
GameJoinedevent - Starts 2-minute reveal phase
- Emits
-
Reveal Phase: Both players reveal their moves within 2 minutes
- Contract verifies commitments match hashes
- Emits
GameRevealedevent
-
Resolution:
- If winner: Winner receives full stake (0.002 ETH)
- If tie: Enters 2-minute tie resolution phase
-
Tie Resolution (if applicable):
- Both players choose REMATCH or SPLIT
- Both REMATCH: Game restarts (same game ID)
- Otherwise: Funds split (0.001 ETH each)
- Timeout: Auto-split funds
// Generate commitment off-chain
bytes32 salt = keccak256("my_secret_salt");
bytes32 nonce = keccak256("my_nonce");
bytes32 moveHash = keccak256(abi.encodePacked(Move.ROCK, salt, nonce));
// Player 1 creates game
uint256 gameId = rps.createGame{value: 0.001 ether}(moveHash);
// Player 2 joins (after discovering via event)
rps.joinGame{value: 0.001 ether}(gameId, moveHash2);
// Both reveal
rps.reveal(gameId, Move.ROCK, salt, nonce);
rps.reveal(gameId, Move.PAPER, salt2, nonce2);
// Winner automatically receives fundscreateGame(bytes32 moveHash): Create a new game with stakejoinGame(uint256 gameId, bytes32 moveHash): Join an existing gamereveal(uint256 gameId, Move move, bytes32 salt, bytes32 nonce): Reveal your movehandleTie(uint256 gameId, TieChoice choice): Handle tie resolution (REMATCH or SPLIT)cancelGame(uint256 gameId): Cancel a game if no one joined
getGame(uint256 gameId): Get game detailsisRevealDeadlinePassed(uint256 gameId): Check if reveal deadline passed
GameCreated(uint256 indexed gameId, address indexed player1, uint256 stake)GameJoined(uint256 indexed gameId, address indexed player2)GameRevealed(uint256 indexed gameId, Move player1Move, Move player2Move)GameResolved(uint256 indexed gameId, address indexed winner, Move move1, Move move2)TieHandled(uint256 indexed gameId, TieChoice player1Choice, TieChoice player2Choice, bool isRematch)GameCancelled(uint256 indexed gameId)
The project follows a test pyramid approach:
-
Unit Tests (
sol_tests/RockPaperScissors.t.sol): Fast, isolated function tests- Function-level logic and revert reasons
- Edge cases (timeouts, invalid commitments, double operations)
- All winner/tie combinations
- Fixed-stake enforcement
-
Integration Tests (
sol_tests/RockPaperScissorsIntegration.t.sol): Full contract flows- State transitions (create → join → reveal → resolve)
- Tie flows (SPLIT/REMATCH)
- Multiple concurrent games
- Timeout-claim flows
-
E2E Tests (
sol_tests/RockPaperScissorsE2E.t.sol+ TypeScript): Client-perspective journeys- Complete happy paths
- Game discovery via events
- Balance assertions
- Real network interaction (Anvil + Sepolia)
See TEST_STRATEGY.md for detailed testing strategy.
- ✅ All core functions (create, join, reveal, handleTie, cancel)
- ✅ Win/loss logic (3 wins to cover all 6 combinations + 2 tie scenarios)
- ✅ Edge cases (timeouts, invalid commitments, double operations)
- ✅ State transitions
- ✅ Fund transfers with exact balance assertions
- ✅ Event emissions
- ✅ Gas optimization testing
# Start Anvil
yarn anvil:zero # or just: anvil
# Deploy (in another terminal)
forge script script/Deploy.s.sol:DeployScript --rpc-url http://localhost:8545 --broadcastThe contract is already deployed at: 0xDa929CFa4E076d9928674Ba4a3adf5E02E71f64C
To deploy a new instance:
forge script script/Deploy.s.sol:DeployScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify- Commit-Reveal: Prevents front-running by hiding moves until both committed
- Deadline Enforcement: Prevents indefinite games
- Automatic Split: Tie resolution timeout prevents stuck games
- Access Control: Only authorized players can interact with their games
MIT
Homework submission for confidential dApp with comprehensive testing strategy.