Architecture

Overview

LiquidLottery runs across two chains:

Chain
Contract
Role

Hyperliquid L1 (HyperEVM)

HyperLotteryV1 (behind UUPS proxy)

Game logic, ticket sales, settlement, claims

Base

LotteryVRFRequester

Receives draw requests, calls Chainlink VRF, sends randomness back

Communication between the two chains uses Chainlink CCIParrow-up-right (Cross-Chain Interoperability Protocol).


Contract Architecture on Hyperliquid

┌──────────────────────────────────────────────────┐
│           LiquidLotteryProxy (ERC-1967)           │
│  ┌────────────────────────────────────────────┐  │
│  │         HyperLotteryV1 (implementation)     │  │
│  │  ┌──────────────┐  ┌───────────────────┐   │  │
│  │  │ LotteryMath   │  │  LotteryViews     │   │  │
│  │  │ (library)     │  │  (library)        │   │  │
│  │  │               │  │                   │   │  │
│  │  │ • buyTickets  │  │ • view functions  │   │  │
│  │  │ • draw logic  │  │ • admin config    │   │  │
│  │  │ • settlement  │  │ • timelocks       │   │  │
│  │  │ • claiming    │  │ • emergency ops   │   │  │
│  │  │ • CCIP send   │  │                   │   │  │
│  │  └──────────────┘  └───────────────────┘   │  │
│  └────────────────────────────────────────────┘  │
│              Shared storage (ERC-7201)            │
│         keccak256("hyperlottery.v1.main.storage") │
└──────────────────────────────────────────────────┘
  • LiquidLotteryProxy — minimal ERC-1967 proxy that holds all state.

  • HyperLotteryV1 — thin UUPS-upgradeable wrapper that delegates to two external libraries.

  • LotteryMath — core game logic: ticket purchasing, CCIP messaging, settlement, prize claiming.

  • LotteryViews — read-only view functions, admin configuration setters, and timelock enforcement.

All three contracts share the same storage layout via an ERC-7201 namespaced storage slot.

Why Libraries?

Hyperliquid enforces a 13 514-byte runtime bytecode limit. Splitting logic into external libraries keeps each deployed contract within this limit.


Cross-Chain Draw Flow

Retry Mechanism

If the CCIP send on Base fails (e.g. insufficient ETH for fees), the random word is persisted in pendingRandomWords[roundId]. The owner can top up the contract and call retryFulfillViaCCIP(roundId) to resend.


Random Number Generation

The VRF provides a single uint256 random word. The contract derives all drawn numbers deterministically from it:

Because the seed comes from Chainlink VRF, the outcome is tamper-proof and independently verifiable on-chain.


Round Lifecycle

State
Description

OPEN

Players can buy tickets. Pools accumulate.

DRAWING

CCIP message in flight. No purchases allowed.

DRAWN

Randomness applied. Ready for settlement.

SETTLED

Winners counted, payouts calculated, next round opened.

Timing

Parameter
Default
Description

upkeepInterval

86 400 s (1 day)

Minimum time between draws

DRAW_GRACE_PERIOD

300 s (5 min)

After this, anyone can trigger a public draw

EMERGENCY_CANCEL_DELAY

24 h

After this, anyone can cancel a stuck draw


Settlement

Settlement iterates over all tickets in the round, counts matching winners, and calculates per-winner payouts.

  • Default batch size: 500 tickets.

  • Rounds with ≤ 500 tickets: single settleRound() call.

  • Larger rounds: multiple settleRoundBatch() calls until all tickets are processed.

After settlement:

  1. currentRound increments.

  2. Unclaimed pools roll over to the new round.

  3. The new round opens in OPEN state.


Upgrade Pattern (UUPS)

HyperLotteryV1 follows the UUPS (Universal Upgradeable Proxy Standard) pattern with a 72-hour timelock and a 1-hour execution window:

  1. Owner calls proposeUpgrade(newImplementation) — starts the 72-hour countdown.

  2. After 72 hours, upgradeToAndCall() can execute within a 1-hour window.

  3. If the window passes, the proposal expires and must be re-proposed.

During the execution window, ticket purchases are blocked to prevent state corruption.


Automation Server

A Node.js automation server (in server/) polls the contract and triggers:

  • closeBettingAndDraw() when the upkeep interval elapses.

  • settleRoundBatch() when a round is in the DRAWN state.

Configuration is read from server/.env. The server can be managed with PM2 via the root ecosystem.config.cjs.

Last updated

Was this helpful?