Skip to main content

DUBS V2: Pool-Based Betting Protocol

Whitepaper v1.0 | January 2025

Table of Contents

  1. Introduction
  2. System Overview
  3. Core Concepts
  4. Account Architecture
  5. Game Lifecycle
  6. Fee Structure
  7. Payout Mechanics
  8. Sponsored Betting
  9. Emergency Mechanisms
  10. Edge Cases
  11. Security Model
  12. Technical Specifications

1. Introduction

DUBS V2 is a decentralized pool-based (pari-mutuel) betting protocol built on Solana. It enables trustless, transparent wagering on sports events with automatic, proportional payouts to winners.

1.1 Design Philosophy

  • Trustless: No custody of funds by operators; all funds held in program-derived accounts
  • Transparent: All pool sizes, stakes, and payouts verifiable on-chain
  • Scalable: Position PDA architecture supports unlimited participants per game
  • Fair: Proportional payouts ensure mathematical fairness regardless of pool imbalance
  • Resilient: Multiple emergency mechanisms prevent permanent fund lockup

1.2 Key Features

FeatureDescription
Pool-Based PayoutsWinners share the pool proportionally based on stake
Variable StakesAny positive amount accepted (no fixed buy-in)
Three OutcomesHome, Away, and Draw as first-class betting options
Position PDAsOne account per player per game for scalability
Referral System1% commission for referrers who bring players
Sponsored BettingThird parties can fund bets on behalf of players
Emergency RefundsMultiple fail-safes to protect player funds

2. System Overview

2.1 Participants

Player

Stakes SOL, claims payouts, wins or loses based on outcomes

Operator

Creates games, earns 4-5% fee

Oracle

Resolves game outcomes, earns 1% fee

Referrer

Brings players, earns 1% commission

Sponsor

Funds bets on behalf of players

Authority

Emergency controls and overrides

2.2 How It Works

1

Game Creation

Operator creates a game for a sports event with a lock time
2

Betting Period

Players stake SOL on Home, Away, or Draw outcomes
3

Lock

At lock time, no more bets accepted
4

Resolution

Oracle reports the winning outcome
5

Claims

Winners claim proportional share of the pool

3. Core Concepts

3.1 Pari-Mutuel Betting

Unlike fixed-odds betting where payouts are determined at bet time, pari-mutuel systems pool all stakes together. Winners share the pool proportionally based on their stake in the winning outcome.
Game: Team A vs Team B

Pool Totals:
  - Home (Team A): 100 SOL
  - Away (Team B): 50 SOL
  - Draw: 10 SOL
  - Total Pool: 160 SOL

After 6% fee: Net Pool = 150.4 SOL

If Home wins:
  - Player who staked 10 SOL on Home
  - Payout = (10 / 100) ร— 150.4 = 15.04 SOL
  - Profit = 15.04 - 10 = 5.04 SOL (50.4% return)

3.2 Implied Odds

The pool ratios create dynamic, market-driven odds:
Implied Probability = Outcome Pool / Total Pool
Example:
OutcomePoolImplied Probability
Home100 SOL62.5%
Away50 SOL31.25%
Draw10 SOL6.25%

3.3 Position PDAs

Each playerโ€™s stake is recorded in a unique Program Derived Address (PDA):
Seeds: ["position", game_pubkey, player_pubkey]
This architecture:
  • Enables unlimited participants per game
  • Prevents vector growth attacks
  • Allows parallel claim processing
  • Provides clear ownership verification

4. Account Architecture

4.1 GameV2 Account

The central game account holding all pool funds and state.
pub struct GameV2 {
    // Identity
    pub game_id: u64,              // Unique identifier
    pub authority: Pubkey,          // Game creator
    pub operator_wallet: Pubkey,    // Fee recipient (hardcoded)
    pub oracle: Pubkey,             // Resolution authority (hardcoded)

    // Pool State
    pub pool_totals: [u64; 3],      // [Home, Away, Draw] in lamports
    pub total_pool: u64,            // Sum of all pools
    pub position_count: u32,        // Number of positions created

    // Event Metadata
    pub sports_event_id: String,    // External event reference (max 50 chars)
    pub lock_timestamp: i64,        // Unix timestamp when betting closes

    // Game State
    pub is_locked: bool,            // Betting closed
    pub is_resolved: bool,          // Outcome determined
    pub winning_outcome: Option<u8>,// 0=Home, 1=Away, 2=Draw, None=Refund

