Skip to content

0xTemplar/GhostMarket

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

63 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

GhostMarket

FHEVM ERC-7984 Sepolia Privy

GhostMarket

Confidential Dark Pool Prediction Market
Every deposit, every bet, every payout β€” a ciphertext from browser to blockchain.
The chain knows you bet. It does not know on what, in what size, or for how much.

Β  Β  Β  Β  Β  Β 


Table of Contents

GhostMarket hero


πŸ‘» What It Is

GhostMarket is a binary prediction market where no financial value ever exists as plaintext on-chain. Not deposits. Not pool totals. Not bet sizes. Not payouts. Every value is an euint64 ciphertext processed by the Zama FHEVM coprocessor on Ethereum Sepolia.

The ERC-7984 cUSDC token holding all collateral literally cannot answer the question "how much does this user own?" The oracle signs over an encrypted handle, not a uint256. Two users can place bets in the same window and neither can infer the other's size from a price delta, because the price is frozen until the window closes.

Combined with a sealed-bid window mechanism, a 4-agent AI oracle quorum (3-of-4 majority; seven agent personas defined in code, configurable via ACTIVE_ORACLE_AGENTS) grounded in live exchange feeds, and Privy walletless login, this is what Polymarket would look like if the chain refused to publish your size.

Problems Solved

Problem Solution Mechanism
FHE encryption of every value euint64 ciphertexts in calldata, storage, events, and settlements
Cryptographic ACL β€” no master key Zama KMS re-encryption; nobody can read another user's position
Sealed-bid windows Pool ACL withheld during window; all bets revealed atomically
4-agent AI quorum (3-of-4) Each active agent fetches a live CEX API, reasons via gpt-4o-mini, votes independently
Settlement over encrypted handle Oracle signs keccak(user, marketId, amountHandle, nonce, expiry)
ERC-7984 cUSDC custody All balances and locks are euint64 in GhostVaultV2
Privy embedded wallets Google / email / passkey login β€” no mnemonic, no extension

πŸ— Architecture

graph TB
    subgraph Browser["🌐 Browser"]
        direction LR
        FE["<b>Next.js 15</b><br/>React 19 Β· Tailwind v4 Β· viem"]
        SDK["<b>@zama-fhe/relayer-sdk</b><br/>encrypt(amount) β†’ (handle, ZKPoK)"]
        PV["<b>Privy v3</b><br/>Embedded EOA Β· Google/Email/Passkey"]
    end

    subgraph Contracts["⛓️ Ethereum Sepolia (chainId 11155111)"]
        direction LR
        GM["<b>GhostMarket</b><br/>Metadata registry<br/>Lifecycle entry point"]
        EAMM["<b>GhostEAMM</b><br/>Encrypted AMM<br/>YES/NO euint64 pools<br/>Sealed-bid windows"]
        VLT["<b>GhostVaultV2</b><br/>ERC-7984 cUSDC custody<br/>EIP-712 settlement"]
        CUSDC["<b>cUSDC Mock</b><br/>ERC-7984 confidential token<br/>Zama canonical Sepolia"]
    end

    subgraph Oracle["πŸ€– Oracle Service (Node.js Β· port 8092)"]
        direction LR
        AGT["<b>4 Active AI Agents</b> (7 defined)<br/>Cipher Β· Specter Β· Wraith Β· Phantom<br/>+ Shade Β· Echo Β· Vex if ACTIVE_ORACLE_AGENTS=7"]
        WATCHER["<b>Sealed-Window Watcher</b><br/>Settle β†’ Zama KMS decrypt<br/>β†’ publishWindowPrice"]
        SIGNER["<b>Oracle Signer</b><br/>EIP-712 Claim(amountHandle)"]
    end

    FE --> SDK
    FE --> PV
    SDK --> EAMM
    SDK --> VLT
    GM --> EAMM
    VLT --> CUSDC
    AGT --> GM
    AGT --> SIGNER
    WATCHER --> EAMM
    SIGNER --> VLT

    style Browser fill:transparent,stroke:#7c3aed,color:#333
    style Contracts fill:transparent,stroke:#3b82f6,color:#333
    style Oracle fill:transparent,stroke:#f59e0b,color:#333
    style FE fill:#7c3aed,stroke:#a78bfa,color:#fff
    style SDK fill:#412891,stroke:#7c3aed,color:#fff
    style PV fill:#8b5cf6,stroke:#a78bfa,color:#fff
    style GM fill:#64748b,stroke:#94a3b8,color:#fff
    style EAMM fill:#3b82f6,stroke:#60a5fa,color:#fff
    style VLT fill:#22c55e,stroke:#4ade80,color:#000
    style CUSDC fill:#06b6d4,stroke:#22d3ee,color:#000
    style AGT fill:#f59e0b,stroke:#fbbf24,color:#000
    style WATCHER fill:#ef4444,stroke:#f87171,color:#fff
    style SIGNER fill:#f59e0b,stroke:#fbbf24,color:#000
