Decentralized Prediction Markets Powered by Flare FTSO Oracle
Live Demo | Architecture | Contracts | Get Started
BetFlare is a fully decentralized prediction market platform built on Flare Network, leveraging the native FTSO (Flare Time Series Oracle) for trustless price resolution. Users can bet on whether asset prices (like XRP) will be above or below target prices at specific times.
| Problem | BetFlare Solution |
|---|---|
| External oracles introduce bridge risk | Native FTSO integration - same chain, no bridges |
| Fragmented liquidity across markets | Unified liquidity pool serves all markets |
| Gas costs discourage small trades | Gasless meta-transactions (EIP-712) |
| Complex UX for prediction markets | Simple YES/NO binary outcomes |
- FTSO-Powered Resolution - Markets resolve using Flare's decentralized price feeds
- Unified Liquidity Vault - Single deposit, multi-market exposure
- Constant Product AMM - Fair pricing via
k = YES × NOformula - ERC4626 Vault Standard - Composable LP shares
- Gasless Trading - Sign messages, relayers pay gas
- XRP Native Support - Deposit XRP, auto-convert to USDT
┌─────────────────┐
│ FTSO Oracle │
│ (Price Feeds) │
└────────┬────────┘
│ resolve()
▼
┌──────────────┐ deposit() ┌───────────────────┐ distribute() ┌─────────────┐
│ │ ───────────────▶│ │ ──────────────────▶│ │
│ User │ │ UniversalVault │ │ Markets │
│ (Trader) │ │ (ERC4626) │ │ (FPMM) │
│ │ ◀───────────────│ │ ◀──────────────────│ │
└──────────────┘ LP shares └───────────────────┘ fees └─────────────┘
│ │ │
│ buyYes() / buyNo() │ │
└───────────────────────────────────┼─────────────────────────────────────┘
│
┌──────┴──────┐
│ Liquidity │
│ Router │
└─────────────┘
- User deposits USDT → Vault mints LP shares
- Router distributes → Liquidity split equally across active markets
- Traders buy YES/NO → AMM prices adjust based on demand
- 0.2% fee collected → Routed back to vault (LPs earn)
- Resolution time hits → FTSO price fetched, outcome determined
- Winners redeem → 1:1 payout in USDT
┌─────────────────────────────────────────────────────────────────────────────┐
│ CORE CONTRACTS │
├─────────────────┬─────────────────┬─────────────────┬───────────────────────┤
│ FPMM │ ConditionalTokens│ UniversalVault │ LiquidityRouter │
│ (AMM Engine) │ (ERC1155) │ (ERC4626) │ (Distribution) │
├─────────────────┼─────────────────┼─────────────────┼───────────────────────┤
│ • buyYes/No │ • splitPosition │ • deposit │ • registerMarket │
│ • sellYes/No │ • mergePositions│ • withdraw │ • deployLiquidity │
│ • addLiquidity │ • redeemPositions│ • receiveFees │ • rebalance │
│ • removeLiquidity│ • reportPayouts │ • depositXRP │ • withdrawFromMarkets │
└─────────────────┴─────────────────┴─────────────────┴───────────────────────┘
│
┌─────────────────┴─────────────────┐
│ MARKET CONTRACTS │
├─────────────────┬─────────────────┤
│ FTSOResolver │ MarketFactory │
├─────────────────┼─────────────────┤
│ • createMarket │ • createXRPMarket│
│ • resolve │ • deployFPMM │
│ • getCurrentPrice│ │
└─────────────────┴─────────────────┘
BetFlare uses a Fixed Product Market Maker (FPMM) - a variant of the constant product formula optimized for binary outcomes.
k = YES_reserve × NO_reserve
When a user buys YES tokens:
// 1. Calculate fee (0.2%)
fee = investmentAmount × 20 / 10000
// 2. Net investment after fee
netInvestment = investmentAmount - fee
// 3. Preserve constant product
newNoReserve = noReserve + netInvestment
newYesReserve = k / newNoReserve
// 4. Tokens received (includes newly minted from split)
tokensReceived = yesReserve - newYesReserve + netInvestmentPrices are expressed as probabilities (0-100%):
YES_price = NO_reserve / (YES_reserve + NO_reserve)
NO_price = YES_reserve / (YES_reserve + NO_reserve)
Example: If YES=1000, NO=3000:
- YES price = 3000/4000 = 75% (market expects YES)
- NO price = 1000/4000 = 25%
Initial State:
YES Reserve: 1,000 USDT
NO Reserve: 1,000 USDT
k = 1,000,000
User buys 100 USDT of YES:
Fee: 0.20 USDT → vault
Net: 99.80 USDT invests
New NO Reserve: 1,099.80
New YES Reserve: 1,000,000 / 1,099.80 = 909.26
YES tokens received: 1,000 - 909.26 + 99.80 = 190.54
Effective price: 100 / 190.54 = 0.525 USDT per YES token
Most platforms require LPs to choose which market to provide liquidity to. This leads to:
- Fragmented liquidity
- Some markets have deep liquidity, others are illiquid
- LPs need active management
┌─────────────────────────────────────────────────────────────────┐
│ UNIVERSAL VAULT │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Total Assets = Idle Balance + Deployed + Fees Earned │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ LIQUIDITY ROUTER │ │
│ └─────────┬─────────┘ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Market A │ │ Market B │ │ Market C │ │
│ │ (33.3%) │ │ (33.3%) │ │ (33.3%) │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
When a new market is added or liquidity changes:
Phase 1: Withdraw Excess
For each market:
if current_balance > target_per_market:
withdraw(current_balance - target_per_market)
Phase 2: Deposit Deficit
For each market:
if current_balance < target_per_market:
deposit(min(deficit, available_balance))
LPs earn from multiple sources:
- Trading Fees - 0.2% of all trades across all markets
- Resolution Gains - If market resolves favorably for LP position
- Compounding - Fees automatically increase share value
Flare Time Series Oracle is Flare's native decentralized oracle. Unlike Chainlink or other external oracles:
- No bridge risk - Data is on-chain natively
- Decentralized data providers - Multiple independent sources
- Built into consensus - Part of Flare's architecture
function resolve(bytes32 marketId) external {
Market storage market = markets[marketId];
// 1. Check resolution time passed
require(block.timestamp >= market.resolutionTime, "Too early");
// 2. Fetch FTSO price
(uint256 price, , uint256 decimals) = ftsoRegistry
.getCurrentPriceWithDecimals(market.symbol);
// 3. Normalize to 5 decimals
uint256 normalizedPrice = normalizeDecimals(price, decimals);
// 4. Determine outcome
bool yesWins = market.isAbove
? normalizedPrice > market.targetPrice
: normalizedPrice < market.targetPrice;
// 5. Report to ConditionalTokens
uint256[] memory payouts = new uint256[](2);
payouts[0] = yesWins ? 1 : 0; // YES payout
payouts[1] = yesWins ? 0 : 1; // NO payout
conditionalTokens.reportPayouts(market.questionId, payouts);
}| Symbol | Asset | Decimals |
|---|---|---|
| testXRP | XRP/USD | 5 |
| testBTC | BTC/USD | 5 |
| testETH | ETH/USD | 5 |
| testFLR | FLR/USD | 5 |
┌────────────────────┬──────────────────────────────────────────────┐
│ Contract │ Address │
├────────────────────┼──────────────────────────────────────────────┤
│ USDT (Collateral) │ 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F │
│ ConditionalTokens │ 0xCe9070d4C6940e7528b418cFB36087345f947c49 │
│ UniversalVault │ 0xB2569b2fbeA31A4f8ECaCF6Dd1fDC53157107F87 │
│ LiquidityRouter │ 0xD6473a91aC0C62e6F2BC1FF60e02C59B0237b4ac │
│ FTSO Registry │ 0x48Da21ce34966A64E267CeFb78012C0282D0Ac87 │
└────────────────────┴──────────────────────────────────────────────┘
- Node.js 18+
- npm or pnpm
- MetaMask or compatible wallet
- Coston2 testnet configured
Network Name: Coston2
RPC URL: https://coston2-api.flare.network/ext/C/rpc
Chain ID: 114
Currency: C2FLR
Explorer: https://coston2-explorer.flare.network
# Clone repository
git clone https://github.com/tomrowbo/BetFlare.git
cd BetFlare
# Install contract dependencies
npm install
# Install frontend dependencies
cd frontend && npm install# Root .env (for contract deployment)
PRIVATE_KEY=your_deployer_private_key
# Frontend .env.local
NEXT_PUBLIC_WALLETCONNECT_ID=your_walletconnect_project_id# Start frontend
cd frontend
npm run dev
# Open http://localhost:3000# Deploy full system
npx hardhat run scripts/deploy.ts --network coston2
# Create a new market
npx hardhat run scripts/deploy-test-market-15min.ts --network coston2// Deploy FTSOResolver
const FTSOResolver = await ethers.getContractFactory("FTSOResolver");
const resolver = await FTSOResolver.deploy(FTSO_REGISTRY, CONDITIONAL_TOKENS);
// Create market: XRP above $2.50 in 15 minutes
const targetPrice = 250000; // 5 decimals
const resolutionTime = Math.floor(Date.now() / 1000) + (15 * 60);
const tx = await resolver.createMarket(
"testXRP", // FTSO symbol
targetPrice, // Target price
resolutionTime, // When to resolve
true // isAbove (bet on price > target)
);const FPMM = await ethers.getContractFactory("FPMM");
const fpmm = await FPMM.deploy(
CONDITIONAL_TOKENS,
USDT_ADDRESS,
conditionId, // From resolver.createMarket()
UNIVERSAL_VAULT
);
// Link FPMM to resolver
await resolver.setFPMM(marketId, fpmm.address);// Set router on FPMM
await fpmm.setRouter(LIQUIDITY_ROUTER);
// Register with router
await router.registerMarket(fpmm.address);
// Authorize in vault
await vault.authorizeMarket(fpmm.address, true);
// Add initial liquidity
await usdt.approve(fpmm.address, INITIAL_LIQUIDITY);
await fpmm.addLiquidity(INITIAL_LIQUIDITY);
// Trigger rebalance for vault distribution
await router.rebalance();frontend/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Home - Market listings
│ │ ├── markets/[slug]/ # Market detail & trading
│ │ ├── portfolio/ # User positions
│ │ ├── liquidity/ # LP deposit/withdraw
│ │ └── providers.tsx # Wagmi + RainbowKit setup
│ │
│ ├── components/
│ │ ├── BetSlip.tsx # Trading form
│ │ ├── MarketCard.tsx # Market display
│ │ ├── PriceChart.tsx # Price history
│ │ ├── Header.tsx # Navigation
│ │ └── home/ # Home page components
│ │
│ └── config/
│ ├── contracts.ts # Addresses & ABIs
│ └── wagmi.ts # Wallet configuration
| Layer | Technology |
|---|---|
| Framework | Next.js 16, React 19, TypeScript |
| Styling | TailwindCSS, Framer Motion |
| Wallet | Wagmi 2.x, RainbowKit 2.x, Viem |
| Data | TanStack React Query |
| Charts | Recharts, Custom SVG |
Unlike other L1s that rely on external oracle networks, Flare has FTSO built into its consensus layer:
Traditional Setup:
[External Oracle] ──bridge──▶ [L1 Chain] ──▶ [DApp]
⚠️ Bridge risk, latency, cost
Flare Setup:
[FTSO (Native)] ──direct──▶ [BetFlare]
✅ No bridge, low latency, trustless
- Native XRP bridging via Flare's State Connector
- Direct XRP deposits to vault (auto-converts via BlazeSwap)
- Access to XRP's massive user base
- Fast Finality: ~2 second block times
- Low Gas: Fraction of Ethereum costs
- EVM Compatible: Deploy Solidity contracts directly
| Protection | Implementation |
|---|---|
| Reentrancy | OpenZeppelin ReentrancyGuard on all state changes |
| Access Control | Owner-only admin functions, authorized market list |
| Safe Transfers | SafeERC20 wrapper for all token operations |
| Signature Security | EIP-712 typed data with nonce replay protection |
| Upgradability | UUPS proxy pattern (vault/router only) |
- Internal review completed
- External audit pending
- Bug bounty program TBD
- Core contracts deployed on Coston2
- Frontend live
- XRP price markets
- Liquidity vault
- Security audit
- Mainnet deployment
- Additional asset markets (BTC, ETH, FLR)
- Mobile-optimized UI
- Multi-outcome markets (3+ outcomes)
- Governance token
- DAO treasury
- Leveraged positions
- SDK for market creators
- API for integrations
- Cross-chain markets
We welcome contributions! Please see our guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Live Demo: betflare.vercel.app
- Flare Network: flare.network
- FTSO Documentation: docs.flare.network
- Coston2 Explorer: coston2-explorer.flare.network
Built with ❤️ for ETHOxford 2026