    // Financial
    pub fees_collected: bool,       // Fees transferred to recipients
    pub net_pool: u64,              // Pool after fees (for payout calc)

    // Optional
    pub sponsor_wallet: Option<Pubkey>,  // If game has sponsored positions
    pub is_emergency_refund: bool,       // True if emergency refund triggered

    pub bump: u8,                   // PDA bump seed
}
PDA Derivation:
Seeds: ["game_v2", game_id.to_le_bytes()]
Account Size: ~250 bytes (fixed, no vectors)

4.2 Position Account

Individual player stake record.
pub struct Position {
    pub game: Pubkey,              // Reference to GameV2
    pub player: Pubkey,            // Player wallet
    pub outcome: u8,               // 0=Home, 1=Away, 2=Draw
    pub amount: u64,               // Stake in lamports
    pub claimed: bool,             // Payout claimed
    pub referrer: Option<Pubkey>,  // Referrer for fee sharing
    pub sponsor: Option<Pubkey>,   // Sponsor (if sponsored stake)
    pub bump: u8,                  // PDA bump seed
}
PDA Derivation:
Seeds: ["position", game.key(), player.key()]
Account Size: ~140 bytes (fixed)

4.3 Outcome Enum

#[repr(u8)]
pub enum Outcome {
    Home = 0,  // Home team / First option
    Away = 1,  // Away team / Second option
    Draw = 2,  // Tie / No winner
}

5. Game Lifecycle

5.1 State Machine

5.2 Instructions

Creates a new game.Parameters:
  • game_id: u64 - Unique identifier
  • lock_timestamp: i64 - When betting closes (must be โ‰ฅ2 minutes in future)
  • sports_event_id: String - External event reference
Validation:
  • Lock time must be at least 2 minutes in the future
  • Game ID must not already exist
Player places a bet.Parameters:
  • game_id: u64 - Game to bet on
  • outcome: Outcome - Home, Away, or Draw
  • amount: u64 - Stake in lamports
Effects:
  1. Transfers amount from player to game account
  2. Creates Position PDA for player
  3. Updates pool_totals and total_pool
  4. Increments position_count
Validation:
  • Game not locked or resolved
  • Amount > 0
  • Valid outcome (0, 1, or 2)
Sponsor funds a bet on behalf of a player.Parameters: Same as stake_automatic_gameKey Difference:
  • Funds come from sponsor wallet, not player
  • Position.sponsor is set
  • On refund, funds return to sponsor (not player)
  • Player still claims winnings if they win
Oracle resolves the game outcome.Parameters:
  • game_id: u64 - Game to resolve
  • winning_outcome: Option<Outcome> - Winner, or None for refund
Effects:
  1. Calculates and transfers fees
  2. Sets winning_outcome and net_pool
  3. Marks game as resolved
Validation:
  • Caller must be the designated oracle
  • Game must be locked (or past lock time)
  • Game not already resolved
Player claims their payout.Parameters:
  • game_id: u64 - Game to claim from
Effects:
  1. Calculates payout based on outcome
  2. Transfers payout from game to player (or sponsor for refunds)
  3. Marks position as claimed
Payout Logic:
  • Winner: Proportional share of net_pool based on stake in winning pool
  • Loser: 0 (position still marked claimed)
  • Refund: Proportional share based on stake in total pool
Permissionless emergency refund after 24 hours.Parameters:
  • game_id: u64 - Game to refund
Validation:
  • Game is locked but not resolved
  • At least 24 hours have passed since lock time
Effects:
  • Sets winning_outcome = None (refund all)
  • No fees collected
  • All players can claim full refund
Authority can trigger emergency refund at any time.Validation:
  • Caller must be program authority
  • Game not already resolved
Effects:
  • Same as emergency_refund_v2
  • No time restriction

6. Fee Structure

6.1 Fee Breakdown

Total fee is 6% of the distributable pool (pool minus rent reserve).
RecipientWith ReferrerWithout Referrer
Operator4%5%
Oracle1%1%
Referrer1%0%
Total6%6%

6.2 Fee Calculation

fn calculate_fees(distributable: u64, has_referrer: bool) -> FeeBreakdown {
    let total_fee = distributable * 600 / 10_000;  // 6%

    let oracle_fee = distributable * 100 / 10_000;  // 1%

    let (operator_fee, referrer_fee) = if has_referrer {
        (distributable * 400 / 10_000,  // 4%
         distributable * 100 / 10_000)  // 1%
    } else {
        (distributable * 500 / 10_000,  // 5%
         0)
    };

    FeeBreakdown { operator_fee, oracle_fee, referrer_fee, total_fee }
}

