Stake, seal your choice, and you are matched with whoever commits next. No lobby. No waiting room. You will never meet them, and the record is permanent.
Neither of you can see the other's choice until both are sealed in. There is no chat, no signal and no second round with the same person.
Each settled game writes both addresses and both choices to the chain. A wallet that always steals is a wallet everyone can see always steals — that is the actual thing at stake here.
Your choice is never on chain in the open until the game is over. What you submit is a hash, and the contract cannot read it — it can only check it later.
keccak(choice, salt, you). The stake moves with it. Nobody — including the contract — can tell split from steal.
One open commit at a time, per wallet. The moment somebody else commits, you are matched and the reveal window opens.
The contract recomputes the hash. If it matches, your choice counts. You cannot change it now — that is the entire point of step one.
If the window closes and only one side revealed, that side takes both stakes. If neither revealed, both are burned. Walking away is not an escape.
Not yet deployed. No owner, no admin, no pause, no upgrade and no rescue function — the same as everything else here.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract SplitSteal { uint8 constant SPLIT = 1; uint8 constant STEAL = 2; uint256 public immutable stake; uint64 public constant WINDOW = 7200; // blocks to reveal struct Game { address a; address b; bytes32 ha; bytes32 hb; uint8 ca; uint8 cb; uint64 deadline; bool settled; } mapping(uint256 => Game) public games; uint256 public openGame; // waiting for a partner, 0 if none uint256 public count; function commit(bytes32 h) external returns (uint256 id) { token.transferFrom(msg.sender, address(this), stake); if (openGame == 0) { // you are player a id = ++count; games[id] = Game(msg.sender, address(0), h, 0, 0, 0, 0, false); openGame = id; } else { // you are player b id = openGame; Game storage g = games[id]; require(g.a != msg.sender, "no self-match"); g.b = msg.sender; g.hb = h; g.deadline = uint64(block.number) + WINDOW; openGame = 0; } emit Committed(id, msg.sender); } function reveal(uint256 id, uint8 choice, bytes32 salt) external { Game storage g = games[id]; require(g.b != address(0) && !g.settled); bytes32 h = keccak256(abi.encode(choice, salt, msg.sender)); if (msg.sender == g.a && h == g.ha) g.ca = choice; else if (msg.sender == g.b && h == g.hb) g.cb = choice; else revert BadReveal(); if (g.ca != 0 && g.cb != 0) _settle(id); } function settle(uint256 id) external { // after the window, anybody require(block.number > games[id].deadline); _settle(id); } function _settle(uint256 id) internal { Game storage g = games[id]; require(!g.settled); g.settled = true; uint256 pot = stake * 2; if (g.ca == SPLIT && g.cb == SPLIT) { _pay(g.a, stake); _pay(g.b, stake); } else if (g.ca == SPLIT && g.cb == STEAL) { _pay(g.b, pot); } else if (g.ca == STEAL && g.cb == SPLIT) { _pay(g.a, pot); } else if (g.ca != 0 && g.cb == 0) { _pay(g.a, pot); } // b no-showed else if (g.ca == 0 && g.cb != 0) { _pay(g.b, pot); } // a no-showed else _burn(pot); // both stole, or both silent emit Settled(id, g.a, g.b, g.ca, g.cb); } }