Skip to main content

Smart Contracts


📋 Executive Summary

Architecture Overview

HYPERGRID implements a three-contract system that separates concerns across:

  1. GameMaster - Configuration and game deployment (UUPS upgradeable)
  2. GameVault - Custody and fund management (immutable)
  3. GameInstance - Individual game logic (minimal clone pattern)

Key Innovation

Self-coordinating games with immutable settlement rules - Once deployed, game instances operate independently with baked-in oracle consensus, preventing any centralized interference with settlements.


🏗️ System Architecture

Three-Layer Design

┌─────────────────────────────────────────────────────────────┐
│ GAMEMASTER │
│ (UUPS Upgradeable Proxy - Configuration Layer) │
│ │
│ • Game template management │
│ • Jackpot metadata registry │
│ • Server authorization │
│ • Operator configuration │
└──────────────────────┬──────────────────────────────────────┘

│ deploys via CREATE2

┌─────────────────────────────────────────────────────────────┐
│ GAMEINSTANCE (Clone) │
│ (Minimal Proxy - Immutable Game Logic) │
│ │
│ • Entry tracking │
│ • Oracle vote coordination │
│ • Settlement consensus │
│ • SELF-EXECUTING settlement trigger │
└──────────────────────┬──────────────────────────────────────┘

│ calls executeSettlement()

┌─────────────────────────────────────────────────────────────┐
│ GAMEVAULT │
│ (Immutable - Custody and Entry Point) │
│ │
│ • Player balance management │
│ • Token custody (ERC-20) │
│ • Prize distribution (Merkle-based) │
│ • Jackpot pool accounting │
│ • Emergency pause controls │
└─────────────────────────────────────────────────────────────┘

🎯 Core Components

1. GameMaster (Configuration Layer)

Contract Type: UUPS Upgradeable Proxy Primary Role: Game deployment and system configuration Upgrade Authority: Contract Owner (Platform)

Key Responsibilities

FunctionDescriptionTrust Level
Template ManagementDefine prize structures, consensus rulesTRUSTED
Jackpot RegistryCreate and manage jackpot poolsTRUSTED
Server AuthorizationWhitelist game serversTRUSTED
Game DeploymentDeploy new GameInstance clonesTRUSTED
Operator ConfigurationSet fee recipientsTRUSTED

Critical Functions

// TEMPLATE MANAGEMENT
function createTemplate(
bytes32 templateId,
uint256 entryFeeBPS, // 10% = 1000 BPS
uint256 platformFeeBPS, // 5% = 500 BPS
uint256 hostFeeBPS, // 5% = 500 BPS
uint256 jackpotFeeBPS, // 5% = 500 BPS
uint256[] payoutBPS, // Prize distribution
uint32 requiredConfirmations, // 3-of-5 judges
uint32 consensusTimeout // 5 minutes
)

// GAME DEPLOYMENT (CREATE2 for deterministic addresses)
function deployGame(
bytes32 templateId,
bytes32 gameId, // Unique identifier
address wagerToken, // USD0, USDC, etc.
uint96 wagerAmount, // Per-entry wager
address host, // Optional host
bytes32 jackpotId, // Optional jackpot
address gameServer, // Authorized server
address[] judges // Immutable judge list
) returns (address gameInstance)

// JACKPOT MANAGEMENT
function createJackpot(
bytes32 jackpotId,
address token
)

function setJackpotState(
bytes32 jackpotId,
JackpotState newState, // ACTIVE/PAUSED/DEPRECATED/CLOSED
string reason
)

Upgrade Mechanism

Pattern: UUPS (Universal Upgradeable Proxy Standard)

  • Implementation can be upgraded by owner
  • Storage layout must be preserved
  • Critical for configuration changes WITHOUT redeploying vault
  • CANNOT affect live games (games are immutable clones)

What CAN be upgraded: ✅ New template creation logic ✅ Jackpot state management ✅ Authorization mechanisms ✅ Gas optimizations

What CANNOT be upgraded: ❌ Live game settlement rules ❌ Deployed game judge lists ❌ GameVault custody logic ❌ Player balances


2. GameVault (Custody Layer)

Contract Type: Immutable (No Upgradeability) Primary Role: Token custody and prize distribution Trust Model: Trustless custody with owner emergency controls

Key Responsibilities

