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.
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 remainswinningIndex— The bytes2 code of the last ticker standingprizePot— 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 requestedIndexDelisted(indexCode, roundId, delistedBy)— Phase 2: delisting completedDelistingCancelled(roundId)— Randomness timeout fallback triggeredGameOver(winningIndexCode, prizePotAddress, potBalance)
Randomness integration:
- Requests 1 random word per delisting via
IRandomnessProvider.requestRandomness(); derives 3 values viakeccak256for 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:
DelistGamecallsrequestRandomness();QuiverRandomnessProviderforwards the request to the Quiver coordinator and records the returnedseq → requestIdmapping- Quiver's own keeper, "Fletcher", delivers the reveal callback — the IndexPVP keeper is not involved in randomness at all
- 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 - 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 swapsafterSwap— Collects fee on exact-output swaps, updates volatility trackingbeforeRemoveLiquidity— Only allows the DelistGame contract to remove liquidityafterInitialize— Seeds initial volatility state
Fee distribution:
- Fees are split between
protocolFeeRecipient(70% default) andtreasuryRecipient/ 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 whentriggerVote()fires. Starts a new round and emitsVoteStarted(roundId, immuneIndex, voteEnd)setRankingRoot(roundId, root, aliveCount)— Keeper posts a Merkle root ofPoseidon(indexCode, rank, topHolder)leaves so voters can generate proofscastVote(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 plaintextrevealAndFinalize(roundId, reveals[])— Keeper-only. Decrypts all votes, verifies each keccak commitment matches, tallies weighted votes, emits the targetfinalize(roundId)— Returns the tallied target to DelistGame duringexecuteDelistinggetRoundInfo(roundId)— View round data: immune index, ranking root, alive count, voteEnd, finalized, targetkeeperPublicKey()— 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 isPoseidon(2, roundId, indexCode), so observers can see which index voted (turnout) but not who they voted forVoteRevealed(roundId, voterIndex, targetIndex, weight)— emitted N times duringrevealAndFinalizeVoteFinalized(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 approvalpreviewClaim(tokenAmount)— View: returns the ETH amount a redemption would yieldsweep()— Emergency withdrawal of all ETH (owner only)
Key state:
snapshotPot— ETH balance frozen at finalizationsnapshotSupply— Winning token total supply at finalizationtotalClaimed— Running total of ETH redeemedfinalized— 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 indextotalTokens()— 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 pricepostMessage(message)— Post a headline (payable)current()— View the active headlinegetRecent(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