Skip to content

Architecture Overview

FairWins is three deployable pieces — Solidity contracts, a React SPA, and static-serving infrastructure — with no application backend. Everything "server-side" happens on-chain, on IPFS, or at the CDN edge.

System context

flowchart TB
    subgraph User ["User's device"]
        Wallet[Wallet<br/>MetaMask / WalletConnect]
        SPA[FairWins SPA<br/>React + Vite + wagmi/ethers]
        Wallet <--> SPA
    end

    subgraph Edge ["Serving infrastructure"]
        CF[Cloudflare<br/>DNS + proxy] --> CR[Cloud Run<br/>nginx serving static SPA]
    end

    subgraph Polygon ["Polygon (137 mainnet / 80002 Amoy)"]
        WR[WagerRegistry]
        MM[MembershipManager]
        SG[SanctionsGuard]
        KR[KeyRegistry]
        PA[PolymarketOracleAdapter]
        CDF[ChainlinkDataFeedOracleAdapter]
        CFN[ChainlinkFunctionsOracleAdapter]
        UMA[UMAOptimisticOracleV3Adapter]
    end

    subgraph External ["External systems"]
        PM[Polymarket CTF]
        CL[Chainlink feeds / DON]
        UMAOO[UMA Optimistic Oracle V3]
        CHA[Chainalysis sanctions oracle]
        IPFS[(IPFS via Pinata)]
        GAMMA[Polymarket Gamma API]
    end

    SPA -- "loads app" --> CF
    SPA -- "JSON-RPC reads/writes" --> WR
    SPA --> MM
    SPA --> KR
    SPA -- "encrypted wager terms" --> IPFS
    SPA -- "market search" --> GAMMA

    WR -- "tier & rate limits" --> MM
    WR -- "screening" --> SG
    WR -- "outcomes" --> PA & CDF & CFN & UMA
    SG --> CHA
    PA --> PM
    CDF --> CL
    CFN --> CL
    UMA --> UMAOO

Contract layer

The active contracts live under contracts/ (everything in contracts-archive/ is reference-only — never import or deploy it).

Contract Directory Responsibility
WagerRegistry contracts/wagers/ Wager lifecycle + stake escrow: create, accept, resolve, draw, claim, refund
MembershipManager contracts/access/ Tiered, time-bound memberships (USDC); monthly & concurrent creation limits
SanctionsGuard contracts/access/ Non-bypassable screening against the Chainalysis oracle + operator deny list
KeyRegistry contracts/privacy/ Public encryption keys for end-to-end encrypted wager terms
PolymarketOracleAdapter contracts/oracles/ Reads outcomes from Polymarket's Conditional Token Framework
ChainlinkDataFeedOracleAdapter contracts/oracles/ Resolves price-threshold conditions against Chainlink feeds
ChainlinkFunctionsOracleAdapter contracts/oracles/ Resolves custom off-chain computations via Chainlink Functions
UMAOptimisticOracleV3Adapter contracts/oracles/ Resolves assertions via UMA Optimistic Oracle V3

All four oracle adapters implement the same IOracleAdapter interface (isConditionResolved, getOutcome, condition metadata), so WagerRegistry resolves any oracle-typed wager through a uniform autoResolveFromOracle / autoResolveFromPolymarket path. See Smart Contracts for per-contract detail and the lifecycle state machine.

Frontend layer

frontend/ is a Vite-built React SPA. Key structural pieces:

Piece Path Role
Routing src/App.jsx / landing, /app dashboard, /wallet account center, /friend-market/accept QR/deep-link acceptance, /admin role-gated panel, /terms /risk /privacy legal pages
Wallet state src/hooks/useWalletManagement.js, wagmi config Connection, roles, network switching (137 ↔ 80002)
Wager creation src/hooks/useFriendMarketCreation.js, components/fairwins/FriendMarketsModal.jsx Membership check → USDC approval → createWager → encrypted IPFS upload
Wager data src/contexts/FriendMarketsContext.jsx, src/data/wagers/ Per-chain cache; reads via direct RPC (EventsSource) with optional subgraph source
Encryption src/hooks/useEncryption.js Wallet-signature-derived keys; envelope encryption of terms; KeyRegistry lookups
Constants src/constants/wagerDefaults.js Canonical resolution-type enum, status names, stake/deadline defaults
Addresses src/config/contracts.js Per-chain contract addresses — generated, do not hand-edit

Data flow