FunctionDescriptionTrust Level
Balance CustodyHold player ERC-20 tokensTRUSTLESS
Entry ProcessingTransfer funds to game balanceTRUSTLESS
Prize DistributionMerkle-based winner claimsTRUSTLESS
Jackpot AccountingTrack jackpot pool balancesTRUSTLESS
Emergency PauseCircuit breaker for securityTRUSTED
Token ConfigurationSet deposit/wager limitsTRUSTED

Critical Functions

// PLAYER OPERATIONS (Trustless)
function deposit(address token, uint256 amount)
function withdraw(address token, uint256 amount)
function enterGame(address game, uint256 entries)

// PRIZE CLAIMS (Trustless - Merkle Proof)
function claimPrize(
bytes32 gameId,
uint256 amount,
bytes32[] merkleProof
)

function claimJackpot(
bytes32 gameId,
uint256 amount,
bytes32[] merkleProof
)

// SETTLEMENT (GameInstance Only)
function executeSettlement(
bytes32 gameId,
bytes32 prizeRoot, // Merkle root for prizes
bytes32 jackpotRoot, // Merkle root for jackpots
uint256 totalWinnings,
uint256 platformFee,
uint256 hostFee,
uint256 jackpotContribution,
address[] jackpotWinners, // Atomic distribution
uint256 claimPercentageBPS
)

// EMERGENCY CONTROLS (Owner Only)
function pause() // Stop deposits/entries
function unpause() // Resume operations

Security Architecture

Immutability Benefits:

  • Settlement logic CANNOT change
  • Prize distribution CANNOT be altered
  • Jackpot balances CANNOT be redirected
  • Player withdrawals ALWAYS available (even when paused)

Trust Boundaries:

TRUSTLESS ZONE (User Operations):
├── deposit() → Direct token transfer
├── withdraw() → Direct token return
├── claimPrize() → Merkle proof validation
└── claimJackpot() → Merkle proof validation

TRUSTED ZONE (Admin Operations):
├── authorizeGame() → GameMaster only
├── enableToken() → Owner only
├── pause()/unpause() → Owner only (emergency)
└── Emergency withdrawal → NOT POSSIBLE

Balance Tracking System

Three-Tier Accounting:

// Tier 1: Player Balances (withdrawable)
mapping(address player => mapping(address token => uint256)) _balances

// Tier 2: Game Balances (locked during gameplay)
mapping(address game => uint256) _gameBalances

// Tier 3: Jackpot Balances (accumulated contributions)
mapping(bytes32 jackpotId => mapping(address token => uint256)) _jackpotBalances

Accounting Invariants (Enforced):

Physical Balance >= (Player Balances + Game Balances + Jackpot Balances)

Automatic Reconciliation:

function syncTokenAccounting(address token) external {
uint256 physicalBalance = IERC20(token).balanceOf(address(this));
uint256 accountedBalance = _totalDeposited - _totalWithdrawn;

if (physicalBalance < accountedBalance) {
_pause(); // AUTO-PAUSE on discrepancy
emit AccountingDiscrepancy(token, physicalBalance, accountedBalance);
}
}

3. GameInstance (Game Logic Layer)

Contract Type: Minimal Clone (EIP-1167) Primary Role: Individual game execution and settlement coordination Trust Model: Immutable rules with decentralized oracle consensus

Key Characteristics

AspectImplementationImmutability
DeploymentCREATE2 (deterministic address)✅ Immutable
Judge ListSet at deployment✅ Immutable
Consensus ThresholdBaked into clone✅ Immutable
Timeout DurationFixed at deployment✅ Immutable
Prize DistributionCached from template✅ Immutable
Vault ReferenceSet at deployment✅ Immutable

Self-Coordination Architecture

CRITICAL INNOVATION: GameInstance is self-executing - no external coordinator needed.

Traditional Pattern (Vulnerable):
Player Entry → Game → Coordinator → Oracle → Coordinator → Vault → Settlement
↑ ↑
TRUST POINT TRUST POINT

HyperGrid (Trustless):
Player Entry → Game → Judges Vote Directly → Game Auto-Settles → Vault

CONSENSUS DETECTION
(3-of-5 threshold)

IMMEDIATE SETTLEMENT
(No coordinator needed)

Oracle Voting System

Multi-Judge Consensus (Gnosis Safe Pattern):

// IMMUTABLE JUDGE LIST (set at deployment)
address[] judges; // e.g., [0xJudge1, 0xJudge2, 0xJudge3, 0xJudge4, 0xJudge5]

// VOTING MECHANISM
mapping(bytes32 settlementHash => uint256 voteCount) settlementVotes;
mapping(address judge => bytes32 votedHash) judgeVotes;