Loading

End-to-End Payout Flow

graph LR
    A["πŸ‘€ User encrypts<br/>amount in browser"] --> B["πŸ”’ cUSDC.setOperator<br/>+ GhostVaultV2.deposit"]
    B --> C["🏦 euint64 balance<br/>in vault"]
    C --> D["🎯 lockForBet +<br/>placeBet (FHE.add)"]
    D --> E["πŸͺŸ Sealed window<br/>pool frozen"]
    E --> F["⏰ Window expires<br/>β†’ PriceRevealed"]
    F --> G["πŸ€– Oracle quorum<br/>resolveMarket on-chain"]
    G --> H["✍️ Oracle signs<br/>Claim(amountHandle)"]
    H --> I["πŸ’Έ confidentialTransfer<br/>(user, amountHandle)"]

    style A fill:#f8fafc,stroke:#334155,color:#000
    style B fill:#412891,stroke:#7c3aed,color:#fff
    style C fill:#22c55e,stroke:#4ade80,color:#000
    style D fill:#3b82f6,stroke:#60a5fa,color:#fff
    style E fill:#06b6d4,stroke:#22d3ee,color:#000
    style F fill:#06b6d4,stroke:#22d3ee,color:#000
    style G fill:#f59e0b,stroke:#fbbf24,color:#000
    style H fill:#f59e0b,stroke:#fbbf24,color:#000
    style I fill:#22c55e,stroke:#4ade80,color:#000
Loading

πŸ” Privacy Model

On-chain β€” anyone can read FHE-encrypted (euint64 ciphertext handle only)
Market ID, title, category, expiry Vault deposit amount
User EOA address Vault balance per user
Bet side (YES / NO) Per-market collateral lock
Market status, resolved outcome Total locked per user
Sealed-window timestamps YES pool total Β· NO pool total
Tx hashes, block numbers Per-user YES position Β· NO position
Pre-window pool snapshot
Payout amount handle

Every on-chain event has zero amount fields β€” by design:

event BetPlaced    (uint256 indexed marketId, address indexed user, bool indexed side);
event Deposited    (address indexed user);
event BetLocked    (address indexed user, bytes32 indexed marketId, bool side);
event PayoutClaimed(address indexed user, bytes32 indexed marketId);

ACL β€” who can decrypt what:

Role Can read Mechanism
Own balance, own lock, own position FHE.allow(handle, msg.sender) on every write
Its own ciphertexts (for re-use in arithmetic) FHE.allowThis(handle)
Pool totals β€” only after resolution or window settlement FHE.allow(pool, resolver) in resolveMarket / settleSealedWindow
Nothing, ever No ACL entry is written for third parties

Privacy is trader-private, not authority-private. A regulator can be issued an ACL grant to specific handles via contract upgrade β€” the cryptography supports selective disclosure. This is the same shape as institutional dark pools in TradFi.


πŸ”¬ Encrypted Stack

1. ERC-7984 Confidential Vault

Layer Sepolia Address Role
Underlying (ERC-20, mintable) 0x9b5C…dFfF Faucet token β€” mint, then wrap
ERC-7984 confidential wrapper 0x7c5B…3639 What users actually deposit

deposit β€” the key insight: the vault never holds a plaintext amount at any step.

user: setOperator(vault, forever)              ← encrypted-token "approve"
user: encrypt(amount) β†’ (handle, ZKPoK)        ← plaintext gone in browser
GhostVaultV2.deposit(handle, proof):
  FHE.fromExternal(handle, proof)              ← verify ZKPoK
  FHE.allow(amount, address(cUSDC))            ← let cUSDC use this handle
  cUSDC.confidentialTransferFrom(user, vault)  ← homomorphic debit, no plaintext
  _balances[user] = FHE.add(_balances[user], transferred)

