Skip to main content

Smart Contracts

IndexPVP contracts are deployed on Robinhood Chain (chain ID 4663, production) and Robinhood Chain Testnet (chain ID 46630, staging). Verify contracts on robinhoodchain.blockscout.com (mainnet) and explorer.testnet.chain.robinhood.com (testnet).

Contract Addresses

Addresses link to explorer.testnet.chain.robinhood.com. Mainnet (4663) is not deployed yet.

ContractRobinhood Chain (4663)Robinhood Chain Testnet (46630)
DelistGameTBD0xe38A3Af952848C1b22b4536DE07b6E5D36EE012c
GovernanceVotingV2 (ZK roundtable)TBD0xd6A923B070015a14fF6d17b3bB32258af6e99E19
HonkVerifier (ZK verifier)TBD0xaBFEb379809A209eEEe5F89631f4cA0e99d014ED
DelistGameHookTBD0xbEB1BAA8ba934e242ad24A90cf806292ec495ACC
QuiverRandomnessProvider (live)TBD0x5F2CF32E9B7a78edAC7C3Ac06caf32D06d495100
↳ Quiver coordinator (Quiver Foundation)TBD0x1da30d6465f657F11B4D7F6Db0B16aD79152fb40
↳ Quiver provider (Quiver Foundation)TBD0xc84CC91131b63d9BECFDe7b2DB3D0C653B690541
KeeperRandomnessProvider (legacy fallback)TBD0xfa728A095be6D0e751A0060fd0a6962559C888be
PrizePotTBD0x625dB82C212681BD0E86471A175537975ea3498C
IndexTokenFactoryTBD0x8c5658fE36aFa26601FBC04714666a6Dc4625370
IndexSwapRouterTBD0x9DB8FD852B987C78AFB7e6ad56F3D7795605B5FC
NewsWireTBD0xbcd6B157BE01696762F830068A135E4Fb5F80ffD
PredictionMarketTBD0x565419249F8538d4c8DeBF333d8d0666EBd7dd98
Uniswap V4 PoolManager0x8366a39CC670B4001A1121B8F6A443A643e409510x1b4fb660fe2bbd47476c47cb0f93a5d103328ee3 (self-deployed for staging)
Live on testnet

IndexPVP is live on Robinhood Chain Testnet (46630) — all 117 index tokens are listed, trading is open, and the game has started. Play at indexpvp-webapp.vercel.app. Mainnet (4663) addresses are filled in when the production deployment lands. The V1 GovernanceVoting contract (token-weighted voting) is deprecated in favour of GovernanceVotingV2 and is not part of this deployment.

Contract Overview

DelistGame

The main game controller. Manages the game lifecycle, index listing, staircase liquidity distribution, vote triggering, delisting execution, ETH redistribution via buyback swaps, and market-close detection. Creates the PrizePot contract on deployment. Consumes randomness through the provider-agnostic IRandomnessProvider interface (fulfilled by QuiverRandomnessProvider).

Key functions:

  • startGame() — Rings the opening bell and begins the first countdown (owner only)
  • triggerVote() — Opens voting when countdown expires (anyone)
  • executeDelisting(roundId) — Phase 1: finalizes the vote, requests randomness from the provider (anyone)
  • rawFulfillRandomness(id, word) — Phase 2: randomness callback that executes the delisting and redistributes ETH (called by the randomness provider only)
  • cancelPendingDelisting() — Fallback: executes the delisting with blockhash/timestamp-derived entropy if the randomness reveal doesn't arrive within 1 hour (owner only)
  • withdrawRandomnessBalance(to, amount) — Withdraw ETH held for randomness-provider payments (owner only, blocked during active delistings)
  • listIndex(name, symbol, indexCode) — Deploys and lists an index (owner only)
  • batchListIndexes(names, symbols, codes) — Batch version of the above

Key state:

  • gameOver — True when only one index remains
  • winningIndex — The bytes2 code of the last ticker standing
  • prizePot — Address of the PrizePot contract (created in constructor)
  • delistingInProgress — True between randomness request and callback (blocks new votes/delistings)
  • activeRandomnessRequestId — The in-flight randomness request ID (0 when idle)

Events:

  • GameStarted(countdownEnd)
  • IndexListed(indexCode, token, poolId)
  • VoteTriggered(roundId, winnerIndexCode)
  • DelistingRequested(requestId, roundId, winnerCode) — Phase 1: randomness requested
  • IndexDelisted(indexCode, roundId, delistedBy) — Phase 2: delisting completed
  • DelistingCancelled(roundId) — Randomness timeout fallback triggered
  • GameOver(winningIndexCode, prizePotAddress, potBalance)