// SETTLEMENT SUBMISSION (Judge Only)
function submitSettlement(
uint256 totalEntries,
address[] winners,
uint256[] scores,
bytes32 prizeRoot,
bytes32 jackpotRoot,
address[] jackpotWinners
) external onlyJudge {
bytes32 hash = keccak256(abi.encode(...));

// Record vote
judgeVotes[msg.sender] = hash;
settlementVotes[hash]++;

// CHECK CONSENSUS
if (settlementVotes[hash] >= requiredConfirmations) {
_executeSettlement(hash); // AUTO-TRIGGER
}
}

Consensus Detection:

  • Threshold: 3-of-5 judges (configurable per game)
  • Auto-Execute: Settlement triggers IMMEDIATELY when threshold reached
  • Timeout Failsafe: After consensusTimeout (e.g., 5 minutes), anyone can call abandonGame()

State Machine

ACTIVE (deposits open)

│ closeDeposits()

ACTIVE (deposits closed)

│ submitSettlement() × 3

SETTLING (consensus reached)

│ _executeSettlement() [AUTO]

COMPLETED (prizes claimable)

OR (timeout path):

│ consensusTimeout exceeded

ABANDONED (refunds available)

Critical Functions

// ENTRY TRACKING (Vault Only)
function registerEntries(address player, uint256 count) external onlyVault

// ORACLE COORDINATION (Judges Only)
function submitSettlement(...) external onlyJudge

// AUTOMATIC SETTLEMENT (Internal - Self-Executing)
function _executeSettlement(bytes32 consensusHash) internal {
// 1. Decode settlement data
// 2. Generate Merkle roots
// 3. Call GameVault.executeSettlement()
// 4. Confirm settlement (state = COMPLETED)
}

// TIMEOUT HANDLING (Permissionless)
function abandonGame() external {
require(block.timestamp > gameEndTime + consensusTimeout);
_setState(GameState.ABANDONED);
}

// REFUND CLAIMS (Abandoned Games Only)
function getRefundAmount(address player) external view returns (uint256)

🔐 Trust Model & Security Boundaries

Decentralized Components (Trustless)

ComponentMechanismGuarantee
Prize ClaimsMerkle proof validationCryptographic proof of winnings
Jackpot DistributionAtomic settlement + MerkleCannot be altered after settlement
RefundsSmart contract logicAutomatic in ABANDONED state
Player WithdrawalsDirect balance accessAlways available (even paused)
Settlement RulesImmutable clone dataBaked in at deployment

Trusted Components (Admin Controlled)

ComponentAuthorityRisk Mitigation
GameMaster UpgradesContract OwnerCANNOT affect live games
Vault PauseContract OwnerWithdrawals still work
Server AuthorizationContract OwnerGames can timeout/abandon
Token ConfigurationContract OwnerExisting balances unaffected
Jackpot StateContract OwnerBalances remain in vault

Critical Security Properties

1. Settlement Immutability

Once GameInstance deployed:
✅ Judge list CANNOT change
✅ Consensus threshold CANNOT change
✅ Prize distribution CANNOT change
✅ Timeout duration CANNOT change
❌ GameMaster upgrade CANNOT interfere
❌ Owner CANNOT modify rules

2. Fund Custody Guarantees

GameVault (immutable):
✅ Player funds always withdrawable
✅ Merkle proofs cryptographically enforce prizes
✅ Platform CANNOT steal player deposits
✅ Platform CANNOT redirect jackpots
✅ Pause ONLY stops new entries (not withdrawals)

3. Oracle Decentralization

Judge voting:
✅ Minimum 3-of-5 consensus required
✅ No single judge can settle alone
✅ Judges vote independently
✅ Collusion requires 3+ judges
✅ Timeout prevents judge censorship

4. Emergency Failsafes

If judges fail to reach consensus:
✅ After timeout, anyone can call abandonGame()
✅ All entry fees become refundable
✅ Players claim refunds via getRefundAmount()
✅ No funds lost to oracle failure

⚙️ Granular Control Mechanisms

Owner Controls (Highest Trust)

GameMaster.sol:

// TEMPLATE MANAGEMENT
onlyOwner createTemplate() // Define new prize structures
onlyOwner updateTemplate() // Modify prize distribution
onlyOwner pauseTemplate() // Disable template usage

// JACKPOT MANAGEMENT
onlyOwner createJackpot() // Create new jackpot pool
onlyOwner setJackpotState() // ACTIVE/PAUSED/DEPRECATED/CLOSED

