Provably Fair

Every GGC CoinFlip result is determined by a Commit-Reveal scheme — a cryptographically verifiable mechanism where the outcome is locked on-chain before your bet is confirmed. Neither the server nor any player can change the result once committed.

How Commit-Reveal Works

1

Server generates a secret seed

Before your bet is placed, the server generates a random 32-byte secret seed. It computes: commitment = keccak256(abi.encode(seed, roundId)) and signs keccak256(abi.encode(commitment, roundId)) with the settler's private key. The seed is stored securely and never revealed until settlement.

2

You place your bet with the commitment

Your wallet calls placeBet(amount, side, commitment, signature). The smart contract verifies the signature is from the trusted settler address, then locks the commitment on-chain. At this point, the outcome is already determined — but neither you nor the server can change it.

3

Server reveals the seed to settle

After your bet is confirmed on-chain, the server calls settleRound(roundId, seed). The contract verifies keccak256(abi.encode(seed, roundId)) == stored commitment. Then it derives the result: resultSide = uint8(keccak256(abi.encodePacked(seed, roundId, player, betAmount)) % 2). The seed is now public and permanently on-chain.

4

You verify the result independently

Anyone can verify: (1) compute keccak256(abi.encode(revealedSeed, roundId)) and compare to the stored commitment. (2) Compute keccak256(abi.encodePacked(revealedSeed, roundId, player, betAmount)) % 2 and compare to resultSide. If both match, the result is provably fair.

Step-by-Step Verification Guide

  1. 1

    Find your Round ID

    After placing a bet, the Round ID is shown in the Provably Fair sidebar on the CoinFlip page. You can also find it in the BetPlaced event emitted by your bet transaction.

  2. 2

    Read the round data on BscScan

    0x3c4d853F...0B4a1F18

    Go to Read Contract → getRound(roundId) and enter your Round ID. Note these fields:

    • commitment — the locked hash
    • revealedSeed — the secret seed (visible after settlement)
    • player — your wallet address
    • betAmount — your bet in wei (18 decimals)
    • resultSide — 0 = Heads, 1 = Tails
  3. 3

    Verify the commitment matches the seed

    Run in your browser console (with ethers.js v6) or any Solidity tool:

    // Verify commitment
    const { ethers } = require("ethers");
    const abiCoder = ethers.AbiCoder.defaultAbiCoder();
    
    const encoded = abiCoder.encode(
      ["bytes32", "uint256"],
      [revealedSeed, roundId]
    );
    const computedCommitment = ethers.keccak256(encoded);
    
    console.log("Stored commitment:", commitment);
    console.log("Computed commitment:", computedCommitment);
    console.log("Match:", commitment === computedCommitment);
    // Must be true!
  4. 4

    Reproduce the result yourself

    Compute the result using the same formula as the smart contract:

    // Derive resultSide (same as Solidity contract)
    const packed = ethers.solidityPacked(
      ["bytes32", "uint256", "address", "uint256"],
      [revealedSeed, roundId, player, betAmount]
    );
    const hash = ethers.keccak256(packed);
    const computedResultSide = Number(BigInt(hash) % 2n);
    
    console.log("Computed resultSide:", computedResultSide);
    // 0 → Heads  |  1 → Tails
    console.log("On-chain resultSide:", resultSide);
    console.log("Match:", computedResultSide === resultSide);
    // Must be true!

    If both checks pass, the game was provably fair.

  5. 5

    Verify the settler's signature (optional)

    The settler address is stored on-chain. Call settlerSigner() on the contract. Verify the signature from the BetPlaced transaction:

    // The settler signs: keccak256(abi.encode(commitment, roundId))
    const sigMessage = ethers.keccak256(
      abiCoder.encode(
        ["bytes32", "uint256"],
        [commitment, roundId]
      )
    );
    const recovered = ethers.verifyMessage(
      ethers.getBytes(sigMessage),
      signature
    );
    console.log("Recovered signer:", recovered);
    console.log("On-chain settlerSigner:", settlerSigner);
    console.log("Match:", recovered === settlerSigner);

Cryptographic Formulas

Commitment
keccak256(abi.encode(seed, roundId))

Locked on-chain before bet. Proves the seed was chosen before the player's bet.

Signature message
keccak256(abi.encode(commitment, roundId))

Signed by settler's private key. Contract verifies ecrecover == settlerSigner.

Result derivation
uint8(keccak256(abi.encodePacked(seed, roundId, player, betAmount)) % 2)

Mixes seed with round-specific data. 0 = Heads, 1 = Tails.

Win condition
resultSide == selectedSide

If the derived side matches the player's chosen side, the player wins.

Security Model

Can the server change the result after you bet?

No. The commitment (keccak256 of the seed + roundId) is locked on-chain before your bet is confirmed. The server must reveal the exact seed that hashes to that commitment — it cannot be changed retroactively.

Can the server refuse to settle?

The settler sweep runs every 15 seconds automatically. If the server fails temporarily, unsettled rounds are retried on recovery. The commitment is already on-chain and the round can be settled by anyone who knows the seed.

Can the result be manipulated by including player/betAmount in the hash?

No. The player address and betAmount are fixed at the time of placeBet. They cannot be changed after the commitment is locked. Including them in the result hash adds extra entropy and prevents the server from pre-computing results for arbitrary bet amounts.

Does the server know the result before you?

Yes — the server generates the seed before your bet. However, it cannot change the seed after your bet is on-chain (the commitment locks it). This is the standard Commit-Reveal trade-off: the operator knows the outcome but cannot manipulate it once committed.

This is equivalent to how Rollbit, Stake.com, and most provably fair casinos operate.

Contract Address

CoinFlipGame (Commit-Reveal v2)0x3c4d85...4a1F18

Frequently Asked Questions

Can the house manipulate the result?

No. Once placeBet() is called with the commitment on-chain, the result is determined solely by the seed that was pre-committed. The contract owner cannot influence, predict, or change the outcome after the commitment is recorded.

What if the server goes offline before settling?

The settler sweep runs every 15 seconds. If the server is temporarily unavailable, unsettled rounds are retried automatically on recovery. The commitment and seed are stored in the database and on-chain respectively.

How is this different from Chainlink VRF?

Chainlink VRF uses a decentralized oracle network to generate randomness, so no single party controls the outcome. Commit-Reveal relies on the server generating the seed honestly before the bet. The trade-off is: VRF is more decentralized but slower (~30s) and more expensive (~$0.25/round); Commit-Reveal is faster (~5s) and cheaper but requires trusting that the server committed before seeing your bet.

Why does the result formula include player address and betAmount?

Including player-specific data (address + betAmount) in the result hash prevents the server from pre-computing results for different bet amounts. Even if the server knows the seed, it cannot predict what result a specific player with a specific bet amount will get until the bet is actually placed.

Where is the source code?

The CoinFlipGame contract source is available in the project repository. You can read the full ABI and verified source code on the BscScan explorer page linked above.