Overdraw can't be a revert β€” the EVM never sees a plaintext comparison. Instead:

ebool   ok              = FHE.ge(freeBalance, amount);
euint64 effectiveAmount = FHE.select(ok, amount, FHE.asEuint64(0));  // clamp in coprocessor

2. Encrypted AMM

GhostEAMM pool totals accumulate via FHE.add. No party has ACL access until market resolution or sealed-window settlement.

The minimum-bet guard runs entirely inside the coprocessor β€” the EVM never sees whether the submitted amount passed:

ebool   aboveMin        = FHE.gt(amount, FHE.asEuint64(MIN_BET_UNITS));
euint64 effectiveAmount = FHE.select(aboveMin, amount, FHE.asEuint64(0));

Per-user positions are ACL-scoped to the owner on every write:

_yesPositions[marketId][msg.sender] = FHE.add(..., effectiveAmount);
FHE.allowThis(_yesPositions[marketId][msg.sender]);
FHE.allow    (_yesPositions[marketId][msg.sender], msg.sender);  // user decrypts own position

After resolution the oracle gets one-shot ACL on only the winning side of one specific user via grantPositionAccess.

3. Sealed-Bid Windows

Even with encrypted bets, a continuously visible price is a side-channel: a tiny probe bet + the resulting price delta reveals the previous bet's size. Sealed windows close this.

stateDiagram-v2
    [*] --> Open: openSealedWindow β€” snapshot pool handles
    Open --> Accepting: placeBet (price display frozen at snapshot)
    Accepting --> Pending: window timer expires β€” placeBet blocked
    Pending --> Settled: oracle calls settleSealedWindow
    Settled --> Revealed: publishWindowPrice β†’ PriceRevealed event
    Revealed --> [*]: frontend animates price jump
Loading

Commit-reveal without a second user transaction. The clock is the reveal; the bets were real the whole time; nobody owes a second tx. The combined delta publishes as one PriceRevealed event β€” no participant can chart anyone else's order flow inside the window.

4. Confidential Settlement

After oracle quorum the oracle signs over the encrypted ciphertext handle, not a plaintext amount:

// GhostVaultV2 β€” CLAIM_TYPEHASH
"Claim(address user, bytes32 marketId, bytes32 amountHandle, uint256 nonce, uint256 expiry)"
//                                      ^^^^^^^^^^^^^^^^^^^
//                                      euint64 handle β€” not a uint256 amount

The vault verifies the signature and calls cUSDC.confidentialTransfer(user, amountHandle). The handle flows through homomorphically.

The plaintext payout exists only in the user's browser when they decrypt their own balance handle. It never appears in: oracle memory, vault storage, token storage, calldata, events, or any block explorer.

Replay protection: usedNonces[user][marketId][nonce]. Settlement TTL: 24 hours. Signer rotation: setSettlementSigner(newOracle).


πŸ€– Oracle Agent Quorum

Agent Source Personality Default active?
Binance Data-driven; only trusts top-tier CEX feeds Yes (1)
CoinGecko Cautious; high threshold before voting YES Yes (2)
Chainlink / CryptoCompare On-chain feeds preferred over off-chain Yes (3)
Coinbase Contrarian; stress-tests the consensus Yes (4)
Kraken Cross-reference consensus-seeker Optional (5)
OKX Volume-weighted aggregator Optional (6)
Bybit Adversarial; hunts manipulation and stale data Optional (7)

Each active agent: fetching (live API call, 6s timeout) β†’ attesting (personality-specific gpt-4o-mini prompt with the real fetched value, JSON-only {vote, reasoning}) β†’ submitted. With the default four agents, quorum is 3-of-4 (floor(N/2)+1). Set ACTIVE_ORACLE_AGENTS up to 7 to use every definition (then quorum is 4-of-7). On quorum: resolveMarket on Sepolia, settlement endpoint opens.

The Oracle Room (/oracle, ~1250 LOC) streams every agent's state, fetched price, live LLM reasoning, and on-chain tx hash via WebSocket. Every reasoning line is a real LLM response grounded in a real API call β€” nothing is pre-canned.


🧭 User Journey