// AUTHORIZATION
onlyOwner authorizeServer() // Whitelist game servers
onlyOwner setOperator() // Change fee recipient
onlyOwner setVault() // Change vault reference (risky)

// UPGRADE
onlyOwner upgradeTo() // UUPS upgrade implementation

GameVault.sol:

// TOKEN CONFIGURATION
onlyOwner enableToken() // Add new ERC-20 token
onlyOwner updateTokenConfig() // Modify deposit/wager limits

// GAME AUTHORIZATION
onlyGameMaster authorizeGame() // Register new game instance
onlyOwner deauthorizeGame() // Emergency deauthorization

// EMERGENCY CONTROLS
onlyOwner pause() // Circuit breaker (stop entries)
onlyOwner unpause() // Resume operations

GameMaster Controls (Privileged)

// GameVault grants special privileges to GameMaster address
modifier onlyGameMaster() {
require(msg.sender == gameMaster);
_;
}

// GameMaster can:
authorizeGame() // Register new games in vault

Operator Controls (Fee Recipient)

No contract permissions - operator is passive fee recipient only.

// Operator receives:
- Platform fees (credited to balance in vault)
- Can withdraw() like any user
- CANNOT modify game logic
- CANNOT access other balances

Server Controls (Authorized Game Deployers)

// Authorized servers can:
modifier onlyAuthorizedServer() {
require(_authorizedServers[msg.sender]);
_;
}

deployGame() // Create new game instances

Judge Controls (Oracle Participants)

// Judges in specific game can:
modifier onlyJudge() {
require(_isJudge(msg.sender));
_;
}

submitSettlement() // Submit consensus vote

Per-game isolation: Judges for Game A cannot interact with Game B.

Permissionless Operations (No Trust Required)

// Anyone can call:
deposit() // Add funds to balance
withdraw() // Remove funds from balance
enterGame() // Join game (if accepting)
claimPrize() // Claim winnings (with proof)
claimJackpot() // Claim jackpot (with proof)
abandonGame() // Trigger timeout refund
getRefundAmount() // Check refund eligibility
syncTokenAccounting() // Audit vault balances

📊 Fund Flow Architecture

Entry Flow

1. Player: deposit(USD0, $100) → GameVault
├─ _balances[player][USD0] += $100
└─ Physical custody: IERC20.transferFrom()

2. Player: enterGame(game, 10 entries) → GameVault
├─ Validate: _balances[player][USD0] >= $10
├─ Deduct: _balances[player][USD0] -= $10
├─ Lock: _gameBalances[game] += $10
└─ Register: GameInstance.registerEntries(player, 10)

3. GameInstance: Track entries
├─ _hasEntry[player] = true
├─ _playerEntries[player] += 10
└─ Emit: EntryRegistered(gameId, player, $1, timestamp)

Settlement Flow (Oracle Consensus)

1. Judge 1: submitSettlement(winners, scores, roots) → GameInstance
├─ Hash: keccak256(settlement data)
├─ Vote: settlementVotes[hash]++ (now 1/3)
└─ Emit: SettlementVoted(judge1, hash)

2. Judge 2: submitSettlement(same data) → GameInstance
├─ Same hash calculated
├─ Vote: settlementVotes[hash]++ (now 2/3)
└─ Emit: SettlementVoted(judge2, hash)

3. Judge 3: submitSettlement(same data) → GameInstance
├─ Same hash calculated
├─ Vote: settlementVotes[hash]++ (now 3/3) ✅ CONSENSUS
├─ AUTO-TRIGGER: _executeSettlement()
└─ ╰─→ GameVault.executeSettlement(...)
├─ Store: _prizeRoots[gameId] = merkleRoot
├─ Store: _jackpotRoots[gameId] = jackpotRoot
├─ Credit: _balances[operator][USD0] += platformFee
├─ Credit: _balances[host][USD0] += hostFee
├─ Add: _jackpotBalances[jackpotId][USD0] += contribution
├─ Deduct: _gameBalances[game] -= totalSettlement
├─ Track: _unclaimedWinnings[gameId] = totalWinnings
└─ Confirm: GameInstance.confirmSettlement()

Prize Claim Flow (Merkle Proof)

1. Winner: claimPrize(gameId, $50, proof) → GameVault
├─ Validate: MerkleProof.verify(proof, _prizeRoots[gameId], leaf)
├─ Check: !_prizeClaimed[gameId][player]
├─ Mark: _prizeClaimed[gameId][player] = true
├─ Credit: _balances[player][USD0] += $50
├─ Track: _unclaimedWinnings[gameId] -= $50
└─ Emit: PrizeClaimed(gameId, player, $50)