flowchart LR
    subgraph Reads
        RPC[Polygon JSON-RPC] -->|getWager / getUserWagers / events| Ctx[FriendMarketsContext cache]
        SUB[Subgraph<br/>optional, legacy v1] -.fallback only.-> Ctx
        Ctx --> UI[Dashboard / My Wagers]
    end
    subgraph Writes
        UI2[User action] --> Approve[ERC-20 approve] --> Call[WagerRegistry call] --> Pending[localStorage pending-tx<br/>resume on reload]
    end
  • Reads go straight to the chain with ethers.js (getWager, getUserWagers, event scans). The Graph subgraph under subgraph/ indexes the legacy v1 FriendGroupMarketFactory, not WagerRegistry, so the live app does not depend on it.
  • Writes are wallet transactions. In-flight transactions are tracked in localStorage so a page reload can resume a half-finished creation flow.
  • Encrypted terms never touch a server: the SPA encrypts client-side, pins to IPFS via Pinata, and stores the CID in the wager's metadataUri.

Contract address sync

Deployment records in deployments/<network>-chain<id>-v2.json are the source of truth. After a deploy, regenerate the frontend config:

npm run sync:frontend-contracts -- --network polygon --chainId 137

This rewrites the per-chain blocks in frontend/src/config/contracts.js.

Serving infrastructure

flowchart LR
    Dev[git push] --> CB[Cloud Build<br/>cloudbuild.yaml]
    CB -->|"docker build (Vite build → nginx image)"| AR[Artifact Registry]
    AR -->|gcloud run deploy| CR[Cloud Run<br/>nginx :8080]
    CR --> CF[Cloudflare] --> Users[fairwins.app]
    SM[Secret Manager<br/>PINATA_JWT, ORIGIN_LOCK_SECRET] --> CR
  • Build: multi-stage Dockerfile — Node builds the Vite bundle, nginx serves it. Public configuration (VITE_NETWORK_ID, VITE_RPC_URL, VITE_IPFS_GATEWAY, WalletConnect project ID) is baked in as build args; secrets are injected at runtime from Secret Manager, never into the bundle.
  • nginx (frontend/nginx.conf): SPA fallback routing, immutable caching for hashed assets, no-cache HTML, and security headers — CSP allowing only the required origins (WalletConnect relay, IPFS gateways, Polymarket Gamma API, Cloudflare Insights), HSTS, and a Permissions-Policy that scopes the camera to self for the in-app QR scanner.
  • Cloudflare fronts Cloud Run; an origin-lock secret ensures traffic reaches Cloud Run only via Cloudflare.

This footprint is intentionally fixed: SPA + nginx on Cloud Run, contracts, IPFS, Cloudflare, and Cloud Logging. New features must not introduce an application backend.

Networks and deployments

Network Chain ID Purpose Record
Polygon mainnet 137 Production deployments/polygon-chain137-v2.json
Polygon Amoy 80002 Testnet deployments/amoy-chain80002-v2.json
Hardhat 1337 Local development generated locally
Mordor (ETC) 63 Testnet (Ethereum Classic, v2 core-only) deployments/mordor-chain63-v2.json

Contracts deploy deterministically via the Safe Singleton Factory with a versioned salt prefix (FairWins-P2P-v2.0-) — see Singleton Deployment Patterns.

Mordor (Ethereum Classic testnet)

Mordor runs a core-only v2 deployment (Spec 015): WagerRegistry, MembershipManager, KeyRegistry, and an enforced SanctionsGuard. Ethereum Classic has no Polymarket, Chainlink, or UMA infrastructure, so the oracle adapters are not deployed and WagerRegistry runs with a zero Polymarket adapter — only peer/designated-resolver wagers (Either/Creator/Opponent/ ThirdParty) are offered. Stakes use Classic USD (USC), the network's real fiat-backed stablecoin (no mock); swaps go through ETCswap when configured. Native gas is test ETC (faucet); the explorer is Blockscout. The legacy v1 Mordor deployment is retired — its addresses live only in version-control history. The Network tab (My Account → Network) surfaces Mordor with capability tags derived from the deployment record and operational links (explorer, faucet, Classic USD, ETCswap).

Security architecture

  • Checks-effects-interactions throughout; payouts are pull-based.
  • Role separation: DEFAULT_ADMIN_ROLE (config), GUARDIAN_ROLE (pause), ACCOUNT_MODERATOR_ROLE (freeze accounts), ROLE_MANAGER_ROLE (memberships). No role can move escrowed stakes.
  • Sanctions screening on every create/accept via SanctionsGuard.
  • CI security gates: Slither, Medusa fuzzing, and the full Hardhat suite must pass — see Security Testing.

Historical note

Earlier iterations of this repository (futarchy governance, conditional-token markets, friend-group market factories, perpetual futures) are preserved under contracts-archive/ and docs/archived/ for reference. They are not deployed, not maintained, and must never be imported by active code.