ROBINHEAD DOCS Play ↗
Stakes & money

Escrow contract

MatchEscrow holds the two stakes of a single match and releases them exactly once: 95% to the winner, 5% to the burn address, or a full refund. There is no shared vault and nothing accumulates, so there is no standing pool to drain.

Trust model#

Three roles, each with a separate key, so that no single secret can move money on its own.

OPERATORserver hot walletlock · settle · refund, pays gas ORACLEsigns results only, no gas OWNER (2-step)rotate roles · pause MATCH ESCROW lockMatch(id, a, b, stake) settle(id, winner, sig) refund(id, sig) reclaim(id) ← anyone, after 3 days withdrawCredits() PLAYERSapprove once · receive 0x…dEaDburn address signature
Operator and oracle are separate secrets: a result is only paid if the oracle signed it, and only the operator sends transactions.
RoleCanCannot
OperatorLock a match, submit signed settle/refund, pay gasPay anyone without an oracle signature; drain the contract
OracleSign Result(matchId, winner, pot)Send transactions or touch funds
OwnerRotate operator/oracle, pause locking and settlingTake funds; reclaim and credit withdrawal still work while paused
PlayerApprove a stake, receive payouts, withdraw credits, reclaim after timeoutChoose the winner

Match state machine#

None Locked Settled Refunded lockMatch (operator)both stakes pulled settle + oracle sig95% winner · 5% burn refund + oracle sigor reclaim after 3 days
A match is locked once, then exactly one of settle / refund / reclaim closes it. That state guard is the replay protection: matchId acts as the nonce.

A match, end to end#

PLAYER APLAYER BSERVERORACLE KEYESCROW queue(stake)queue(stake) 1. lockMatch(id, A, B, stake) pulls stake from each (approve), checks delta == stake 2. PLAY: 60 s + golden goal (inputs ↔ snapshots) 3. Result(id, winner, pot)signature 4. settle(id, winner, sig) 5. prize → winner · fee → 0x…dEaD balance + allowance checked before pairing
Gas for steps 1 and 4 is paid by the operator wallet, not by players.

What the contract enforces#

ProtectionHow it works
EIP-712 signed resultssettle verifies an oracle signature over Result(matchId, winner, pot). The domain binds chain id and contract address, so a signature cannot be replayed elsewhere.
Pay-onceState moves Locked → Settled/Refunded before any transfer (checks-effects-interactions) and cannot go back.
Balance-delta checkOn lock, the contract measures what actually arrived per transfer and requires it to equal the stake. A fee-on-transfer token would fail at lock time instead of stranding a pot later.
Refunds are signed tooA refund needs an oracle signature over the same struct with winner = 0, so the operator alone cannot force one.
Fee guardrailConstructor requires feeBps < 10000 (live: 500 = 5%).
Reentrancy & pausingOpenZeppelin ReentrancyGuard, Pausable, Ownable2Step, SafeERC20.
Neutral domain nameThe EIP-712 name is a constructor parameter. The server must sign with the same ESCROW_DOMAIN_NAME or settlement fails (the server refuses to boot on a mismatch).

Escape hatches#

Server crashed

Auto-refund

Locks older than five minutes are swept; locks left by a crashed process are refunded at startup.

Everything offline

reclaim()

If a match stays Locked past 3 days, anyone can return both stakes to the players. No signature needed, funds only go back to the original two.

Hostile recipient

Pull-payment credits

If a transfer to one recipient reverts, their share is credited and pulled later with withdrawCredits(). It can never wedge the match or the other player's payout.

Why 3 days

reclaim cannot tell "server dead" from "result merely delayed", so it always refunds. A short window would let the loser of a decided-but-unsettled match reclaim and rob the winner. Real settlement lands in seconds, so 3 days is only reachable after a genuine outage.

Events#

EventEmitted when
Locked(matchId, a, b, stake)Both stakes are pulled in
Settled(matchId, winner, prize, burned)Winner paid and fee burned
Refunded(matchId)Refund or reclaim
Credited(to, amount) / CreditsWithdrawnA push payout failed and was parked, or later pulled
OperatorChanged / OracleChangedRole rotated by the owner

The live burn counter on the website is built from Settled events: it only reads logs and moves no funds.

Why the 5% goes to 0x…dEaD#

The game token does not expose a burn() function, so a native burn is not possible. Sending to the dead address removes the tokens from circulation just the same: nobody holds its private key. The only cosmetic difference is that total supply does not decrease.

RobinHead Ball is a game of skill. Staking tokens carries risk — never stake more than you can afford to lose. You are responsible for complying with the regulations in your jurisdiction.