6.3 Rent-First Economics

Fees are calculated on the distributable amount, not total pool:
Distributable = Game Account Lamports - Rent Reserve

Net Pool = Distributable - Total Fees
This ensures the game account remains rent-exempt throughout its lifecycle.

7. Payout Mechanics

7.1 Winner Payout Formula

payout = floor(position.amount ร— net_pool / winning_pool_total)
Net Pool: 94 SOL
Winning Pool (Home): 50 SOL
Player Stake on Home: 10 SOL

Payout = floor(10 ร— 94 / 50) = floor(18.8) = 18 SOL
Profit = 18 - 10 = 8 SOL (80% return)

7.2 Refund Payout Formula

When winning_outcome = None (tie, cancellation, or emergency):
payout = floor(position.amount ร— net_pool / total_pool)
Fees collected (6%)
Net Pool: 94 SOL (after 6% fee)
Total Pool: 100 SOL
Player Stake: 10 SOL

Payout = floor(10 ร— 94 / 100) = 9 SOL
Loss = 1 SOL (6% fee still applies)

7.3 Remainder Handling

Due to integer division, small remainders may accumulate in the game account. These are not distributed and remain in the account after all claims.

8. Sponsored Betting

8.1 Overview

Sponsors can fund bets on behalf of players, enabling:
  • Promotional free bets
  • Onboarding campaigns
  • Treasury-funded engagement

8.2 Mechanics

8.3 Refund Routing

ScenarioRecipient
Player winsPlayer (winnings are theirs)
Player losesNo payout
Refund (normal)Sponsor (gets funds back)
Emergency refundSponsor (gets full funds back)

9. Emergency Mechanisms

9.1 Oracle Failure Protection

If the oracle fails to resolve a game, funds are not locked forever.
Permissionless Emergency Refund:
  • Available 24 hours after lock time
  • Anyone can trigger (not just oracle/authority)
  • No fees collected
  • All players receive full refund
// Available when:
current_time >= lock_timestamp + 86400  // 24 hours

9.2 Authority Override

The program authority can trigger an emergency refund at any time. Use Cases:
  • Event cancellation before lock time
  • Detected irregularities
  • Oracle key compromise
  • Any situation requiring immediate intervention
Guarantees:
  • No fees collected
  • All players receive full refund
  • Works even if game isnโ€™t locked yet

9.3 Emergency Refund Comparison

FeaturePermissionlessAuthority
Who can triggerAnyoneProgram authority only
Time requirement24h after lockAnytime
FeesNoneNone
Game stateMust be lockedAny state

10. Edge Cases

10.1 Single-Sided Pool

If all stakes are on one outcome (e.g., everyone bet Home): Problem: Winners would just get their money back (no profit) Solution: Auto-resolve to refund mode
  • winning_outcome = None
  • Everyone gets proportional refund
  • Fees still collected (normal refund)

10.2 Empty Winning Pool

If oracle selects a winner but no one bet on that outcome: Problem: Division by zero in payout calculation Solution: Auto-resolve to refund mode
  • Same as single-sided pool
  • Detected at resolution time

10.3 Zero Stakes Game

If a game is resolved with no positions: Handling:
  • Game resolves normally
  • No payouts to process
  • Rent remains in account

10.4 Dust Amounts

Very small stakes may result in 0 payout due to integer division:
Example:
Stake: 100 lamports
Net Pool: 1,000,000 lamports
Winning Pool: 500,000,000 lamports

Payout = floor(100 ร— 1,000,000 / 500,000,000) = floor(0.0002) = 0
Mitigation: UI should warn users about minimum effective stakes

11. Security Model

11.1 Trust Assumptions

EntityTrust LevelRisk
PlayersUntrustedNone - can only affect own funds
OperatorSemi-trustedCreates games, receives fees
OracleTrustedCan determine winners
AuthorityHighly trustedEmergency powers
ProgramTrustlessImmutable logic

11.2 Attack Mitigations

  • Position.claimed flag prevents re-entry
  • Checked before any transfer
  • Hardcoded oracle address
  • Cannot be changed per-game
  • Authority can override via emergency refund
  • Lock time prevents last-second stakes
  • Pool totals visible on-chain (transparent)
  • Rent reserved before fee calculation
  • Account cannot go below rent-exempt minimum
  • All arithmetic uses checked operations
  • Errors returned on overflow

