A decentralized prediction market game built on the Linera blockchain that combines social gaming, cryptocurrency price predictions, and a points-based economy.
Roxy Price is an innovative blockchain game where players predict cryptocurrency price movements (daily, weekly, and monthly), trade points in peer-to-peer markets, join guilds for collaborative gameplay, and compete on leaderboards. The game features a sophisticated progression system with levels, achievements, and experience points.
- Multi-Timeframe Predictions: Make predictions on daily, weekly, and monthly cryptocurrency price movements
- Oracle Integration: Real-time price data from crypto APIs (CoinMarketCap, CoinGecko)
- Outcome Types: Predict Rise, Fall, or Neutral price movements
- Reward System:
- Daily predictions: 100 points per correct prediction
- Weekly predictions: 500 points per correct prediction
- Monthly predictions: 1000 points per correct prediction
- Guild Synergy: When a guild member makes a correct prediction, ALL guild members earn rewards
- Progressive Exchange Rate: Level-based exchange system (10:1 ratio across all levels)
- Level 1: Pay 10 points to receive 100 points
- Level 2: Pay 100 points to receive 1000 points
- Level N: Pay X/10 points to receive X points
- Market Creation: Level 5+ players with 10,000+ points can create markets
- Custom Fee Structure: Market creators set their own fee percentage (0-100%)
- Fee Distribution:
- Market creator keeps 98% of fees
- Platform receives 2% of creator fees
- Buying & Selling:
- All players can buy points from markets
- Only Level 5+ players can sell points to markets
- Experience & Leveling: Exponential progression system
- Level 1: 1,000 total XP required
- Level 2: 4,000 total XP required (4x multiplier)
- Level 3: 16,000 total XP required (4x multiplier)
- Formula:
XP_required = 1000 × (4^(level-1))
- Reputation System: Track player performance and trustworthiness
- Win Streaks: Monitor consecutive successful predictions
- Achievement System: Unlock rewards for milestones
Earn rewards for completing specific milestones:
| Achievement | Requirement | Reward | XP |
|---|---|---|---|
| Market Creator | Create first market | 100 points | 200 XP |
| First Buyer | Make first purchase | 50 points | 100 XP |
| First Seller | Make first sale | 50 points | 100 XP |
| Guild Member | Join a guild | 150 points | 300 XP |
| Level 2 Achiever | Reach level 2 | 200 points | 400 XP |
| Level 3 Achiever | Reach level 3 | 400 points | 800 XP |
| Level 5 Achiever | Reach level 5 | 1000 points | 2000 XP |
- Create Guilds: Form social groups for collaborative gameplay
- Shared Rewards: Guild members share prediction rewards
- Shared Penalties: Guild members share prediction losses
- Guild Pool: Contribute points to collective fund
- Guild Leaderboard: Compete with other guilds based on total member earnings
- Player Rankings: Top 50 traders by total points earned
- Guild Rankings: Top 20 guilds by total member points
- Statistics Tracked:
- Total profit (points earned)
- Win rate percentage
- Player level
- Guild member count
- Claim free points every 24 hours
- Encourages regular player engagement
- Configurable reward amounts
- Horizontal Scaling: Application runs on multiple Linera chains simultaneously
- Global State Synchronization: Players, markets, and guilds synchronized across all chains
- Idempotent Messaging: Safe message retries with duplicate detection
- Conflict Resolution: Timestamp-based ordering prevents stale updates
- Global Leaderboards: Aggregated rankings across all chains
- Reliable Broadcasting: Messages use authentication and tracking for guaranteed delivery
- Blockchain: Linera Protocol v0.15.4
- Smart Contract Language: Rust
- State Management: Linera Views (persistent storage)
- Oracle Integration: External price feed from crypto APIs
- Cross-Chain Messaging: Linera's native message system with idempotency and conflict resolution
- Development Tools: Docker, Docker Compose, Makefile for streamlined development
-
Register: Create your player account
- Receive initial token allocation (configurable)
- Start at Level 1 with 0 XP
-
Claim Daily Rewards:
- Log in daily to claim free points
- Build your balance for trading
-
Make Predictions:
- Predict daily crypto price movements (100 point reward)
- Predict weekly movements (500 point reward)
- Predict monthly movements (1000 point reward)
-
Buy Points from Markets:
- Browse available markets
- Progressive exchange rate: pay 10% of desired amount
- Market creator earns fees on your purchase
-
Progress & Earn:
- Complete achievements
- Level up for better trading capabilities
- Join guilds for shared rewards
-
Create Markets:
- Requires Level 5 and 10,000+ points
- Pay 100 point creation fee
- Set custom fee percentage (0-100%)
- Earn trading fees from buyers and sellers
-
Sell Points to Markets:
- Level 5+ only
- Convert your points back to market liquidity
- Pay market creator's fee
-
Build Guild Empire:
- Create or join guilds
- Share prediction rewards with members
- Compete on guild leaderboard
RegisterPlayer: Create new player accountUpdateProfile: Change display nameClaimDailyReward: Claim 24-hour login bonusPredictDailyOutcome: Make daily price predictionPredictWeeklyOutcome: Make weekly price predictionPredictMonthlyOutcome: Make monthly price prediction
CreateMarket: Create new point trading market (Level 5+)BuyShares: Purchase points from a marketSellShares: Sell points to a market (Level 5+)
CreateGuild: Form a new guildJoinGuild: Join existing guildLeaveGuild: Leave current guildContributeToGuild: Add points to guild pool
UpdateGameConfig: Modify game parametersMintPoints: Create additional point supplyUpdateMarketPrice: Update crypto prices from oracle
- Authentication: All operations require authenticated signer
- Authorization: Admin-only operations protected
- Level Requirements: Market creation and selling restricted by level
- Balance Checks: Prevent overdraft on all transactions
- Fee Validation: Market fees capped at 100%
- Cross-Chain Security:
- Message deduplication prevents replay attacks
- Timestamp-based conflict resolution prevents race conditions
- Idempotent operations safe to retry
- Message authentication ensures integrity
- Fuzz Testing: Property-based and coverage-guided fuzzing to find edge cases and vulnerabilities
- Initial player allocation (configurable)
- Daily login rewards
- Prediction rewards (correct guesses)
- Achievement rewards
- Market creation fees (100 points → platform)
- Trading fees (2% → platform, 98% → market creator)
- Wrong predictions (100-1000 points)
- Market creation cost (100 points)
- Trading transaction fees
- Selling points to markets
The exchange rate scales with player level to reward progression:
- Buying Formula:
payment = desired_points / 10 - Example Level 1: Want 100 points? Pay 10 points
- Example Level 2: Want 1000 points? Pay 100 points
- Example Level 5: Want 10000 points? Pay 1000 points
This creates natural market dynamics where higher-level players can make larger trades.
Players are ranked by total points earned (total_earned), which includes:
- Initial allocation
- Daily rewards
- Prediction rewards
- Achievement rewards
- Trading profits
Guilds are ranked by sum of all member earnings, encouraging guilds to recruit active traders and predictors.
The prediction system relies on external price data from cryptocurrency APIs:
- Admin/Oracle calls
UpdateMarketPricewith latest crypto price - Contract captures initial price at period start (from crypto API)
- Contract captures end price at period end (from crypto API)
- Outcome Calculation:
- Rise:
end_price > initial_price - Fall:
end_price < initial_price - Neutral:
end_price == initial_price
- Rise:
- Players' predictions compared to actual outcome
- Rewards/penalties distributed automatically
- System Overview
- Contract Architecture
- State Management
- Core Subsystems
- Economic Model
- Data Flow
- Security Architecture
- Oracle Integration
- Scalability Considerations
┌─────────────────────────────────────────────────────────────┐
│ Roxy Price Platform │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Price │ │ Market │ │ Guild │ │
│ │ Prediction │ │ Trading │ │ System │ │
│ │ Engine │ │ System │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Player │ │ Achievement │ │ Leaderboard │ │
│ │ Management │ │ System │ │ System │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ Linera Smart Contract Layer │
├─────────────────────────────────────────────────────────────┤
│ Linera Blockchain │
└─────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
Players API Oracle API Admin API
The contract follows the Linera Contract Pattern:
- State: Persistent storage using Linera Views
- Runtime: Access to blockchain context (time, chain ID, authentication)
User Request
↓
Authentication Check (runtime.authenticated_signer())
↓
Operation Validation (balance, level, permissions)
↓
State Mutation (update MapView/RegisterView)
↓
Side Effects (messages, events, rewards)
↓
State Persistence (automatic via Linera SDK)
The contract uses Linera's message system for event broadcasting:
pub enum Message {
MarketCreated { market_id: MarketId, creator: PlayerId },
MarketResolved { market_id: MarketId, outcome_id: OutcomeId },
TradeExecuted { player_id, market_id, outcome_id, shares, price },
PlayerLeveledUp { player_id: PlayerId, new_level: u32 },
AchievementUnlocked { player_id: PlayerId, achievement_id: u64 },
GuildCreated { guild_id: GuildId, name: String },
PredictionMade { player_id, period, outcome },
PredictionResolved { player_id, period, correct: bool },
}Messages are sent to the current chain for event tracking and frontend updates.
pub struct PredictionMarketState {
// Core Configuration
pub config: RegisterView<GameConfig>,
pub total_supply: RegisterView<Amount>,
pub next_market_id: RegisterView<MarketId>,
// Player Data
pub players: MapView<PlayerId, Player>,
// Market Data
pub markets: MapView<MarketId, Market>,
// Guild Data
pub guilds: MapView<GuildId, Guild>,
// Achievement System
pub achievements: MapView<u64, Achievement>,
// Leaderboard
pub leaderboard: RegisterView<Leaderboard>,
// Price Prediction System
pub current_market_price: RegisterView<MarketPrice>,
pub predictions: MapView<String, PlayerPrediction>,
pub period_prices: MapView<String, PeriodPriceData>,
}pub struct Player {
id: PlayerId,
display_name: Option<String>,
registration_time: Timestamp,
last_login: Timestamp,
// Economics
token_balance: Amount,
total_earned: Amount,
total_spent: Amount,
// Progression
level: u32,
experience_points: u64,
reputation: u32,
// Statistics
markets_participated: u32,
markets_won: u32,
total_profit: Amount,
win_streak: u32,
best_win_streak: u32,
// Social
guild_id: Option<GuildId>,
achievements_earned: Vec<u64>,
active_markets: Vec<MarketId>,
}pub struct Market {
id: MarketId,
creator: PlayerId,
title: String,
amount: Amount, // Initial liquidity
fee_percent: u8, // Creator's fee (0-100)
creation_time: Timestamp,
status: MarketStatus,
total_liquidity: Amount, // Current liquidity
positions: BTreeMap<PlayerId, PlayerPosition>,
total_participants: u32,
}pub struct PlayerPrediction {
player_id: PlayerId,
period: PredictionPeriod, // Daily/Weekly/Monthly
outcome: PriceOutcome, // Rise/Fall/Neutral
prediction_time: Timestamp,
period_start: Timestamp,
resolved: bool,
correct: Option<bool>,
}
pub struct PeriodPriceData {
period_start: Timestamp,
period_end: Timestamp,
start_price: Option<MarketPrice>, // From crypto API
end_price: Option<MarketPrice>, // From crypto API
outcome: Option<PriceOutcome>,
resolved: bool,
}MapView Usage: For indexed collections with unique keys
// Player lookup by ID
self.state.players.get(&player_id).await?
// Market lookup by ID
self.state.markets.get(&market_id).await?
// Prediction lookup by composite key
let key = format!("{:?}_{:?}_{}", player_id, period, period_start);
self.state.predictions.get(&key).await?RegisterView Usage: For single global values
// Game configuration
self.state.config.get()
// Total point supply
self.state.total_supply.get()
// Current market price
self.state.current_market_price.get()Responsibilities:
- Player registration and profile management
- Experience and leveling system
- Daily reward distribution
- Balance management
Key Functions:
async fn register_player(player_id, display_name, current_time)
async fn update_player_profile(player_id, display_name)
async fn claim_daily_reward(player_id, current_time)
async fn add_experience(player: &mut Player, xp: u64)Leveling Algorithm:
Level 1: 1,000 total XP
Level 2: 4,000 total XP (4x multiplier)
Level 3: 16,000 total XP (4x multiplier)
Level N: 1,000 × 4^(N-1) total XP
Formula: XP_required(N) = 1000 × 4^(N-1)
This exponential progression encourages continuous engagement while becoming progressively challenging.
Responsibilities:
- Market creation and lifecycle management
- Point buying and selling
- Fee calculation and distribution
- Liquidity management
Market Creation Flow:
1. Validate: Level ≥ 5, Balance ≥ 10,000 points
2. Deduct: 100 point creation fee
3. Create: New market with custom fee percentage
4. Distribute: Creation fee to platform (total_supply)
5. Emit: MarketCreated message
Trading Flow (Buy):
1. Validate: Market active, sufficient buyer balance
2. Calculate:
- Base payment = desired_points / 10
- Fee = base_payment × market.fee_percent / 100
- Total payment = base_payment + fee
3. Transfer:
- Buyer pays: total_payment
- Buyer receives: desired_points (from market liquidity)
- Creator receives: base_payment (98% after platform cut)
- Platform receives: 2% of fee
4. Update: Market liquidity, player positions
5. Emit: TradeExecuted message
Progressive Exchange Rate:
// Level-agnostic formula (works for all levels 1 to ∞)
let payment_tokens = desired_points_tokens / 10;
Examples:
- Want 100 points → pay 10 points (10:1 ratio)
- Want 1,000 points → pay 100 points (10:1 ratio)
- Want 10,000 points → pay 1,000 points (10:1 ratio)This creates a consistent 10% cost regardless of level, maintaining economic balance.
Architecture:
┌──────────────────────────────────────────────────────┐
│ Oracle/Admin │
│ (Fetches from Crypto APIs) │
└───────────────────┬──────────────────────────────────┘
│ update_market_price(price)
▼
┌──────────────────────────────────────────────────────┐
│ Smart Contract Storage │
│ - current_market_price (latest from API) │
│ - period_prices (start_price, end_price) │
│ - predictions (player predictions) │
└───────────────────┬──────────────────────────────────┘
│ resolve_expired_predictions()
▼
┌──────────────────────────────────────────────────────┐
│ Prediction Resolution │
│ 1. Compare end_price vs start_price (both from API) │
│ 2. Determine outcome: Rise/Fall/Neutral │
│ 3. Check player predictions │
│ 4. Award/penalize players and guilds │
└──────────────────────────────────────────────────────┘
Prediction Flow:
1. Player submits prediction (Rise/Fall/Neutral)
2. Contract captures initial_price from current_market_price
3. Period elapses (24h for daily, 7d for weekly, 30d for monthly)
4. Oracle updates current_market_price with latest crypto API price
5. Contract captures end_price from current_market_price
6. Contract calculates outcome: compare end_price vs initial_price
7. Contract resolves player prediction (correct/incorrect)
8. Contract distributes rewards or penalties
Outcome Calculation:
fn calculate_outcome_from_prices(initial_price, end_price) -> PriceOutcome {
match end_price.cmp(&initial_price) {
Greater => PriceOutcome::Rise, // Price increased
Less => PriceOutcome::Fall, // Price decreased
Equal => PriceOutcome::Neutral, // Price unchanged
}
}Reward/Penalty Schedule:
| Period | Correct Reward | Wrong Penalty | XP Reward |
|---|---|---|---|
| Daily | 100 points | -100 points | 50 XP |
| Weekly | 500 points | -500 points | 250 XP |
| Monthly | 1000 points | -1000 points | 500 XP |
Guild Multiplier: When a guild member makes a prediction:
- Correct: ALL guild members receive the reward
- Wrong: ALL guild members lose the penalty amount
This creates strong incentives for guild coordination and collective decision-making.
Guild Model:
pub struct Guild {
id: GuildId,
name: String,
founder: PlayerId,
members: Vec<PlayerId>,
creation_time: Timestamp,
total_guild_profit: Amount,
guild_level: u32,
shared_pool: Amount,
}Guild Operations:
- Create: Founder creates guild (must not be in another guild)
- Join: Players join existing guilds
- Leave: Members can leave guilds
- Contribute: Members add points to shared pool
- Collective Rewards: All members earn from member predictions
Guild Ranking Algorithm:
// Sum all member earnings
guild_total_points = sum(member.total_earned for each member)
// Sort guilds by total points (descending)
// Top 20 displayed on leaderboardAchievement Triggers:
pub enum AchievementRequirement {
CreateMarket, // Create first market
FirstBuy, // Make first purchase
FirstSell, // Make first sale
JoinGuild, // Join a guild
ReachLevel(u32), // Reach specific level
WinMarkets(u32), // Win N markets
WinStreak(u32), // Achieve N win streak
TotalProfit(Amount), // Earn total profit
ParticipateInMarkets(u32), // Participate in N markets
CreateMarkets(u32), // Create N markets
}Achievement Check Flow:
1. Player completes action (e.g., creates market)
2. Contract calls check_achievements(&mut player)
3. For each achievement:
- Check if player already earned it
- Check if requirement met
- If yes: award tokens + XP, add to player.achievements_earned
4. Emit AchievementUnlocked message
5. Update player state
Rewards Distribution:
Achievement::award() {
player.token_balance += achievement.reward_tokens;
player.total_earned += achievement.reward_tokens;
player.experience_points += achievement.reward_xp;
total_supply += achievement.reward_tokens;
}Ranking Algorithms:
Player Ranking:
// Rank by total_earned (all-time earnings)
player_score = player.total_earned
// Calculate win rate for display
win_rate = (markets_won / markets_participated) × 100
// Sort descending, take top 50Guild Ranking:
// Sum all member total_earned
guild_score = sum(member.total_earned for each guild.member)
// Sort descending, take top 20Update Triggers:
- Market creation fee distribution
- Trading fee distribution
- Prediction reward distribution
Leaderboard Model:
pub struct Leaderboard {
top_traders: Vec<LeaderboardEntry>, // Top 50 players
top_guilds: Vec<GuildLeaderboardEntry>, // Top 20 guilds
last_updated: Timestamp,
}Sources (Minting):
- Initial player allocation (configurable, default: varies)
- Daily login rewards
- Correct predictions (100/500/1000 points)
- Guild prediction rewards (multiplied by member count)
- Achievement rewards (50-1000 points)
- Market creation fees (100 points → platform)
- Trading platform fees (2% of creator fee)
Sinks (Burning):
- Wrong predictions (100/500/1000 points)
- Guild prediction penalties (multiplied by member count)
- Market trading (net burn after fees)
Total Supply Tracking:
self.state.total_supply.get() // Current circulating supply
self.state.total_supply.set(new_supply) // Update supplyMarket Creation:
Cost: 100 points (flat fee)
Distribution: 100% to platform (total_supply)
Trading Fees:
Market Creator Fee: X% (set by creator, 0-100%)
Platform Cut: 2% of creator fee
Creator Keeps: 98% of creator fee
Example with 10% creator fee on 1000 point trade:
- Buyer pays: 1000 + 100 (fee) = 1100 points
- Creator fee: 100 points
- Platform gets: 2 points (2% of 100)
- Creator keeps: 98 points (98% of 100)
Buy Formula:
base_payment = desired_points / 10
creator_fee = base_payment × market.fee_percent / 100
total_payment = base_payment + creator_fee
platform_cut = creator_fee × 0.02
creator_keeps = creator_fee × 0.98Sell Formula:
creator_fee = amount × market.fee_percent / 100
points_to_market = amount - creator_fee
platform_cut = creator_fee × 0.02
creator_keeps = creator_fee × 0.98Overflow Protection:
// All arithmetic uses saturating operations
player.token_balance = player.token_balance.saturating_add(reward);
player.token_balance = player.token_balance.saturating_sub(cost);Underflow Protection:
// Check balance before deducting
if player.token_balance < cost {
return Err(ContractError::InsufficientBalance);
}Zero-Floor Penalties:
// For prediction penalties, set balance to zero if insufficient
if player.token_balance >= penalty {
player.token_balance = player.token_balance.saturating_sub(penalty);
} else {
player.token_balance = Amount::ZERO;
}User → RegisterPlayer{display_name}
↓
Check: Player doesn't exist
↓
Create: Player{initial_tokens, level: 1, xp: 0}
↓
Update: total_supply += initial_tokens
↓
Store: players.insert(player_id, player)
↓
Response: Success
User → CreateMarket{title, amount, fee_percent}
↓
Validate: level ≥ 5, balance ≥ 10,000, balance ≥ 100 (fee)
↓
Deduct: player.balance -= 100 (creation fee)
↓
Create: Market{creator, title, amount, fee_percent, status: Active}
↓
Distribute: total_supply += 100 (platform fee)
↓
Update: player.active_markets.push(market_id)
↓
Check: Achievements (Market Creator)
↓
Emit: MarketCreated message
↓
Response: Success
User → BuyShares{market_id, amount}
↓
Validate: Market active, market has liquidity
↓
Calculate:
- points_to_receive = min(amount, market.liquidity)
- base_payment = points_to_receive / 10
- creator_fee = base_payment × market.fee_percent / 100
- total_payment = base_payment + creator_fee
↓
Validate: player.balance ≥ total_payment
↓
Transfer:
- player.balance -= total_payment
- player.balance += points_to_receive (from market)
- market.liquidity -= points_to_receive
- creator.balance += base_payment
- Distribute creator_fee (98% creator, 2% platform)
↓
Update: Position, XP (+10), statistics
↓
Check: Achievements (First Buyer)
↓
Emit: TradeExecuted message
↓
Response: Success
User → PredictDailyOutcome{outcome}
↓
Calculate: period_start (current day start)
↓
Validate: No existing prediction for this period
↓
Create: PlayerPrediction{player_id, period, outcome, period_start}
↓
Initialize: PeriodPriceData{start_price: current_market_price}
↓
Store: predictions.insert(key, prediction)
↓
Emit: PredictionMade message
↓
[Wait for period to end]
↓
Oracle → UpdateMarketPrice{price} (at period end)
↓
Capture: period_data.end_price = current_market_price
↓
Calculate: outcome = compare(end_price, start_price)
↓
Resolve: prediction.correct = (prediction.outcome == outcome)
↓
If Correct:
- Player: +100/500/1000 points, +XP
- Guild: All members +100/500/1000 points, +XP
- Update: total_supply
If Wrong:
- Player: -100/500/1000 points
- Guild: All members -100/500/1000 points
- Burn: from total_supply
↓
Emit: PredictionResolved message
↓
Update: Leaderboard
Oracle/Admin → UpdateMarketPrice{price}
↓
Validate: Caller is admin
↓
Update: current_market_price = {price, timestamp}
↓
Resolve Expired Predictions:
↓
For each period (Daily/Weekly/Monthly):
↓
Check: current_time ≥ period_end
↓
If period ended:
- Set: period_data.end_price = current_market_price
- Calculate: outcome = compare(end_price, start_price)
- Resolve: All predictions for this period
- Award/Penalize: Players and guilds
- Emit: PredictionResolved messages
↓
Response: Success
Signer-Based Auth:
let player_id = self.runtime.authenticated_signer().unwrap();Every operation requires an authenticated signer. The blockchain ensures the signer owns the private key for the player_id.
Admin-Only Operations:
let config = self.state.config.get();
if let Some(admin) = config.admin {
if caller != admin {
return Err(ContractError::NotAdmin);
}
}Admin operations:
UpdateGameConfig: Modify game parametersMintPoints: Create point supplyUpdateMarketPrice: Oracle price updates
Level-Based Restrictions:
// Market creation requires Level 5
if player.level < 5 {
return Err(ContractError::InsufficientLevel);
}
// Selling requires Level 5
if player.level < 5 {
return Err(ContractError::InsufficientLevel);
}Balance Checks:
// Before any deduction
if player.token_balance < cost {
return Err(ContractError::InsufficientBalance);
}Fee Validation:
// Market creator fee capped at 100%
if fee_percent > 100 {
return Err(ContractError::InvalidOutcome);
}Cooldown Validation:
// Daily reward: 24-hour cooldown
let time_diff = current_time.micros() - player.last_login.micros();
let one_day_micros = 24 * 60 * 60 * 1_000_000;
if time_diff < one_day_micros {
return Err(ContractError::DailyRewardAlreadyClaimed);
}Duplicate Prevention:
// Prevent duplicate predictions for same period
let prediction_key = format!("{:?}_{:?}_{}", player_id, period, period_start);
if self.state.predictions.contains_key(&prediction_key).await? {
return Err(ContractError::InvalidOutcome);
}Atomic Updates: All state changes within a single operation are atomic. If any step fails, the entire transaction reverts.
View Isolation: Linera Views ensure concurrent operations don't interfere:
// Each MapView key is independently locked
self.state.players.insert(&player_id, player)?;
self.state.markets.insert(&market_id, market)?;Error Handling:
#[derive(Error, Debug)]
pub enum ContractError {
#[error("unauthorized")] Unauthorized,
#[error("player already exists")] PlayerAlreadyExists,
#[error("insufficient balance")] InsufficientBalance,
// ... comprehensive error types
}┌──────────────────────────────────────────────────────┐
│ External Crypto Price APIs │
│ (CoinMarketCap, CoinGecko, Binance, etc.) │
└───────────────────┬──────────────────────────────────┘
│ HTTPS API Calls
▼
┌──────────────────────────────────────────────────────┐
│ Oracle Service │
│ - Fetches latest crypto prices │
│ - Converts to Amount type │
│ - Calls smart contract │
└───────────────────┬──────────────────────────────────┘
│ update_market_price(price)
▼
┌──────────────────────────────────────────────────────┐
│ Roxy Price Smart Contract │
│ - Stores price in current_market_price │
│ - Updates period_prices (start/end) │
│ - Resolves expired predictions │
│ - Distributes rewards/penalties │
└──────────────────────────────────────────────────────┘
Update Function:
async fn update_market_price(
&mut self,
caller: PlayerId, // Must be admin
price: Amount, // Price from crypto API
current_time: Timestamp,
) -> Result<(), ContractError>Price Storage:
pub struct MarketPrice {
price: Amount, // Latest price from API
timestamp: Timestamp, // When price was captured
}Period Tracking:
pub struct PeriodPriceData {
period_start: Timestamp,
period_end: Timestamp,
start_price: Option<MarketPrice>, // Captured at period start
end_price: Option<MarketPrice>, // Captured at period end
outcome: Option<PriceOutcome>, // Rise/Fall/Neutral
resolved: bool,
}Off-Chain Oracle Service (pseudo-code):
# Oracle service runs continuously
while True:
# Fetch latest BTC price from CoinGecko
response = requests.get('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd')
btc_price = response.json()['bitcoin']['usd']
# Convert to Amount (assuming 1 point = $1 USD)
price_amount = Amount.from_tokens(btc_price)
# Call smart contract as admin
contract.update_market_price(
caller=admin_account,
price=price_amount,
current_time=now()
)
# Wait before next update
time.sleep(60) # Update every minuteAutomatic Resolution:
When update_market_price is called, the contract automatically:
- Checks Expired Periods:
for period in [Daily, Weekly, Monthly] {
if current_time >= period_end {
// Period has ended
}
}- Captures End Price:
if period_data.end_price.is_none() {
period_data.end_price = Some(current_market_price.clone());
}- Resolves Predictions:
for each prediction in period {
outcome = calculate_outcome(start_price, end_price);
is_correct = (prediction.outcome == outcome);
award_or_penalize(player, is_correct);
}On-Demand Resolution: When players query their prediction results:
async fn get_daily_outcome(&mut self, player_id: PlayerId) -> Result<bool, ContractError> {
let prediction = self.get_prediction(player_id, Daily)?;
if !prediction.resolved {
self.resolve_prediction(&mut prediction, Daily, period_start).await?;
}
Ok(prediction.correct.unwrap_or(false))
}Key Strategies:
- Use composite keys for predictions:
"{player_id}_{period}_{period_start}" - Limit leaderboard to top 50 players and top 20 guilds
- Period prices stored per period (not per player)
- Achievements stored globally (not duplicated per player)
Storage Growth:
Players: O(n) where n = number of players
Markets: O(m) where m = number of markets
Predictions: O(n × p × t) where:
- n = number of players
- p = prediction periods (3: daily/weekly/monthly)
- t = time periods (grows continuously)
Guilds: O(g) where g = number of guilds
Achievements: O(7) fixed (7 predefined achievements)
Mitigation Strategies:
- Implement prediction archival (move old predictions to cold storage)
- Set max active markets per player (currently unlimited)
- Periodic leaderboard snapshots (reduce recalculation frequency)
- Lazy resolution (resolve on-demand vs. batch resolution)
Roxy Price implements a robust cross-chain messaging system that enables the application to scale horizontally across multiple Linera chains while maintaining global state consistency.
┌─────────────────────────────────────────────────────────────┐
│ Chain 1 (Roxy Instance) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Local State: Players, Markets, Guilds (Chain 1) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Global State: Cross-Chain Registry │ │
│ │ - global_players: All players across all chains │ │
│ │ - global_markets: All markets across all chains │ │
│ │ - global_guilds: All guilds across all chains │ │
│ │ - subscribed_chains: Chain registry │ │
│ └────────────────────────────────────────────────────────┘ │
└───────────────────┬───────────────────────────────────────────┘
│ Messages
▼
┌───────────────────────┐
│ Linera Message Bus │
│ (Authenticated & │
│ Tracked Messages) │
└───────────────────────┘
▲
│ Messages
┌───────────────────┴───────────────────────────────────────────┐
│ Chain 2 (Roxy Instance) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Local State: Players, Markets, Guilds (Chain 2) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Global State: Cross-Chain Registry (Synced) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Unique Message IDs: Each cross-chain message includes a unique ID based on content, chain, and timestamp
- Processed Message Tracking:
processed_message_idsMapView tracks all processed messages - Idempotent Operations: Messages can be safely retried without side effects
- Timestamp-Based Ordering:
GlobalPlayerUpdatedmessages include timestamps - Last-Write-Wins: Only newer updates are applied (timestamp > existing
last_updated) - Price Validation:
GlobalPriceUpdateonly applies if incoming timestamp is newer
- Player Registration: Only registers if player doesn't already exist
- Market Creation: Only creates if market doesn't already exist
- Guild Creation: Only creates if guild doesn't already exist
All cross-chain messages include:
message_id: Unique identifier for deduplicationchain_id: Source chain identifiertimestamp: For conflict resolution (where applicable)
Message Types:
GlobalPlayerRegistered: Broadcast player registrationGlobalPlayerUpdated: Broadcast player state changes (with timestamp)GlobalMarketCreated: Broadcast market creationGlobalGuildCreated: Broadcast guild creationGlobalPriceUpdate: Broadcast price updates (with timestamp validation)ChainRegistered: Chain discovery and registration
Global Registries:
global_players: All players across all chains with their current chain locationglobal_markets: All markets with their origin chainglobal_guilds: All guilds with their origin chainglobal_leaderboard: Aggregated leaderboard across all chains
Update Flow:
- Local operation occurs on Chain A
- Chain A updates its local state
- Chain A broadcasts message to all subscribed chains
- Receiving chains update their global registries
- Global leaderboard recalculated on each chain
- Message Authentication: All messages use
.with_authentication() - Message Tracking: All messages use
.with_tracking()for delivery guarantees - Error Handling: Proper error propagation and state consistency
- Network Resilience: Safe to retry on network failures
- Docker and Docker Compose installed
- (Optional) Rust 1.86.0+ for local development
- Git for version control
# Clone the repository
git clone <repository-url>
cd roxy/roxy
# Start development container
docker-compose up -d roxy-dev
# Access container shell
docker-compose exec roxy-dev bash
# Build and test
cargo build --release
cargo test --jobs 1-
roxy-dev: Development container with all build tools
- Rust 1.86.0
- wasm32-unknown-unknown target
- clippy, rustfmt tools
- Volume-mounted for live code editing
-
roxy: Production container with built binaries
- Minimal runtime image
- Optimized WASM binaries
- Production-ready deployment
Using Makefile:
make build # Build the project
make test # Run tests
make docker-dev # Start dev container
make docker-shell # Access container shellUsing Docker Compose:
docker-compose exec roxy-dev cargo build
docker-compose exec roxy-dev cargo test --jobs 1
docker-compose exec roxy-dev cargo clippy
docker-compose exec roxy-dev cargo fmt --checkFor large builds, the container is configured with:
CARGO_BUILD_JOBS=2: Limits parallel compilationCARGO_INCREMENTAL=1: Enables incremental compilation.cargo/config.toml: Build job limits
If you encounter linker errors (signal 9), try:
# Run with single job
docker-compose exec roxy-dev cargo test --jobs 1
# Or increase Docker Desktop memory limit to 6-8GB# Install Rust 1.86.0
rustup install 1.86.0
rustup default 1.86.0
# Install wasm32 target
rustup target add wasm32-unknown-unknown
# Install tools
rustup component add clippy rustfmt rust-src
# Install system dependencies (Ubuntu/Debian)
sudo apt-get install -y pkg-config libssl-dev protobuf-compiler
# Build and test
cargo build --release
cargo build --release --target wasm32-unknown-unknown
cargo testSee QUICK_START.md for detailed Docker setup instructions.
The project includes fuzzing capabilities to find bugs and security vulnerabilities:
Property-Based Testing (proptest):
# Run property-based fuzz tests
cargo test --test fuzz_tests
# Or in Docker
docker-compose exec roxy-dev cargo test --test fuzz_testsWhat Gets Fuzzed:
- Real contract operations (player registration, market creation, trading, predictions, guilds)
- Operation input validation (fees 0-100, amounts, strings)
- State transitions (player operations, market trading)
- Arithmetic operations (overflow/underflow protection)
- Edge cases (boundaries, invalid inputs)
- State invariants:
- Total supply never negative
- Total supply consistent with mint/spend operations
- Balances never negative
- Cross-chain message handling:
- Message deduplication (through operations on multiple chains)
- Timestamp-based conflict resolution (through price updates)
Key Strategies:
- Use composite keys for predictions:
"{player_id}_{period}_{period_start}" - Limit leaderboard to top 50 players and top 20 guilds
- Period prices stored per period (not per player)
- Achievements stored globally (not duplicated per player)
- Message deduplication tracking to prevent unbounded growth
Storage Growth:
Players: O(n) where n = number of players
Markets: O(m) where m = number of markets
Predictions: O(n × p × t) where:
- n = number of players
- p = prediction periods (3: daily/weekly/monthly)
- t = time periods (grows continuously)
Guilds: O(g) where g = number of guilds
Achievements: O(7) fixed (7 predefined achievements)
Processed Messages: O(m) where m = cross-chain messages sent
Mitigation Strategies:
- Implement prediction archival (move old predictions to cold storage)
- Set max active markets per player (currently unlimited)
- Periodic leaderboard snapshots (reduce recalculation frequency)
- Lazy resolution (resolve on-demand vs. batch resolution)
- Message ID expiration (TTL for old message IDs - future enhancement)
Operation Complexities:
| Operation | Time Complexity | Notes |
|---|---|---|
| RegisterPlayer | O(1) | Single map insertion |
| CreateMarket | O(1) | Single map insertion + checks |
| BuyShares | O(1) | Direct map access |
| SellShares | O(1) | Direct map access |
| PredictOutcome | O(1) | Single map insertion |
| ResolveExpiredPredictions | O(n × p) | n players, p periods |
| UpdateLeaderboard | O(n log n) | Sort all players/guilds |
| CheckAchievements | O(a) | a = number of achievements (fixed 7) |
| BroadcastCrossChainMessage | O(c) | c = number of subscribed chains |
| ProcessCrossChainMessage | O(1) | Message deduplication check + state update |
Performance Bottlenecks:
-
Leaderboard Updates: O(n log n) for sorting all players
- Solution: Cache leaderboard, update periodically (not per transaction)
- Implementation: Only update after significant events (fee distributions, predictions)
-
Prediction Resolution: O(n) for resolving all player predictions in a period
- Solution: Lazy resolution (resolve when queried)
- Current: Hybrid approach (batch resolve on price update, on-demand otherwise)
-
Guild Member Iteration: O(m) where m = guild size
- Solution: Cap guild size (currently unlimited)
- Recommendation: Add max_guild_members config parameter
