UnitFlow LogoUnitFlow Docs

Architecture

UnitFlow Predict is composed of four upgradeable smart contracts deployed on Arc Testnet, a Next.js 15 frontend, and a set of React hooks for reading on-chain state.

Contract System

All four contracts use OpenZeppelin's TransparentUpgradeableProxy pattern. The proxy address is permanent — only the implementation can be upgraded by the owner.

PredictMarketFactory (proxy)
  │
  ├── deploys ──▶ PredictMarket (one per question)
  │                   │
  │                   └── routes fees ──▶ FeeDistributor (proxy)
  │
  └── references ──▶ PredictOracle (proxy)
                          │
                          └── calls resolveMarket() on PredictMarket

PredictMarketFactory

The entry point for market creation. Validates parameters, collects the creation fee and initial liquidity from the creator, deploys a new PredictMarket contract via CREATE2 (deterministic address from a bytes32 market ID), seeds the market's liquidity pools, and registers the address in getAllMarkets().

  • Maintains the canonical list of all deployed markets
  • Enforces minimum liquidity (10 USDC/EURC) and valid resolution dates
  • Charges a 5 USDC/EURC creation fee routed to FeeDistributor
  • Supports pausing all new market creation in an emergency

PredictMarket

One contract per question. Holds the YES and NO liquidity pools, tracks every user's share position, enforces staking rules, and executes payouts. It is the only contract users interact with directly when staking or claiming.

  • Stores yesPool, noPool, totalStaked
  • Tracks UserPosition per address: yesShares, noShares, totalStaked, claimed
  • Maintains a _participants array for enumeration (leaderboard)
  • Enforces MIN_STAKE = 1e6 (1 USDC), MAX_STAKE_BPS = 1000 (10% of pool)
  • Only accepts calls from resolver (the oracle) for resolution

PredictOracle

A modular resolution authority with a mandatory 24-hour dispute window. Separates the act of proposing an outcome from finalizing it, giving the community time to dispute incorrect resolutions.

  • Only the owner or authorizedResolvers can propose outcomes
  • Anyone can call finalizeResolution() after the dispute window
  • Disputed resolutions require an owner override via overrideResolution()

FeeDistributor

Receives all protocol fees (1% stake fee + 0.5% claim fee) and splits them ondistributeFees():

ShareDestination
60%UNIT buyback-and-burn
20%LP reward pool
20%Treasury

Before UNIT token launches, the 60% buyback share accumulates in apendingBuyback mapping per currency. The owner callsexecuteBuyback() once UNIT is live.

AMM Pricing Model

Odds are determined by a constant-product AMM. The implied probability of YES at any moment is:

yesPrice = noPool  / (yesPool + noPool)   // P(YES)
noPrice  = yesPool / (yesPool + noPool)   // P(NO)

Prices are expressed in basis points (0–10,000). When a user stakes, the pool they chose grows, which lowers the odds for that side and raises them for the other.

Share Issuance

Shares represent a proportional claim on the winning pool. They are issued against theopposite pool to reward early stakers on the less-popular side:

// Stake YES
netAmount = amount - protocolFee          // 1% fee deducted
shares    = netAmount × noPool / yesPool  // more shares when noPool is large

// Stake NO
netAmount = amount - protocolFee
shares    = netAmount × yesPool / noPool

Payout Calculation

After resolution, each winner's payout is proportional to their share of the winning pool:

grossPayout = winningShares × totalPool / totalWinningShares
claimFee    = grossPayout × 0.5%
netPayout   = grossPayout - claimFee

totalPool is the sum of both pools (all money staked). Winners split the entire pool proportionally — losers receive nothing.

Frontend Architecture

The frontend is built with Next.js 15 (App Router), TypeScript, Tailwind CSS, wagmi v2, viem, and TanStack Query. All on-chain reads use wagmi's useReadContractswith multicall batching — a single RPC round-trip fetches data for all markets.

Data Flow

useOnChainMarkets
  └── factory.getAllMarkets()          → address[]
  └── market.getMarketInfo()          → metadata (batched)
  └── market.yesPool / noPool / ...   → pool state (batched, 15s refresh)

useOnChainLeaderboard
  └── market.getParticipants()        → address[] per market
  └── market.getUserPosition(addr)    → position per user×market (batched)

usePortfolio
  └── market.getUserPosition(wallet)  → position per market (batched)

useUserPosition
  └── market.getUserPosition(wallet)  → single market position
  └── market.estimatePayout(wallet)   → live payout estimate

State Management

A Zustand store (predictStore) holds UI state: active filter/sort/category, search query, stake drawer open state, and optimistic odds (applied immediately after a stake confirms, before the next RPC refresh).

Cache Invalidation

After any write transaction (stake, claim, create market), the hook callsqueryClient.invalidateQueries() with no filter. This busts wagmi's entire internal read cache, causing all useReadContract hooks to refetch on the next render. Pool state typically updates within one block (~1 second on Arc).