2. Winner: withdraw(USD0, $50) → GameVault
├─ Deduct: _balances[player][USD0] -= $50
├─ Transfer: IERC20(USD0).transfer(player, $50)
└─ Emit: Withdrawn(player, USD0, $50)

Jackpot Flow (Accumulation + Distribution)

ACCUMULATION (per game):
1. Game A Settlement:
└─→ _jackpotBalances[jackpotId][USD0] += $0.50 (5% of $10 entry)

2. Game B Settlement:
└─→ _jackpotBalances[jackpotId][USD0] += $0.50

3. Game C Settlement:
└─→ _jackpotBalances[jackpotId][USD0] += $0.50

Current pool: $1.50

DISTRIBUTION (when triggered):
4. Game D has jackpot winner (oracle determined):
└─→ GameVault.executeSettlement(..., jackpotWinners=[0xWinner])
├─ Calculate: jackpotClaim = $1.50 × 50% = $0.75 (claimPercentageBPS)
├─ Deduct: _jackpotBalances[jackpotId][USD0] -= $0.75
├─ Store: _unclaimedJackpots[gameId][winner] = $0.75
└─ Remaining pool: $0.75 (rolls over)

5. Winner: claimJackpot(gameId, $0.75, proof) → GameVault
├─ Validate: MerkleProof.verify(proof, _jackpotRoots[gameId], leaf)
├─ Mark: _jackpotClaimed[gameId][player] = true
├─ Credit: _balances[player][USD0] += $0.75
└─ Emit: JackpotClaimed(gameId, player, $0.75)

Refund Flow (Abandoned Games)

1. Game timeout reached (no consensus):
├─ Anyone: abandonGame() → GameInstance
├─ State: ACTIVE → ABANDONED
└─ Emit: GameAbandoned(gameId, timestamp)

2. Player: refund via Vault
├─ Call: GameInstance.getRefundAmount(player)
│ └─→ Returns: entries × wagerAmount ($10 for 10 entries)
├─ Vault: processRefund(game, player)
│ ├─ Deduct: _gameBalances[game] -= $10
│ ├─ Credit: _balances[player][USD0] += $10
│ └─ Emit: RefundProcessed(gameId, player, $10)
└─ Player: withdraw(USD0, $10)

🔄 Operational Workflows

Deploying a New Game

Actor: Authorized Server Trust: Server must be whitelisted by GameMaster owner

// Step 1: Generate unique game ID
const gameId = keccak256(abi.encode(
templateId,
wagerToken,
wagerAmount,
host,
jackpotId,
timestamp,
nonce
));

// Step 2: Deploy via GameMaster
const tx = await gameMaster.deployGame(
templateId, // "ARENA_BTC_10S"
gameId, // Unique identifier
USD0_ADDRESS, // Wager token
ethers.parseUnits("1", 6), // $1 per entry
ethers.ZeroAddress, // No host
JACKPOT_ID, // Global BTC jackpot
SERVER_ADDRESS, // Authorized server
[JUDGE1, JUDGE2, JUDGE3, JUDGE4, JUDGE5] // Oracle judges
);

// Step 3: Compute deterministic address (CREATE2)
const gameAddress = computeCreate2Address(
gameMaster.address,
gameId,
gameInstanceBytecode
);

// Step 4: Vault automatically authorizes game
// (GameMaster calls vault.authorizeGame() during deployment)

// Result: Game is live and accepting entries

Processing a Game Settlement

Actor: Oracle Judges (3-of-5 consensus) Trust: Judges must agree on game outcome

// OFF-CHAIN: Server computes final game state
const settlement = {
totalEntries: 1000,
winners: [0xPlayer1, 0xPlayer2, ...], // Top 20%
scores: [5000, 4800, ...], // Prediction Points
jackpotWinners: [0xPlayer1], // Achieved 7+ connection
prizesUSD: [127, 80, 50, ...], // Prize amounts
jackpotUSD: [1500] // Jackpot claim
};

// Generate Merkle trees
const prizeTree = new MerkleTree(
winners.map((addr, i) => keccak256(abi.encode(addr, prizes[i])))
);
const jackpotTree = new MerkleTree(
jackpotWinners.map((addr, i) => keccak256(abi.encode(addr, jackpots[i])))
);