11.3 PDA Security

Game PDAs:
  • Seeds include game_id (unique per game)
  • Cannot be forged without program signature
Position PDAs:
  • Seeds include player pubkey
  • One position per player per game enforced
  • Cannot claim another playerโ€™s position

12. Technical Specifications

12.1 Program Constants

// Fee basis points (1 bp = 0.01%)
pub const OPERATOR_BPS_WITH_REFERRER: u64 = 400;   // 4%
pub const OPERATOR_BPS_NO_REFERRER: u64 = 500;     // 5%
pub const REFERRER_BPS: u64 = 100;                 // 1%
pub const ORACLE_BPS: u64 = 100;                   // 1%
pub const TOTAL_FEE_BPS: u64 = 600;                // 6%
pub const BPS_DENOMINATOR: u64 = 10_000;

// Time constants
pub const MIN_LOCK_LEAD_TIME: i64 = 120;           // 2 minutes
pub const EMERGENCY_REFUND_DELAY: i64 = 86_400;    // 24 hours

// Limits
pub const MAX_EVENT_ID_LEN: usize = 50;
pub const NUM_OUTCOMES: usize = 3;

12.2 Hardcoded Addresses

// Operator wallet (receives 4-5% fee)
pub const OPERATOR_WALLET: &str = "BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib";

// Oracle wallet (resolves games, receives 1% fee)
pub const ORACLE_WALLET: &str = "FWUJCthDfPcgmTvdQWM5uofxxiYjqJFMMwiLYvS7LBFa";

12.3 Account Sizes

AccountSizeRent (approx)
GameV2~250 bytes0.002 SOL
Position~140 bytes0.001 SOL

12.4 Program ID

85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1

Appendix A: Example Game Flow

Scenario: NBA Game - Lakers vs Celtics

1

Game Creation

Game ID: 12345
Event: "NBA_LAL_BOS_20240315"
Lock Time: March 15, 2024 7:00 PM EST
2

Betting Period

Alice stakes 1 SOL on Home (Lakers)
Bob stakes 2 SOL on Away (Celtics)
Charlie stakes 0.5 SOL on Home (Lakers)
Diana stakes 1.5 SOL on Draw

Pool Totals:
  Home: 1.5 SOL
  Away: 2 SOL
  Draw: 1.5 SOL
  Total: 5 SOL
3

Lock

Game locks at 7:00 PM. No more bets accepted.
4

Resolution

Lakers win! Oracle calls resolve with outcome = Home
Fee Calculation:
  Distributable: 5 SOL - rent = ~4.998 SOL
  Total Fee (6%): 0.2999 SOL
  - Operator: 0.2499 SOL (5%, no referrer)
  - Oracle: 0.05 SOL (1%)
  Net Pool: 4.698 SOL
5

Claims

PlayerStakeOutcomePayoutProfit
Alice1 SOLHome โœ“3.132 SOL+2.132 SOL (+213%)
Charlie0.5 SOLHome โœ“1.566 SOL+1.066 SOL (+213%)
Bob2 SOLAway โœ—0-2 SOL
Diana1.5 SOLDraw โœ—0-1.5 SOL

Appendix B: Comparison with V1

FeatureV1V2
Payout ModelFixed buy-in, equal splitPool-based, proportional
Stake AmountFixed per gameAny positive amount
Outcomes2 (Home/Away)3 (Home/Away/Draw)
Player StorageVector in game accountIndividual Position PDAs
ScalabilityLimited by vector sizeUnlimited
Fee StructureConfigurable per gameHardcoded constants
Emergency Refund7 days24 hours
Authority OverrideLimitedFull anytime refund

Appendix C: Glossary

TermDefinition
Pari-MutuelBetting system where all bets are pooled and payouts calculated proportionally
PDAProgram Derived Address - deterministic account address from seeds
LamportsSmallest unit of SOL (1 SOL = 1,000,000,000 lamports)
Net PoolTotal pool minus fees, available for winner payouts
DistributablePool amount minus rent reserve
Lock TimeTimestamp after which no more bets accepted
OracleTrusted party that reports event outcomes
SponsorThird party that funds bets on behalf of players
ReferrerParty that receives commission for bringing players

Document Version: 1.0 Last Updated: January 2025 Program Version: V2