
Veritas_IITD Markets
On-chain prediction markets for campus events — real stakes for students, real signal for colleges.
Demo
About
Veritas (IITD Markets) For students: put your money where your prediction is. Pick a side in any inter-hostel event — cricket finals, sports championships, any competition with a real outcome — and stake USDC on it. Win, and you don't just get bragging rights: you claim a proportional share of everyone who backed the losing side, after a 10% platform fee. It turns campus rivalry into something with actual stakes, and gives you a live, crowd-priced read on who's actually favored before the event happens. For institutions: Veritas isn't just a betting app for students — it's a plug-in engagement and insight layer for the college itself. Student councils and event bodies get a transparent, on-chain gauge of real sentiment (backed by real money, not casual polls) around their own events, which drives attention and participation toward those events in the run-up. It's a product a college can point to as infrastructure it runs, not just something students happen to use. Two sponsor integrations are load-bearing, not decorative. Resolution is handled by a Chainlink CRE workflow that fetches the official result through a confidential-compute step and writes it on-chain via a DON-signed report through a Forwarder → Receiver contract, rather than a plain script or a trusted admin key. Uniswap V2 is used two ways: users can swap ETH for USDC and place a bet in one transaction if they don't already hold USDC, and every bet mints a transferable ERC20 "position token" that's listed on its own Uniswap pair — so a user can exit a position before resolution, or someone else can buy into a position on the open market and later claim the payout themselves, never having placed a bet at all. Payouts are claim-based rather than pushed to avoid unbounded gas costs as the number of bettors grows. The full stack — Solidity contracts, the CRE workflow, and a React/wagmi frontend with market list, betting, and results/claim screens — is tested end-to-end, including a headless-browser verification of the actual wallet-connected UI against a live local deployment.
How it's made
The stack is Hardhat + Solidity 0.8.24 for the contracts, a two-agent split repo (Chainlink resolution side / Market+Uniswap+frontend side, coordinated via AGENT_COORDINATION_*.md docs), and a React + TypeScript + Vite frontend using wagmi/viem for wallet and contract calls. Contracts: Market.sol owns the OPEN→CLOSED→RESOLVED lifecycle: USDC-denominated createMarket/placeBet/closeMarket, and a claim-based (never looped) payout so gas cost stays flat regardless of how many people bet. resolveMarket is gated to a single resolver address, designed from the start to eventually be CREMarketResolverReceiver rather than an EOA. Chainlink CRE: the resolution side is a Chainlink CRE workflow with a confidential-compute step that fetches and validates the official competition result, then writes on-chain via a DON-signed report through a Forwarder → CREMarketResolverReceiver.onReport → Market.resolveMarket. We built a mock resolver first (same validation logic, plain EOA signer) so the market side could be built and tested against a real call path before the CRE deployment existed, then verified the real receiver contract against it with a dedicated integration test. Uniswap: two integrations, both load-bearing. placeBetWithETH swaps ETH → USDC through a real Uniswap V2 Router and bets the actual received amount in one transaction. More ambitiously, every bet also mints a transferable ERC20 "position token" (OutcomeToken, 1:1 with USDC staked) that createMarket lists on its own Uniswap V2 pair against USDC — a genuine secondary market, not an internal ledger. claim() was reworked to pay out (and burn) whoever currently holds the winning token, not whoever originally placed the bet, since that's the entire point of making it tradeable. Notably hacky bits: 1. Vendoring the real Uniswap V2 Factory/Router locally for tests hit the classic gotcha where UniswapV2Library's hardcoded CREATE2 init-code-hash only matches Uniswap's original 0.5.16 build — a different Hardhat/solc/optimizer config produces different UniswapV2Pair bytecode, so every swap silently resolved to the wrong (non-contract) pair address. Fixed by vendoring a patched copy of the library with the hash recomputed from our own build (scripts/lib/computeInitCodeHash.js), and wrote a script to regenerate it if compiler settings ever change. 2. Listing an OutcomeToken on Uniswap at market-creation time had to degrade gracefully — a call to a Router with no code "succeeds" at the EVM level with empty returndata, and Solidity's try/catch can't catch the resulting ABI-decode failure (it's an uncatchable revert), so we added an explicit code.length check before attempting the call, so the stretch goal never blocks core market creation. 3. Pulling in OpenZeppelin's Strings (for token symbols) transitively requires the MCOPY opcode, which isn't available under the default paris EVM target — had to explicitly set evmVersion: "cancun" in the Hardhat config. 4. For frontend verification without a real wallet extension, we drove the actual UI in a headless Playwright browser using a ~40-line fake EIP-1193 provider that forwards eth_sendTransaction etc. straight to a live local Hardhat node (which auto-signs for its own known accounts) — this caught a real bug where the UI's "transaction confirmed" indicator flipped invisible the instant an approve transaction landed, before the user ever saw it.