// STEP 1: Judge 1 submits
await gameInstance.connect(judge1).submitSettlement(
settlement.totalEntries,
settlement.winners,
settlement.scores,
prizeTree.root,
jackpotTree.root,
settlement.jackpotWinners
);
// → Vote count: 1/3

// STEP 2: Judge 2 submits (agrees)
await gameInstance.connect(judge2).submitSettlement(...);
// → Vote count: 2/3

// STEP 3: Judge 3 submits (consensus reached!)
await gameInstance.connect(judge3).submitSettlement(...);
// → Vote count: 3/3 ✅
// → AUTO-EXECUTES: gameInstance._executeSettlement()
// ╰─→ vault.executeSettlement(gameId, roots, fees, ...)
// ├─ Stores prize Merkle root
// ├─ Distributes platform/host fees
// ├─ Adds to jackpot pool
// ├─ Distributes jackpot to winners
// └─ Game state → COMPLETED

// STEP 4: Winners claim prizes
const proof = prizeTree.getProof(player1Leaf);
await vault.connect(player1).claimPrize(gameId, 127, proof);
// → player1 balance credited with $127

Emergency Pause Scenario

Actor: Contract Owner Trust: Owner has emergency circuit breaker

// SCENARIO: Exploit detected in game logic

// Step 1: Owner pauses vault
await vault.connect(owner).pause();
// → Deposits: BLOCKED
// → Game entries: BLOCKED
// → Settlements: BLOCKED
// → Withdrawals: STILL WORK ✅

// Step 2: Investigate issue
// → Review game contracts
// → Identify vulnerability
// → Prepare fix (if needed)

// Step 3A: If GameMaster needs upgrade
await gameMaster.connect(owner).upgradeTo(newImplementation);
// → Existing games: UNAFFECTED (immutable clones)
// → New games: Deploy with fixed logic

// Step 3B: If specific game compromised
await vault.connect(owner).deauthorizeGame(compromisedGame);
// → Game can no longer settle
// → Players can claim refunds after timeout

// Step 4: Resume operations
await vault.connect(owner).unpause();
// → Normal operations restored
// → Player funds: NEVER AT RISK

📈 Scalability & Gas Optimization

Minimal Clone Pattern (EIP-1167)

Why: Deploying full GameInstance contracts is prohibitively expensive.

// Traditional deployment: ~2M gas per game
GameInstance game = new GameInstance(...);

// Minimal clone: ~45K gas per game (98% reduction)
GameInstance game = GameInstance(
Clones.cloneDeterministic(implementation, gameId)
);

Benefits:

  • ✅ Cheap game deployment ($0.001 vs $0.050 per game)
  • ✅ Deterministic addresses (CREATE2)
  • ✅ Same security guarantees
  • ✅ Immutable logic per game

Packed Storage

GameInstance bitfield packing:

// Instead of 4 separate SLOAD operations:
GameState state; // 1 byte
bool depositsOpen; // 1 byte
uint64 gameStartTime; // 8 bytes
uint64 gameEndTime; // 8 bytes

// Pack into single uint256 (1 SLOAD):
uint256 _packedMutableState;

Gas savings: ~15K per state read (3 SLOADs → 1 SLOAD)

Merkle-Based Prize Distribution

Problem: Looping over 1000 winners costs 24M gas.

// Traditional Pattern (PUSH): 24M gas for 1000 winners
for (uint i = 0; i < winners.length; i++) {
_balances[winners[i]] += prizes[i]; // 20K gas × 1000
}

// Merkle Pattern (PULL): 180K gas for settlement + 50K per claim
bytes32 prizeRoot = merkleRoot; // Store once
// Winners claim individually with proofs

Benefits:

  • ✅ Constant settlement gas (O(1) vs O(n))
  • ✅ Winners pay their own claim gas
  • ✅ No limit on winner count
  • ✅ Unclaimed prizes tracked accurately

Aggregated Getters (Aave V3 Pattern)

// Instead of 8 separate eth_call:
uint96 wagerAmount = game.wagerAmount();
address token = game.wagerToken();
address host = game.host();
// ... 5 more calls

// Single aggregated call:
(
wagerAmount,
token,
host,
...
) = game.getImmutableConfig(); // 1 eth_call

Benefits:

  • ✅ 90% reduction in RPC overhead
  • ✅ Single SLOAD for packed structs
  • ✅ Faster off-chain queries

⚠️ Risk Analysis & Mitigations

Identified Risks

