Smart Contracts
All contracts are deployed on Base (Ethereum L2), written in Solidity 0.8.24, and use OpenZeppelin libraries for security primitives.
Contract Overview
BaseEscrow
The abstract base contract that every game engine inherits. It handles everything related to money — staking, settlement, rake, refunds, timeouts. Game engines only need to implement game-specific logic.
Constants
| Constant | Value | Description |
|---|---|---|
RAKE_BPS | 1000 | 10% total rake on every settled pot |
DEV_ROYALTY_BPS | 250 | 2.5% of pot to the game developer |
MUTUAL_TIMEOUT_PENALTY_BPS | 100 | 1% penalty on mutual timeout refunds |
JOIN_WINDOW | 1 hour | Time for opponents to join after match creation |
MIN_STAKE | 100,000 | Minimum stake: 0.10 USDC (6 decimals) |
Match Lifecycle
Every match follows the same state machine regardless of game type:
OPEN — Match created, waiting for opponents. Creator has staked USDC.
PENDING — All players joined. Spectator betting window is open. Match activates after bettingDeadline passes.
ACTIVE — Game in progress. Players submit moves, referee settles rounds.
SETTLED — Winner determined. Pot distributed minus rake.
VOIDED — Match cancelled. All players refunded.
Match Creation
When a game engine calls _initMatch(), BaseEscrow:
- Validates the stake meets
MIN_STAKE - Validates player count (2-6)
- Validates
maxRounds >= winsRequired - Increments
matchCounterfor a new match ID - Pulls USDC from the creator via
safeTransferFrom - Adds creator as
players[0] - Initializes the
wins[]array
Joining
joinMatch() pulls the same stake amount from the joining player. When the match is full:
- If
bettingWindow > 0— match goes to PENDING, prediction pool is auto-created, match activates after the deadline - If
bettingWindow == 0— match goes directly to ACTIVE
Settlement
BaseEscrow provides three settlement paths:
Single winner — _settleMatchSingleWinner(matchId, winnerIndex)
- Calculates 10% rake (7.5% protocol + 2.5% developer)
- Sends developer royalty to the address from LogicRegistry
- Sends protocol rake to treasury
- Sends remaining pot to winner
- Records volume in LogicRegistry
Draw — _settleMatchDraw(matchId)
- Same rake calculation
- Remaining pot split equally among all players
- Rounding dust goes to last player
Split pot — _settleMatch(matchId, resolution)
- Supports arbitrary winner splits via basis points (must sum to 10,000)
- Used for poker split pots
Rake Math
For a 2-player match with 1 USDC stakes each (2 USDC pot):
Total pot: 2,000,000 (2.00 USDC)
Total rake (10%): 200,000 (0.20 USDC)
Dev royalty: 50,000 (0.05 USDC) → game developer
Protocol rake: 150,000 (0.15 USDC) → treasury
Winner payout: 1,800,000 (1.80 USDC)
If no developer is registered for the game logic, the full 10% goes to treasury.
Timeouts
BaseEscrow exposes two timeout mechanisms:
claimTimeout()— any player in an active match can call this. The game engine's_claimTimeout()override determines who timed out and awards the win.mutualTimeout()— both players get refunded minus a 1% penalty. The game engine's_mutualTimeout()override enforces timing requirements.
Safety Features
- ReentrancyGuard on all external fund-moving functions
- Ownable2Step — ownership transfer requires acceptance (prevents accidental transfers)
- Pausable — owner can freeze all match creation and gameplay
- Pull payments — if a USDC transfer fails (blocklisted address), funds go to a withdrawal queue via
pendingWithdrawals adminVoidMatch()— emergency escape hatch with pro-rata refunds accounting for mid-game distributions
PokerEngineV5
The poker-specific game engine. Inherits BaseEscrow and adds a complex phase machine for fair card games with hidden information.
Supported Variants
| Variant | Players | Private Cards | Board Cards | Draw Phase |
|---|---|---|---|---|
| 5-Card Draw | 2 | 5 | 0 | Yes |
| Texas Hold'em | 2-6 | 2 | 5 | No |
| Omaha | 2-6 | 4 | 5 | No |
| 7-Card Stud | 2-6 | 3 | 0 | No |
Phase Machine
Poker matches follow a strict phase progression:
Entropy Commit/Reveal
This is how FALKEN ensures fair dealing without trusting the referee:
- Commit — each player submits
keccak256(salt)on-chain. Nobody can see anyone's salt. - Reveal — each player reveals their actual salt. The contract verifies it matches the commit.
- Seed — the referee combines all salts with the match ID and round number to derive a deterministic deck shuffle.
Since both players contribute entropy, neither the referee nor any single player can control the deck.
Delegatecall Pattern
PokerEngineV5 exceeds the EIP-170 contract size limit (24KB), so betting actions, resolution logic, and timeout handling are split into PokerEngineV5Actions — a separate contract called via delegatecall. This executes the Actions code in the context of PokerEngineV5's storage.
Key Functions
| Function | Caller | Description |
|---|---|---|
createMatch() | Player | Stake USDC, pick variant, set rounds |
commitEntropy() | Player | Submit salt hash |
submitPrivateEntropy() | Referee | Reveal player salts |
publishInitialHandReceipt() | Referee | Commit to dealt hands |
publishDealCheckpoint() | Referee | Advance deal stage |
check() / call() / raise() / fold() | Player | Betting actions |
submitDiscardMask() | Player | Draw phase discards |
resolveRound() | Referee | Declare round winner |
resolveShowdownSplit() | Referee | Split pot between winners |
TurnEngineV1
A generic engine for any 2-player turn-based game with open state — chess, checkers, Go, Connect Four, RPG battles. Much simpler than PokerEngineV5 because there's no hidden information.
How It Works
- Players alternate submitting moves as strings (e.g.
"e2e4"for chess) - The contract records each move as an event and enforces turn order
- Move validation happens off-chain in the FISE sandbox
- The referee calls
settleRound()when a terminal state is detected
Multi-Round Support
TurnEngineV1 supports best-of-N matches:
createMatch(stake, logicId, winsRequired=2, maxRounds=3)
- Starting player alternates each round for fairness
- Move count resets each round
- Match auto-settles when a player reaches
winsRequiredormaxRoundsis exhausted - If tied after max rounds,
_settleByMostWins()picks the leader or declares a draw
Timeouts
MOVE_TIMEOUT— 10 minutes. If the active player doesn't move in time, the opponent can claim the round.- In multi-round matches, a timeout awards a round win (not an instant match win).
- Mutual timeout — if both players are inactive, match settles as a draw with refunds.
Key Functions
| Function | Caller | Description |
|---|---|---|
createMatch() | Player | Stake USDC, pick game, set rounds |
submitMove() | Player | Record a move (turn enforced) |
settleRound() | Referee | Declare round winner |
claimTimeout() | Player | Claim win if opponent is inactive |
getTurnState() | Anyone | View current turn, move count, last move time |
LogicRegistry
The on-chain registry that maps game logic to IPFS content hashes. This is how the protocol knows which JavaScript to execute for each game.
Registration
// Register a new game
bytes32 logicId = registry.registerLogic(
"QmPokerV7...", // IPFS CID
developerAddr, // Receives 2.5% royalty
true, // bettingEnabled
4 // maxStreets (Hold'em)
);
// Register a simple game (no betting phases)
bytes32 logicId = registry.registerSimpleGame(
"QmChessV1...", // IPFS CID
developerAddr // Receives 2.5% royalty
);
The logicId is keccak256(ipfsCid) — deterministic and verifiable.
Volume Tracking
Every time a match settles, the game engine calls recordVolume(logicId, amount) on the registry. Only authorized escrow contracts can call this function. This tracks total USDC volume per game for analytics and developer leaderboards.
Access Control
- Only the protocol owner can register new games (curated beta)
- Games can be deactivated (
setActive(logicId, false)) — existing matches continue but no new ones can be created - Escrow contracts must be whitelisted (
setAuthorizedEscrow) to record volume
PredictionPool
Parimutuel betting pools where spectators predict match outcomes.
How It Works
- When a match goes to PENDING, the game engine auto-creates a prediction pool with outcomes like
["Player 1", "Player 2", "Draw"] - Spectators place USDC bets on their predicted outcome during the betting window
- After the match settles, the contract resolves the pool
- Winners split the losing side's bets proportionally to their stake (minus 10% rake)
- If the match is a draw, all bets are refunded (no rake)
Key Properties
| Property | Value |
|---|---|
| Rake | 10% on winning pool |
| Minimum bet | 0.10 USDC |
| Max outcomes | 10 per pool |
| Draw handling | Full refund, no rake |
Security
- Only authorized escrow contracts can create match-linked pools
- Betting deadline enforced on-chain — no bets after the window closes
- Duplicate pool prevention per match
- Pull-payment fallback for failed claim transfers