Phase Action What happens on-chain
Browse /, click a market Reads public metadata + post-window PriceRevealed events
Click YES/NO β†’ Google sign-in Privy creates embedded EOA β€” no mnemonic
/vault β†’ mint β†’ wrap β†’ setOperator β†’ deposit confidentialTransferFrom; vault balance becomes euint64
Enter amount, click Place Bet (one click) lockForBet + placeBet β€” BetPlaced event has no amount
Watch SealedCountdown β†’ timer hits zero Oracle settles window, KMS decrypts pools, PriceRevealed emitted
/oracle β†’ Resolve β†’ watch agents stream live resolveMarket tx appears on Sepolia in real time
/portfolio β†’ Claim Oracle signs Claim(amountHandle); vault calls confidentialTransfer(user, handle)

πŸ“¦ Deployed Contracts

Contract Address Role
0x9b5C…dFfF Underlying ERC-20, public mint
0x7c5B…3639 ERC-7984 confidential wrapper
0xab75…0F5E Encrypted AMM + sealed-bid windows
0xddd9…B48f cUSDC custody + EIP-712 settlement
0xbD4f…Bfd0 Metadata registry + lifecycle entry

The cUSDC addresses are Zama's canonical Sepolia tokens β€” anyone with Sepolia USDC can interact with the same vault.

Live shielded bet tx (encrypted calldata, no amount in events): 0x5994…9350


πŸ—‚ Repository Layout

GhostMarket/
β”œβ”€β”€ πŸ“œ contracts/
β”‚   β”œβ”€β”€ contracts/
β”‚   β”‚   β”œβ”€β”€ GhostEAMM.sol           β€” FHE encrypted AMM + sealed-bid windows
β”‚   β”‚   β”œβ”€β”€ GhostVaultV2.sol        β€” ERC-7984 cUSDC custody + EIP-712 settlement
β”‚   β”‚   └── GhostMarket.sol         β€” Metadata registry + lifecycle forwarder
β”‚   β”œβ”€β”€ scripts/
β”‚   β”‚   β”œβ”€β”€ deploy-sepolia.ts       β€” Full stack deploy (use this)
β”‚   β”‚   β”œβ”€β”€ redeploy-eamm.ts        β€” EAMM-only upgrade + auto-rewire
β”‚   β”‚   β”œβ”€β”€ demo-sealed-window.ts   β€” Seed a 5-min sealed-window market
β”‚   β”‚   └── seed-markets.ts / seed-3-markets.ts
β”‚   └── test/
β”‚       └── GhostEAMM.test.ts       β€” FHE bets, sealed windows, ACL grants, min-bet guard
β”‚
β”œβ”€β”€ πŸ€– oracle/src/
β”‚   β”œβ”€β”€ index.ts                    β€” Express + WebSocket server (port 8092)
β”‚   β”œβ”€β”€ agents.ts                   β€” 7 agent definitions; first 4 active by default (`ACTIVE_ORACLE_AGENTS`)
β”‚   β”œβ”€β”€ fetcher.ts                  β€” Live CEX / DeFiLlama / FRED fetchers (no API keys needed)
β”‚   β”œβ”€β”€ sealed-window-watcher.ts    β€” Settle expired windows β†’ Zama KMS decrypt β†’ publishWindowPrice
β”‚   β”œβ”€β”€ oracle-signer.ts            β€” EIP-712 sign Claim(user, marketId, amountHandle)
β”‚   └── eamm-resolver.ts            β€” On-chain resolveMarket + grantPositionAccess
β”‚
└── 🌐 web/src/
    β”œβ”€β”€ app/                        β€” / Β· /markets/[id] Β· /vault Β· /portfolio Β· /oracle Β· /admin
    β”œβ”€β”€ components/
    β”‚   β”œβ”€β”€ bet-slip.tsx            β€” 5-step shielded bet chain (one button click)
    β”‚   β”œβ”€β”€ sealed-countdown.tsx    β€” Window countdown + price reveal animation
    β”‚   └── oracle-room.tsx         β€” Live agent WebSocket dashboard (~1250 LOC)
    └── lib/
        β”œβ”€β”€ eamm.ts                 β€” relayer-sdk init, encryption, sealed-window subscriptions
        └── vault.ts                β€” wrap β†’ setOperator β†’ deposit β†’ lock β†’ claim β†’ withdraw

⚑ Running Locally

1. Contracts

cd contracts && npm install
cp .env.example .env   # fill DEPLOYER_PRIVATE_KEY, ORACLE_PRIVATE_KEY, SEPOLIA_RPC_URL