RiskSeverityImpactMitigation
Oracle CollusionHIGHFraudulent settlements3-of-5 threshold, timeout failsafe
GameMaster Upgrade ExploitMEDIUMConfig manipulationCannot affect live games
Vault Pause AbuseMEDIUMEntry denialWithdrawals always work
Merkle Proof ForgeryLOWFalse prize claimsCryptographic guarantee
Accounting DiscrepancyMEDIUMBalance mismatchAuto-pause + syncTokenAccounting()

Security Measures

1. Oracle Security

3-of-5 Judge Requirement:
✅ Requires 60% consensus
✅ No single point of failure
✅ Judges are independent servers
✅ Timeout prevents censorship (anyone can call abandonGame())

2. Upgrade Safety

GameMaster Upgradeable:
✅ UUPS pattern (not transparent proxy)
✅ Only affects NEW game deployments
✅ Existing games are immutable clones
✅ Storage layout must be preserved

3. Fund Custody

GameVault Immutable:
✅ NO upgrade capability
✅ NO admin withdrawal function
✅ Pause ONLY stops entries (not withdrawals)
✅ Emergency deauthorization available

4. Accounting Integrity

Triple-Entry Accounting:
✅ Player balances tracked
✅ Game balances tracked
✅ Jackpot balances tracked
✅ Auto-pause on discrepancy
✅ Public reconciliation function

🎯 User Journey Examples

Scenario 1: Happy Path Winner

1. Alice deposits $100 USD0 to GameVault
├─ Balance: $100

2. Alice enters Game #123 (10 entries × $1)
├─ Balance: $90
├─ Game balance: $10 (locked)

3. Alice plays game, achieves high score
├─ Off-chain: Server records final score

4. Judges submit settlement (3-of-5 consensus)
├─ Alice identified as 1st place winner
├─ Prize: $127 (127x return)
├─ GameVault stores Merkle root

5. Alice claims prize with Merkle proof
├─ Balance: $90 + $127 = $217
├─ Proof verified cryptographically

6. Alice withdraws $217
├─ Balance: $0
├─ Receives $217 USD0 to wallet

Scenario 2: Jackpot Winner

1. Bob enters Game #456 with BTC jackpot
├─ Entry fee: $1
├─ Jackpot contribution: $0.05 (5%)
├─ Current jackpot pool: $1,500

2. Bob achieves 7+ connected platforms
├─ Qualifies for jackpot trigger
├─ Oracle includes Bob in jackpotWinners[]

3. Settlement executes with jackpot distribution
├─ 50% of pool claimed: $750
├─ Deducted from _jackpotBalances[btcJackpot]
├─ Stored as unclaimed for Bob
├─ Remaining pool: $750 (rolls over)

4. Bob claims jackpot with proof
├─ Balance: previous + $750
├─ Jackpot marked as claimed

5. Bob withdraws jackpot winnings
├─ Receives $750 USD0 to wallet

Scenario 3: Oracle Failure (Timeout Refund)

1. Carol enters Game #789
├─ Balance: $100 → $90
├─ Game balance: $10

2. Game completes but judges disagree
├─ Judge 1 votes for Settlement A
├─ Judge 2 votes for Settlement B
├─ Judge 3 votes for Settlement A (2/3 for A)
├─ Judge 4 votes for Settlement B (2/3 for B)
├─ Judge 5 offline (no vote)
├─ NO CONSENSUS reached

3. Timeout period elapses (5 minutes)
├─ Game still in ACTIVE state
├─ consensusTimeout exceeded

4. Anyone calls abandonGame()
├─ State: ACTIVE → ABANDONED
├─ All entries now refundable

5. Carol claims refund
├─ GameInstance.getRefundAmount(carol) = $10
├─ GameVault.processRefund(game, carol)
├─ Balance: $90 + $10 = $100 (fully restored)

6. Carol withdraws original deposit
├─ NO LOSS from oracle failure ✅

📚 Technical Reference

Contract Addresses (HyperEVM)

Network: HyperEVM (Chain ID: 998)

GameMaster (Proxy): 0x... [DEPLOYED]
GameMaster (Implementation): 0x... [DEPLOYED]
GameVault: 0x... [DEPLOYED]
GameInstance (Implementation): 0x... [DEPLOYED]

Tokens:
├─ USD0: 0xd0d06b9d9a9b0c0b97169293f6c72f0f7e50bb02
└─ [Additional tokens configured per vault]

Jackpots:
├─ JACKPOT_BTC: 0x... [bytes32]
├─ JACKPOT_ETH: 0x... [bytes32]
└─ [Additional jackpots per GameMaster registry]