Randomness integration:

  • Requests 1 random word per delisting via IRandomnessProvider.requestRandomness(); derives 3 values via keccak256 for buyback target selection
  • Contract must hold ETH to pay provider fees (funded at deployment, replenishable via receive())
  • See QuiverRandomnessProvider below

QuiverRandomnessProvider

Randomness now comes from Quiver VRF, provided by the Quiver Foundation — a verifiable two-party commit–reveal randomness coordinator that is live on Robinhood Chain. Chainlink VRF (and comparable oracles like Pyth Entropy or Gelato VRF) are still not available on Robinhood Chain, so IndexPVP integrates Quiver behind the same provider-agnostic IRandomnessProvider interface the game already used:

interface IRandomnessProvider {
function requestRandomness() external payable returns (uint256 id);
}
// consumer callback: rawFulfillRandomness(uint256 id, uint256 word)

QuiverRandomnessProvider is a thin adapter contract that requests randomness from the Quiver coordinator (using Quiver's QuiverConsumer push flow from lib/quiver-kit) and maps the Quiver seq (sequence number) to the game's requestId.

How it works:

  1. DelistGame calls requestRandomness(); QuiverRandomnessProvider forwards the request to the Quiver coordinator and records the returned seq → requestId mapping
  2. Quiver's own keeper, "Fletcher", delivers the reveal callback — the IndexPVP keeper is not involved in randomness at all
  3. The final value is randomNumber = keccak256(userRandom ‖ providerRevelation) — combining the consumer's own randomness with Quiver's revelation. This two-party scheme is fair and unbiasable as long as either party (the game or Quiver) is honest, so neither side can grind the outcome alone
  4. The provider delivers the word to the consumer via rawFulfillRandomness(id, word), gated so only the provider can invoke the callback

The Quiver coordinator and provider addresses are selected by chain id in Deploy.s.sol, overridable via the QUIVER_COORDINATOR / QUIVER_PROVIDER env vars. If the callback never arrives within RANDOMNESS_TIMEOUT (1 hour), DelistGame.cancelPendingDelisting() completes the delisting with fallback entropy keccak256(blockhash(block.number - 1), block.timestamp, ...). block.prevrandao is never used — it is the constant 1 on Arbitrum-stack chains like Robinhood Chain.

Live on testnet (46630): the QuiverRandomnessProvider adapter is 0x5F2CF32E9B7a78edAC7C3Ac06caf32D06d495100, set as DelistGame.randomnessProvider(). It fronts the Quiver Foundation coordinator 0x1da30d6465f657F11B4D7F6Db0B16aD79152fb40 and provider 0xc84CC91131b63d9BECFDe7b2DB3D0C653B690541, whose Fletcher keeper delivers the reveal.

KeeperRandomnessProvider (legacy)

The original home-grown keeper commit–reveal provider. It still exists in the codebase and remains owner-swappable via DelistGame.setRandomnessProvider(), but it is unused now that Quiver VRF is live. It is kept as a fallback option only.

DelistGameHook

A Uniswap V4 hook that implements the dynamic fee system, circuit breakers, halt-window fees, dual-recipient fee distribution, and liquidity removal restrictions.

Permissions:

  • beforeSwap — Collects fee on exact-input swaps
  • afterSwap — Collects fee on exact-output swaps, updates volatility tracking
  • beforeRemoveLiquidity — Only allows the DelistGame contract to remove liquidity
  • afterInitialize — Seeds initial volatility state

Fee distribution:

  • Fees are split between protocolFeeRecipient (70% default) and treasuryRecipient / PrizePot (30% default)
  • Split ratio configurable via setTreasurySplit()
  • Base fee (flat + dynamic) hard-capped at 5% by immutable ABSOLUTE_MAX_FEE_BPS
  • Flat fee and max total fee are admin-configurable via setFees()

Key state:

  • Per-pool volatility tracking (reference tick, accumulator, timestamps)
  • Circuit-breaker activation timestamps per pool (circuitBreakerActivatedAt)
  • Global halt-window flag (haltWindowActive)
  • flatFeeBps (default 100), maxTotalFeeBps (default 500), treasurySplitBps (default 3000)

GovernanceVotingV2

Secret-ballot, ZK-proved roundtable voting. Each round, the top holder of every listed index can cast one rank-weighted encrypted vote. Votes are decrypted only after the voting window closes, then tallied on-chain.

Key functions:

  • startVoteRound(immuneIndex) — Called by DelistGame when triggerVote() fires. Starts a new round and emits VoteStarted(roundId, immuneIndex, voteEnd)
  • setRankingRoot(roundId, root, aliveCount) — Keeper posts a Merkle root of Poseidon(indexCode, rank, topHolder) leaves so voters can generate proofs
  • castVote(nullifier, weight, voteCommitment, encryptedVote, proof) — A leader submits their encrypted vote. The ZK proof attests they're the top holder of some listed index at some rank without revealing which. voteCommitment = keccak256(voterIndex, targetIndex, salt) binds the keeper to the plaintext
  • revealAndFinalize(roundId, reveals[]) — Keeper-only. Decrypts all votes, verifies each keccak commitment matches, tallies weighted votes, emits the target
  • finalize(roundId) — Returns the tallied target to DelistGame during executeDelisting
  • getRoundInfo(roundId) — View round data: immune index, ranking root, alive count, voteEnd, finalized, target
  • keeperPublicKey() — The NaCl-box public key the frontend encrypts votes to

Weight formula: weight = aliveCount - rank + 1. Rank 1 = blue-chip index (most powerful vote, cannot be delisted).

Events:

  • VoteStarted(roundId, immuneIndex, voteEnd)
  • RankingRootSet(roundId, root, aliveCount)
  • VoteCast(roundId, voteIndex, nullifier, weight) — nullifier is Poseidon(2, roundId, indexCode), so observers can see which index voted (turnout) but not who they voted for
  • VoteRevealed(roundId, voterIndex, targetIndex, weight) — emitted N times during revealAndFinalize
  • VoteFinalized(roundId, targetIndex)

Trust model: the keeper cannot forge votes (keccak commitments are posted before the keeper sees plaintext) and cannot invent votes (every reveal must match a previously stored commitment). It can refuse to reveal, in which case the round stalls — a future V3 will replace the single keeper with a threshold-decryption committee.

See Voting & Delisting for the full circuit, keeper, and frontend flow.

HonkVerifier

Noir-generated UltraHonk verifier contract. Called from GovernanceVotingV2.castVote to verify each vote proof. Deployed standalone (not via-IR) so the 14.8KB bytecode fits under the EIP-170 contract-size limit.

PrizePot

Accumulates ETH from the hook's 30% treasury fee split. When the market closes, the winning index's token holders forfeit tokens to redeem proportional ETH at NAV (the RFV).

Key functions:

  • finalize(winningToken) — Called by DelistGame when the last ticker remains. Snapshots pot balance and token supply (game only)
  • claim(tokenAmount) — Forfeit winning tokens for proportional ETH — redemption at NAV. Requires prior approval
  • previewClaim(tokenAmount) — View: returns the ETH amount a redemption would yield
  • sweep() — Emergency withdrawal of all ETH (owner only)

Key state:

  • snapshotPot — ETH balance frozen at finalization
  • snapshotSupply — Winning token total supply at finalization
  • totalClaimed — Running total of ETH redeemed
  • finalized — Whether the pot has been finalized

Events:

  • Finalized(winningToken, pot, supply)
  • Claimed(user, tokenAmount, ethAmount)
  • Swept(to, amount)

IndexSwapRouter

User-facing trading interface. Simplifies buying and selling index tokens by wrapping Uniswap V4 pool operations.

Key functions:

  • buyTokens(indexCode, minTokensOut) — Buy tokens with ETH (payable)
  • sellTokens(indexCode, tokenAmount, minEthOut) — Sell tokens for ETH

IndexTokenFactory

Deploys and tracks all index token contracts. Maintains a mapping from index codes (ISO alpha-2 country codes) to token addresses.

Key functions:

  • indexTokens(bytes2 code) — Get token address for an index
  • totalTokens() — Total number of deployed index tokens

IndexToken

Standard ERC20 with a delisted flag and an indexCode identifier (the ISO alpha-2 code of the index's country). Each has a fixed supply of 1 billion tokens.

NewsWire

Pay-to-post headline board with dutch auction pricing. Floor price 0.005 ETH, doubles on each purchase, decays back to floor over 6 hours.

Key functions:

  • getCurrentPrice() — Current posting price
  • postMessage(message) — Post a headline (payable)
  • current() — View the active headline
  • getRecent(count) — View recent headline history

Technical Details

  • Solidity version: 0.8.26
  • EVM version: Cancun
  • Optimizer: Enabled, 200 runs, via-IR
  • Uniswap V4: Pools use dynamic fees (0x800000 flag) with tick spacing 60
  • All liquidity is protocol-owned — No external LPs. The DelistGame contract owns all positions.
  • LP fees are zero — All trading fees are split between protocol (70%) and Prize Pot (30%) via the hook