Everything about how Hoodfutures works — the protocol design, the smart contracts, the math behind swaps and prediction payouts, how markets resolve, and how to run and deploy the stack yourself.
Overview
Hoodfutures is two products that share one oracle layer and one wallet: a spot DEX for tokenized stocks, and a prediction market on the events that move those stocks. The thesis is simple — the two things people actually want to do with a stock are trade it and bet on what happens to it.
On the Trade side, you swap tokenized equities (NVDA, TSLA, AAPL, SPY, HOOD, META …) against USDG through constant-product liquidity pools. On the Predict side, you take YES/NO positions on verifiable outcomes — “Will NVDA close above $200 by Friday?”, “Will the Fed cut 25bps in September?” — collateralized in USDG and settled on-chain.
The connective tissue is a shared OracleRouter: the same NVDA price feed that quotes the spot pool also settles the “NVDA above $200” market. That means you can express a thesis and hedge it in one place, in one transaction context, with one balance.
Design principle
Context
Robinhood Chain is an Ethereum-compatible L2 optimized for real-world assets, most notably tokenized stocks. Those tokens are ERC-20s enhanced for corporate actions (dividends and splits) and priced by Chainlink total-return feeds. That makes it the natural home for a product whose core asset is the stock token.
System
The system is a small set of independent contracts plus a shared price layer. Nothing is upgradeable-by-proxy in the MVP; contracts are deployed and their addresses recorded.
┌─────────────────────────────┐
│ OracleRouter │ shared price layer (1e18 USD)
│ Chainlink feed OR manual │
└───────┬─────────────┬───────┘
│ │
prices pools │ │ settles price markets
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────┐
│ SpotAMM │ │ PredictionMarketFactory │
│ createPool(stock) │ │ createMarket(params) │
│ │ │ │ │ resolver (manual) │
│ ▼ │ │ ▼ │
│ StockPool (ERC-20 LP) │ │ PredictionMarket (parimutuel)│
│ stock <> USDG, 0.30% │ │ YES/NO pools, USDG │
└───────────────────────────┘ └──────────────────────────────┘
│ │
└──────┬──────┘
▼
USDG (ERC-20 collateral / quote asset)The frontend (Next.js) reads addresses from a generated deployment file and talks to these contracts through wagmi/viem. In demo mode it runs against an in-memory store with simulated live prices so every screen is fully populated without a live chain.
hoodfarm/
├── contracts/ # Foundry project
│ ├── src/
│ │ ├── shared/ # OracleRouter, IOracleRouter, MockERC20
│ │ ├── trade/ # SpotAMM, StockPool
│ │ └── predict/ # PredictionMarket, PredictionMarketFactory
│ ├── script/ # Deploy.s.sol, SeedLocal.s.sol
│ └── test/ # OracleRouter / SpotAMM / PredictionMarket tests
└── web/ # Next.js 16 app
├── app/ # routes: /, /predict, /trade, /portfolio, /docs
├── components/ # brand, ui, layout, predict, trade, docs
└── lib/ # store, catalog, contracts (abis, networks, addresses)Primitives
USDG is the dollar-denominated stablecoin used everywhere: the quote side of every spot pool, the collateral for every prediction stake, and the unit all payouts are made in. On local/testnet a MockERC20 stands in with a public faucet; on mainnet the real USDG contract is used.
Each listed equity is an ERC-20. A spot pool pairs one stock token with USDG. Prices in the UI are derived from pool reserves and cross-checked against the oracle.
OracleRouter returns a USD price scaled to 1e18for any token. It supports two modes per token: a live Chainlink aggregator (production), or a manual price set by the owner (local/testnet, and demo bootstrapping). Chainlink answers are staleness-checked and normalized from the feed's decimals to 1e18.
Why 1e18 everywhere
Product
The Trade product is a Uniswap-v2-style automated market maker. A SpotAMM factory deploys one StockPool per stock token, each pairing that token with USDG. Every pool is its own ERC-20: LP shares are minted to liquidity providers and burned on withdrawal.
x · y = k; trades move along the curve.minOut and reverts if the realized output is worse.Mechanics
For input amountIn against reserves reserveIn / reserveOut, with a 30 bps fee (fee basis = 10000):
amountInWithFee = amountIn * (10000 - 30);
amountOut = (amountInWithFee * reserveOut)
/ (reserveIn * 10000 + amountInWithFee);Because the fee stays in the pool, the invariant k = x·y grows slightly on every trade — that growth is exactly the LP yield. A test asserts k is non-decreasing across swaps.
The first provider sets the initial price via the ratio they deposit and receives sqrt(x·y) − MINIMUM_LIQUIDITYshares. Subsequent providers must match the current reserve ratio; shares are minted pro-rata to the smaller of the two contributed proportions, so you can't mint free shares by skewing the ratio.
Burning s shares returns s/total · reserveStock and s/total · reserveUsdg — your pro-rata slice of both reserves, including accrued fees.
Larger trades move further along the curve, so the effective price worsens with size relative to pool depth. The UI surfaces an estimated price impact and flags trades that exceed ~2%.
Product
Prediction markets are binary (YES/NO) and use a parimutuel model: rather than an order book or an LMSR maker, both sides stake USDG into a shared pool, and after resolution the winning side splits the entire pool pro-rata. This is simple, capital-efficient, and impossible to grief with fake liquidity.
The market's implied probability of YES is just the pool split:
P(YES) = yesPool / (yesPool + noPool)An empty market shows 50%. As stakes come in, the probability — and the “price” of a YES or NO share in cents — moves continuously. The market detail page charts this over time.
| Type | Example | Resolution |
|---|---|---|
| Price target | NVDA ≥ $200 at close | Chainlink feed, trustless |
| Earnings | AAPL beats EPS consensus | Manual (public filing) |
| Macro | Fed cuts 25bps in September | Manual (FOMC statement) |
| Index | S&P 500 finishes the week green | Manual / feed |
Mechanics
Let the pools at resolution be:
total = yesPool + noPool
winning = pool on the winning side
losing = pool on the losing side
fee = losing * protocolFeeBps / 10000 (default 2% of the losing pool)
payout(user) = userStake_winningSide / winning * (total - fee)In words: winners get their stake back plus a pro-rata share of the losers' money, net of the protocol fee — which is taken only from the losing pool, never from principal. Your realized multiple is payout / stake.
Alice stakes 300 USDG on YES, Bob stakes 100 on NO. NVDA closes above the strike, so YES wins.
No winners → market voids
VOID and every participant can reclaim their exact principal. No fee is charged on a void.After resolution, winners call claim(). The protocol fee is paid once, lazily, on the first winning claim, and each address can claim exactly once (tracked by a claimed mapping). Losing positions simply return a zero payout.
Settlement
Price-target markets carry a priceToken, a strike1e18, and a yesIfAbove flag. After closeTime, anyone can call resolvePrice(). It reads the oracle price and resolves YES iff price ≥ strike (or ≤ when yesIfAbove is false). No privileged party is involved.
function resolvePrice() external {
require(block.timestamp >= closeTime, "too early");
uint256 price = oracle.getPrice(priceToken); // 1e18 USD
bool yesWins = yesIfAbove ? price >= strike1e18
: price <= strike1e18;
_finalize(yesWins ? Outcome.YES : Outcome.NO);
}Earnings and macro outcomes can't (yet) be read from a feed, so they use ResolutionType.MANUAL. Only the factory's resolver can settle them, via factory.resolveManual(market, yesWins), and only after close. Each market records the exact public source it will be resolved against.
Phase 2: UMA
maxStaleness (default 1 day) revert.Reference
Shared price source. Owner-managed feeds; permissionless reads.
| Function | Access | Description |
|---|---|---|
setAggregator(token, agg) | owner | Point a token at a Chainlink feed |
setManualPrice(token, p1e18) | owner | Set a manual 1e18 price |
getPrice(token) | view | Returns 1e18 USD price (reverts if stale/missing) |
getPriceData(token) | view | Price + last-updated timestamp |
hasFeed(token) | view | Whether a source exists |
| Function | Access | Description |
|---|---|---|
createPool(stock) | anyone | Deploy a stock/USDG StockPool |
poolFor(stock) | view | Pool address for a stock token |
allPools(i) / allPoolsLength() | view | Enumerate pools |
| Function | Access | Description |
|---|---|---|
addLiquidity(stock, usdg, to) | anyone | Deposit; mints LP shares |
removeLiquidity(shares, to) | anyone | Burn LP; withdraw pro-rata |
swapUsdgForStock(in, minOut, to) | anyone | Buy stock with USDG |
swapStockForUsdg(in, minOut, to) | anyone | Sell stock for USDG |
getReserves() / getAmountOut(...) | view/pure | Quote helpers |
| Function | Access | Description |
|---|---|---|
createMarket(params) | anyone | Deploy a market; charges creation fee |
resolveManual(market, yesWins) | resolver | Settle a manual market |
setParams(fee, bps, recipient) | owner | Creation fee / protocol fee / sink |
setResolver(addr) | owner | Rotate the manual resolver |
| Function | Access | Description |
|---|---|---|
betYes(amount) / betNo(amount) | anyone | Stake USDG on a side (while open) |
resolvePrice() | anyone | Settle a PRICE market from the oracle |
resolveManual(yesWins) | factory | Settle a MANUAL market |
claim() | anyone | Collect payout / refund after resolution |
probabilityYes() / payoutOf(user) | view | Live odds and owed payout |
Economics
| Fee | Amount | Who receives it |
|---|---|---|
| Swap fee | 0.30% of input | Liquidity providers (stays in pool) |
| Market creation fee | Flat USDG (configurable, 0 by default) | Protocol treasury |
| Protocol fee (predict) | 2% of the losing pool | Protocol treasury |
There is no protocol token in the MVP. If one is ever introduced, it should accrue real revenue (swap + market fees), never redistribution taxes. The protocol fee is bounded in code — protocolFeeBps ≤ 1000 (10%) at the factory and per market — so it can never be set to confiscatory levels.
Reference
Local deployment addresses are generated by the seed script into web/lib/contracts/deployments.local.json and exposed through lib/contracts/addresses.ts, keyed by chain id. Mainnet/testnet entries are filled in after running the deploy script.
Guide
cd contracts
forge install # OpenZeppelin + forge-std
forge build
forge test # 20 tests: oracle, AMM, prediction flowsanvil # terminal 1
forge script script/SeedLocal.s.sol \
--rpc-url http://127.0.0.1:8545 --broadcast # terminal 2The seed script deploys USDG, the oracle, the AMM with five funded pools, and five prediction markets, then writes all addresses to the frontend's deployment file.
cd web
pnpm install
pnpm dev # http://localhost:3000Demo mode
WagmiProvider (config in lib/web3/config.ts) and swapping store actions for the contract calls in lib/contracts/abis.ts.Guide
Set your environment (see contracts/.env.example) and run the deploy script against the target network. On mainnet, provide the real USDG_ADDRESS; if omitted, a mock USDG is deployed (testnet/local).
# testnet
forge script script/Deploy.s.sol \
--rpc-url rh_testnet --broadcast --verify
# mainnet (chainId 4663)
USDG_ADDRESS=0x... FEE_RECIPIENT=0x... \
forge script script/Deploy.s.sol \
--rpc-url rh_mainnet --broadcast --verifyAfter deploying, wire feeds with oracle.setAggregator(token, chainlinkFeed), create pools and seed liquidity, then open your launch markets. Record the resulting addresses in addresses.ts.
Engineering
useSyncExternalStore store (lib/data/store.ts) with referentially-stable snapshots, localStorage persistence, and a 2s tick that simulates live prices and probability drift.wagmi/viem with Robinhood Chain, testnet and anvil pre-configured; trimmed ABIs for the calls the UI makes.Important
ReentrancyGuard on all state-changing pool and market functions; SafeERC20 for transfers.Not audited, not financial advice
What's next
| Phase | Scope |
|---|---|
| MVP (now) | Spot swaps + parimutuel prediction markets, Chainlink price resolution |
| Phase 2 | Perpetual futures (PerpEngine + shared liquidity vault) on the same oracle |
| Phase 3 | UMA Optimistic Oracle for permissionless earnings/macro resolution |
| Phase 4 | Order-book / CLOB markets, cross-margin between Trade and Predict, mobile |
Questions
No. The MVP has no protocol token and requires none.
It's the pool split: a YES share costs P(YES) cents, a NO share costs 1 − P(YES)cents. Your payout depends on the pools at resolution, not at entry, because it's parimutuel.
If the winning side is empty at resolution the market voids and everyone is refunded. If the losing side is empty, winners simply get their principal back (there's nothing to win).
Price markets: anyone, permissionlessly, after close (the oracle decides). Manual markets: only the factory resolver.
Yes — the 0.30% swap fee stays in the pool, growing the invariant and LP share value.
Terms
| Term | Meaning |
|---|---|
| USDG | Dollar-denominated stablecoin; quote asset and collateral |
| Parimutuel | Betting pool where winners split the total stake pro-rata |
| Implied probability | yesPool / (yesPool + noPool) |
| Strike | Price threshold that decides a price market |
| Constant product | AMM invariant x·y = k |
| Price impact | How much a trade moves the pool price, vs depth |
| Staleness | Max age of an oracle answer before it's rejected |
| Void | Market outcome with no winning stake; everyone refunded |