Key Interfaces

interface IGameMaster {
function deployGame(...) external returns (address);
function createTemplate(...) external;
function createJackpot(...) external;
function authorizeServer(...) external;
}

interface IGameVault {
function deposit(address token, uint256 amount) external;
function withdraw(address token, uint256 amount) external;
function enterGame(address game, uint256 entries) external;
function claimPrize(bytes32 gameId, uint256 amount, bytes32[] proof) external;
function executeSettlement(...) external;
}

interface IGameInstance {
function registerEntries(address player, uint256 count) external;
function submitSettlement(...) external;
function abandonGame() external;
function getRefundAmount(address player) external view returns (uint256);
}

Event Signatures

// GameMaster
event TemplateCreated(bytes32 indexed templateId, ...);
event GameDeployed(bytes32 indexed gameId, address indexed game, ...);
event JackpotCreated(bytes32 indexed jackpotId, address token);

// GameVault
event Deposited(address indexed player, address indexed token, uint256 amount);
event Withdrawn(address indexed player, address indexed token, uint256 amount);
event PlayerEnteredGame(bytes32 indexed gameId, address indexed player, ...);
event PrizeClaimed(bytes32 indexed gameId, address indexed player, uint256 amount);
event JackpotClaimed(bytes32 indexed gameId, address indexed player, uint256 amount);

// GameInstance
event EntryRegistered(bytes32 indexed gameId, uint256 indexed entryId, ...);
event SettlementVoted(address indexed judge, bytes32 indexed hash);
event SettlementExecuted(bytes32 indexed gameId, bytes32 indexed consensusHash);
event GameAbandoned(bytes32 indexed gameId, uint256 timestamp);

Storage Layout

GameMaster (UUPS Proxy):

Slot 0: _initialized, _initializing (OpenZeppelin)
Slot 1-50: OpenZeppelin standard storage
Slot 51: operator (address)
Slot 52: vault (address)
Slot 53: wagerImplementation (address)
Slot 54: _authorizedServers (mapping)
Slot 55: _templates (mapping)
Slot 56: _jackpots (mapping)
Slot 57: _games (mapping) [DEPRECATED - removed in CREATE2 migration]

GameVault (Immutable):

Slot 0: _initialized, _paused (OpenZeppelin)
Slot 1-50: OpenZeppelin standard storage
Slot 51: gameMaster (address) [immutable]
Slot 52: _balances (mapping)
Slot 53: _gameBalances (mapping)
Slot 54: _jackpotBalances (mapping)
Slot 55: _tokenConfigs (mapping)
Slot 56: _prizeRoots (mapping)
Slot 57: _jackpotRoots (mapping)
Slot 58: _authorizedGames (mapping)

GameInstance (Minimal Clone):

[Implementation storage - shared by all clones]
Slot 0: _initialized
Slot 1: _immutableConfig (struct)
Slot 2: _packedMutableState (bitfield)
Slot 3: judges (address[])
Slot 4: settlementVotes (mapping)
Slot 5: judgeVotes (mapping)
Slot 6: _hasEntry (mapping)
Slot 7: _playerEntries (mapping)
Slot 8: _largestConnections (mapping)

🎓 Key Takeaways

For Developers

  1. Immutable Games: GameInstance clones are frozen at deployment - settlements cannot be manipulated
  2. Self-Coordination: No external coordinator needed - games auto-settle when judges reach consensus
  3. Merkle Efficiency: Prize distribution is O(1) gas regardless of winner count
  4. Upgrade Safety: GameMaster upgrades cannot affect live games (only new deployments)

For Security Auditors

  1. Trust Boundaries: Clear separation between trusted (owner) and trustless (user) operations
  2. Oracle Decentralization: 3-of-5 judges with timeout failsafe prevents censorship
  3. Fund Safety: GameVault immutability ensures no admin backdoors to player funds
  4. Accounting Integrity: Triple-entry system with automatic reconciliation and pause

For Product Managers

  1. Scalability: Minimal clone pattern enables thousands of games at low cost
  2. Flexibility: GameMaster upgrades allow template improvements without vault redeployment
  3. User Safety: Timeout refunds protect players from oracle failures
  4. Transparency: All prizes cryptographically provable via Merkle proofs

DOCUMENTATION COMPLETE

Last Updated: Based on Smart_Contract_System_Specification.md Status: FINALIZED IMPLEMENTATION Confidence: HIGH (Derived from actual Solidity code)