npx hardhat compile
npx hardhat run scripts/deploy-sepolia.ts --network sepolia
# Prints addresses β†’ paste into contracts/.env, oracle/.env, and web/.env.local (all three)
npx hardhat run scripts/seed-3-markets.ts --network sepolia

# Optional: sealed-window demo market
npx hardhat run scripts/demo-sealed-window.ts --network sepolia
# Note the market ID; set WATCHED_MARKETS=<id> in oracle/.env

2. Oracle service

cd oracle && npm install && cp .env.example .env
# Required: SEPOLIA_RPC_URL, SEPOLIA_PRIVATE_KEY, contract addresses, GHOST_VAULT_EIP712_VERSION=2
# Optional: OPENAI_API_KEY, ACTIVE_ORACLE_AGENTS, WATCHED_MARKETS

npm run dev
# http://localhost:8092/oracle/health
# ws://localhost:8092/oracle/ws/:marketId

3. Frontend

cd web && npm install && cp .env.local.example .env.local
# Fill NEXT_PUBLIC_* contract addresses, NEXT_PUBLIC_PRIVY_APP_ID, NEXT_PUBLIC_ORACLE_URL

npm run dev   # http://localhost:3000
Optional: FastAPI orchestration layer
python -m venv .venv && source .venv/bin/activate
pip install -r api/requirements.txt
uvicorn api.main:app --reload   # http://localhost:8000/docs

The frontend works without this. It's an orchestration surface for relay / server-side flows.


πŸ§ͺ Tests

cd contracts
npx hardhat test --network hardhat   # mock FHE β€” fast, CI-friendly
npx hardhat test --network sepolia   # real FHE coprocessor β€” slower, costs gas
Full test coverage β€” GhostEAMM.test.ts
placeBet          βœ” encrypted YES + NO bets accepted
                  βœ” BetPlaced event has 3 args only β€” no amount field
                  βœ” self-decrypt of own position correct
                  βœ” cross-decrypt by another user rejected
                  βœ” multi-bet accumulation in encrypted pool

min-bet guard     βœ” above-min: stored handle equals original amount
                  βœ” below-min: silently zeroed β€” no dust position

resolveMarket     βœ” resolver / owner can resolve; others revert
                  βœ” double-resolve reverts; post-resolve placeBet reverts

grantPositionAccess βœ” winner-only ACL after YES resolution
                  βœ” both sides granted after cancellation
                  βœ” rejects users with no position; rejects active markets

sealed windows    βœ” open β†’ snapshot handles non-zero
                  βœ” bets accepted during open window
                  βœ” bets blocked between expiry and settlement
                  βœ” settle-before-expiry reverts
                  βœ” resolver decrypts pool ciphertexts after settle
                  βœ” double-settle reverts; bets reopen after settle
                  βœ” publishWindowPrice emits PriceRevealed
                  βœ” publish on unsettled window reverts
                  βœ” getActiveWindowIdx returns max-uint when no active window

admin             βœ” pause / unpause gate all state changes

πŸ“‹ Operational Notes

  • Always full-deploy. deploy-sepolia.ts wires GhostMarket β†’ GhostEAMM atomically. Redeploying only the EAMM without updating GhostMarket.setEamm leaves metadata and encrypted state on different contracts (MarketNotFound reverts on bets). Use scripts/redeploy-eamm.ts for EAMM-only upgrades β€” it handles the rewire.
  • EIP-712 domain version is "2". GhostVaultV2 uses EIP712("GhostVault", "2"). Set GHOST_VAULT_EIP712_VERSION=2 in oracle .env.
  • Sealed windows need a watcher. Set WATCHED_MARKETS=<id> in oracle/.env, or rely on dynamic discovery via the SealedWindowOpened event listener.
  • Mainnet swap. Replace underlying USDC + cUSDC addresses with Circle USDC + a production ERC-7984 wrapper. Zero contract or frontend code changes needed.

πŸ›  Tech Stack

Layer Stack
Next.js 15 Β· React 19 Β· Tailwind v4 Β· viem v2 Β· Privy v3
@zama-fhe/relayer-sdk Β· fhevmjs (browser-side encryption + ZKPoK)
Solidity 0.8.26 Β· Hardhat Β· @fhevm/solidity Β· OpenZeppelin v5
Node.js Β· Express Β· WebSockets Β· OpenAI gpt-4o-mini
Zama FHEVM coprocessor Β· KMS gateway Β· Sepolia relayer

About

Confidential Dark-Pool Prediction Markets

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors