{"id":46226785,"url":"https://github.com/hallelx2/phantomzero-vrf","last_synced_at":"2026-03-03T16:46:53.394Z","repository":{"id":334560103,"uuid":"1139388688","full_name":"hallelx2/phantomzero-vrf","owner":"hallelx2","description":null,"archived":false,"fork":false,"pushed_at":"2026-02-20T09:27:03.000Z","size":936,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-20T12:56:34.403Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hallelx2.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-21T22:32:35.000Z","updated_at":"2026-02-20T09:27:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hallelx2/phantomzero-vrf","commit_stats":null,"previous_names":["hallelx2/phantomzero-vrf"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hallelx2/phantomzero-vrf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Fphantomzero-vrf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Fphantomzero-vrf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Fphantomzero-vrf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Fphantomzero-vrf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hallelx2","download_url":"https://codeload.github.com/hallelx2/phantomzero-vrf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Fphantomzero-vrf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30052133,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T15:26:47.567Z","status":"ssl_error","status_checked_at":"2026-03-03T15:26:17.132Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-03T16:46:52.778Z","updated_at":"2026-03-03T16:46:53.365Z","avatar_url":"https://github.com/hallelx2.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# phantomzero-vrf\n\ni need you start a solana smart contract program in this repo in smat-contract/ folder,  iit will be a sportsbook betting prediction contract but powered by randomness, i have a solidity sample here -  // SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\nimport \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport \"./interfaces/IGameEngine.sol\";\nimport \"./interfaces/ILiquidityPoolV2.sol\";\n\n/**\n * @title BettingPoolV2.1 - Unified LP Pool Model\n * @notice Pool-based betting system where ALL risk flows through LP pool\n * @dev NEW ARCHITECTURE:\n *      - Protocol earns 5% fee on all bets\n *      - LP pool covers ALL payouts (base + parlay bonuses)\n *      - LP pool funds round seeding (3k per round)\n *      - Reduced parlay bonuses (1.25x max) for LP safety\n *      - AMM-style LP shares (deposit/withdraw anytime)\n *\n * KEY FEATURES:\n * - ✅ Unified LP pool (no separate protocol reserve)\n * - ✅ Direct deduction model (losses immediately reduce LP value)\n * - ✅ 5% protocol fee on every bet\n * - ✅ Reduced parlay multipliers (1.0x - 1.25x)\n * - ✅ Max bet, max payout, and per-round caps\n */\ncontract BettingPoolV2_1 is Ownable, ReentrancyGuard {\n    using SafeERC20 for IERC20;\n\n    // ============ State Variables ============\n\n    IERC20 public immutable leagueToken;\n    IGameEngine public immutable gameEngine;\n    ILiquidityPoolV2 public immutable liquidityPoolV2;\n\n    address public immutable protocolTreasury;\n    address public rewardsDistributor;\n\n    // NEW: Protocol fee model (5% on all bets)\n    uint256 public constant PROTOCOL_FEE = 500; // 5% fee in basis points\n    uint256 public constant WINNER_SHARE = 2500; // 25% distributed to winners (REDUCED for LP safety)\n    uint256 public constant SEASON_POOL_SHARE = 200; // 2% for season rewards\n\n    // Multibet stake bonus rates (basis points) - added to pool\n    uint256 public constant BONUS_2_MATCH = 500;   // 5%\n    uint256 public constant BONUS_3_MATCH = 1000;  // 10%\n    uint256 public constant BONUS_4_PLUS = 2000;   // 20%\n\n    // Parlay payout multipliers (1e18 scale) - UPDATED to match-based linear scaling\n    // REDUCED Linear progression: 1.05x (2 matches) to 1.25x (10 matches) for LP safety\n    // Reduces LP risk by ~50% while keeping parlays attractive\n    uint256 public constant PARLAY_MULTIPLIER_1_MATCH = 1e18;      // 1.0x (no bonus)\n    uint256 public constant PARLAY_MULTIPLIER_2_MATCHES = 105e16;  // 1.05x (was 1.15x)\n    uint256 public constant PARLAY_MULTIPLIER_3_MATCHES = 11e17;   // 1.10x (was 1.194x)\n    uint256 public constant PARLAY_MULTIPLIER_4_MATCHES = 113e16;  // 1.13x (was 1.238x)\n    uint256 public constant PARLAY_MULTIPLIER_5_MATCHES = 116e16;  // 1.16x (was 1.281x)\n    uint256 public constant PARLAY_MULTIPLIER_6_MATCHES = 119e16;  // 1.19x (was 1.325x)\n    uint256 public constant PARLAY_MULTIPLIER_7_MATCHES = 121e16;  // 1.21x (was 1.369x)\n    uint256 public constant PARLAY_MULTIPLIER_8_MATCHES = 123e16;  // 1.23x (was 1.413x)\n    uint256 public constant PARLAY_MULTIPLIER_9_MATCHES = 124e16;  // 1.24x (was 1.456x)\n    uint256 public constant PARLAY_MULTIPLIER_10_MATCHES = 125e16; // 1.25x (was 1.5x)\n\n    // Protocol seeding per match (INCREASED for tighter odds control)\n    // Strategy: Large seed amounts provide natural depth, reducing need for virtual liquidity\n    uint256 public constant SEED_HOME_POOL = 1200 ether;   // Favorite\n    uint256 public constant SEED_AWAY_POOL = 800 ether;    // Underdog\n    uint256 public constant SEED_DRAW_POOL = 1000 ether;   // Middle\n    uint256 public constant SEED_PER_MATCH = 3000 ether;   // Total per match (10x increase)\n    uint256 public constant SEED_PER_ROUND = SEED_PER_MATCH * 10; // 30,000 LEAGUE per round\n\n    // Virtual liquidity for odds dampening (DISABLED for maximum variance)\n    // Multiplier = 1 means NO virtual liquidity, raw seeding determines odds\n    // Creates true 1.2-1.8x odds range based on allocation\n    uint256 public constant VIRTUAL_LIQUIDITY_MULTIPLIER = 12000000;\n\n    // Liquidity-aware parlay parameters (NEW per Logic2.md)\n    uint256 public constant MIN_IMBALANCE_FOR_FULL_BONUS = 4000; // 40% in basis points\n    uint256 public constant MIN_PARLAY_MULTIPLIER = 11e17; // 1.1x minimum\n\n    // Count-based parlay tiers (PRIMARY FOMO mechanism) - NEW!\n    uint256 public constant COUNT_TIER_1 = 10;   // First 10 parlays\n    uint256 public constant COUNT_TIER_2 = 20;   // Parlays 11-20\n    uint256 public constant COUNT_TIER_3 = 30;   // Parlays 21-30\n    uint256 public constant COUNT_TIER_4 = 40;   // Parlays 31-40\n    // Tier 5: 41+ parlays\n\n    // Count-based multipliers (decreasing with each tier)\n    uint256 public constant COUNT_MULT_TIER_1 = 25e17;  // 2.5x (first 10)\n    uint256 public constant COUNT_MULT_TIER_2 = 22e17;  // 2.2x (next 10)\n    uint256 public constant COUNT_MULT_TIER_3 = 19e17;  // 1.9x (next 10)\n    uint256 public constant COUNT_MULT_TIER_4 = 16e17;  // 1.6x (next 10)\n    uint256 public constant COUNT_MULT_TIER_5 = 13e17;  // 1.3x (41+)\n\n    // Reserve-based multiplier decay (SECONDARY safety valve) - NEW!\n    uint256 public constant RESERVE_TIER_1 = 100000 ether;  // 0-100k locked reserve\n    uint256 public constant RESERVE_TIER_2 = 250000 ether;  // 100k-250k locked reserve\n    uint256 public constant RESERVE_TIER_3 = 500000 ether;  // 250k-500k locked reserve\n    // Tier 4: 500k+ locked reserve\n\n    // Multiplier decay per tier (applied as percentage of base multiplier)\n    uint256 public constant TIER_1_DECAY = 10000; // 100% (no decay)\n    uint256 public constant TIER_2_DECAY = 8800;  // 88% (12% decay)\n    uint256 public constant TIER_3_DECAY = 7600;  // 76% (24% decay)\n    uint256 public constant TIER_4_DECAY = 6400;  // 64% (36% decay)\n\n    // ============ RISK MANAGEMENT CAPS (CRITICAL) ============\n    // These caps protect protocol reserve from catastrophic depletion\n    uint256 public constant MAX_BET_AMOUNT = 10000 ether; // 10,000 LEAGUE max bet\n    uint256 public constant MAX_PAYOUT_PER_BET = 100000 ether; // 100,000 LEAGUE max payout\n    uint256 public constant MAX_ROUND_PAYOUTS = 500000 ether; // 500,000 LEAGUE per round\n\n    // REMOVED: protocolReserve (now handled by LP pool)\n    // REMOVED: lockedParlayReserve (LP pool covers all payouts)\n    uint256 public seasonRewardPool;\n    uint256 public nextBetId;\n\n    // ============ Structs ============\n\n    struct MatchPool {\n        uint256 homeWinPool;    // Total LEAGUE bet on HOME_WIN (outcome 1)\n        uint256 awayWinPool;    // Total LEAGUE bet on AWAY_WIN (outcome 2)\n        uint256 drawPool;       // Total LEAGUE bet on DRAW (outcome 3)\n        uint256 totalPool;      // Sum of all three pools\n    }\n\n    // NEW: Locked odds at betting close\n    struct LockedOdds {\n        uint256 homeOdds;       // e.g., 1.5e18 = 1.5x odds for Home win\n        uint256 awayOdds;       // e.g., 1.7e18 = 1.7x odds for Away win\n        uint256 drawOdds;       // e.g., 1.8e18 = 1.8x odds for Draw\n        bool locked;            // Have odds been locked for this match?\n    }\n\n    struct RoundAccounting {\n        // Match-level pools (10 matches per round)\n        mapping(uint256 =\u003e MatchPool) matchPools;\n\n        // Locked odds per match (NEW!)\n        mapping(uint256 =\u003e LockedOdds) lockedMatchOdds;\n\n        // Round totals\n        uint256 totalBetVolume;         // Total LEAGUE bet in this round\n        uint256 totalWinningPool;       // Sum of all winning outcome pools (after settlement)\n        uint256 totalLosingPool;        // Sum of all losing outcome pools\n        uint256 totalReservedForWinners; // Total owed to winners (calculated from pools)\n        uint256 totalClaimed;            // Total LEAGUE claimed so far\n        uint256 totalPaidOut;            // Total LEAGUE paid out (including parlay bonuses) - NEW\n\n        // Revenue distribution\n        uint256 protocolFeeCollected;    // Protocol fee collected (5% of bets) - NEW\n        uint256 protocolRevenueShare;   // Protocol's share of net revenue\n        uint256 lpRevenueShare;          // LP's share of net revenue\n        uint256 seasonRevenueShare;      // Season pool share\n        bool revenueDistributed;         // Has revenue been distributed?\n\n        // Seeding tracking - NEW!\n        uint256 protocolSeedAmount;      // Total LEAGUE seeded by protocol\n        bool seeded;                     // Has round been seeded?\n\n        // LP borrowing for odds-weighted allocation - NEW!\n        uint256 lpBorrowedForBets;       // Total borrowed from LP to balance pools\n        uint256 totalUserDeposits;       // Actual user deposits (for season pool calculation)\n\n        // Parlay count tracking (for count-based tiers) - NEW!\n        uint256 parlayCount;             // Number of parlays placed this round\n\n        // Timestamps\n        uint256 roundStartTime;\n        uint256 roundEndTime;\n        bool settled;\n    }\n\n    struct Prediction {\n        uint256 matchIndex;         // 0-9\n        uint8 predictedOutcome;     // 1=HOME_WIN, 2=AWAY_WIN, 3=DRAW\n        uint256 amountInPool;       // How much LEAGUE was added to this pool\n    }\n\n    struct Bet {\n        address bettor;\n        uint256 roundId;\n        uint256 amount;             // User's total bet amount (SHOWN IN FRONTEND)\n        uint256 amountAfterFee;     // Amount after 5% protocol fee\n        uint256 allocatedAmount;    // Total allocated to pools (includes LP borrowed)\n        uint256 lpBorrowedAmount;   // Amount borrowed from LP for this bet\n        uint256 bonus;              // Protocol stake bonus added to pools\n        uint256 lockedMultiplier;   // Parlay multiplier locked at bet placement (CRITICAL FIX)\n        Prediction[] predictions;   // Match predictions\n        bool settled;               // Has round been settled?\n        bool claimed;               // Has user claimed winnings?\n    }\n\n    // ============ Mappings ============\n\n    mapping(uint256 =\u003e RoundAccounting) public roundAccounting;\n    mapping(uint256 =\u003e Bet) public bets;\n    mapping(address =\u003e uint256[]) public userBets;\n    mapping(uint256 =\u003e uint256) public betParlayReserve;  // NEW: betId =\u003e reserved parlay bonus\n\n    // ============ Events ============\n\n    event BetPlaced(\n        uint256 indexed betId,\n        address indexed bettor,\n        uint256 indexed roundId,\n        uint256 amount,\n        uint256 bonus,\n        uint256 parlayMultiplier,  // NEW\n        uint256[] matchIndices,\n        uint8[] outcomes\n    );\n\n    event RoundSeeded(\n        uint256 indexed roundId,\n        uint256 totalSeedAmount\n    );\n\n    event OddsLocked(\n        uint256 indexed roundId,\n        uint256 timestamp\n    );\n\n    event RoundSettled(\n        uint256 indexed roundId,\n        uint256 totalWinningPool,\n        uint256 totalLosingPool,\n        uint256 totalReserved\n    );\n\n    event WinningsClaimed(\n        uint256 indexed betId,\n        address indexed bettor,\n        uint256 basePayout,\n        uint256 parlayMultiplier,\n        uint256 finalPayout\n    );\n\n    event BetLost(uint256 indexed betId, address indexed bettor);\n\n    event ParlayBonusReleased(\n        uint256 indexed betId,\n        uint256 reservedAmount,\n        uint256 actualBonus\n    );\n\n    event RoundRevenueFinalized(\n        uint256 indexed roundId,\n        uint256 netRevenue,\n        uint256 toProtocol,\n        uint256 toLP,\n        uint256 toSeason,\n        uint256 seedRecovered\n    );\n\n    event ProtocolReserveFunded(address indexed funder, uint256 amount);\n\n    // ============ Constructor ============\n\n    constructor(\n        address _leagueToken,\n        address _gameEngine,\n        address _liquidityPool,\n        address _protocolTreasury,\n        address _rewardsDistributor,\n        address _initialOwner\n    ) Ownable(_initialOwner) {\n        require(_leagueToken != address(0), \"Invalid token\");\n        require(_gameEngine != address(0), \"Invalid game engine\");\n        require(_liquidityPool != address(0), \"Invalid liquidity pool\");\n\n        leagueToken = IERC20(_leagueToken);\n        gameEngine = IGameEngine(_gameEngine);\n        liquidityPoolV2 = ILiquidityPoolV2(_liquidityPool);\n        protocolTreasury = _protocolTreasury;\n        rewardsDistributor = _rewardsDistributor;\n    }\n\n    // ============ Seeding Functions ============\n\n    /**\n     * @notice Calculate differentiated seed amounts for a match (HYBRID MODEL)\n     * @dev Round 1-3: Pseudo-random based on team IDs (no stats yet)\n     *      Round 4+: Stats-based using actual team performance\n     * @param roundId The round ID\n     * @param matchIndex The match index (0-9)\n     * @return homeSeed Amount to seed home pool\n     * @return awaySeed Amount to seed away pool\n     * @return drawSeed Amount to seed draw pool\n     */\n    function _calculateMatchSeeds(uint256 roundId, uint256 matchIndex)\n        internal\n        view\n        returns (uint256 homeSeed, uint256 awaySeed, uint256 drawSeed)\n    {\n        // Get match from game engine\n        IGameEngine.Match memory matchData = gameEngine.getMatch(roundId, matchIndex);\n        uint256 homeTeamId = matchData.homeTeamId;\n        uint256 awayTeamId = matchData.awayTeamId;\n\n        // Get current season info\n        uint256 seasonId = gameEngine.getCurrentSeason();\n        IGameEngine.Season memory season = gameEngine.getSeason(seasonId);\n        uint256 seasonRound = season.currentRound;\n\n        // Use pseudo-random for first 3 rounds (no meaningful stats yet)\n        if (seasonRound \u003c= 3) {\n            return _calculatePseudoRandomSeeds(homeTeamId, awayTeamId, roundId);\n        }\n\n        // Use actual team stats from round 4 onwards\n        return _calculateStatsBasedSeeds(seasonId, homeTeamId, awayTeamId);\n    }\n\n    /**\n     * @notice Calculate seeds using TIGHT BALANCED distribution for profitable LP\n     * @dev UPDATED: Keeps odds in 1.3-1.6 range for LP profitability\n     * @dev Strategy: Even distribution (30-35% per outcome) with small variance\n     */\n    function _calculatePseudoRandomSeeds(\n        uint256 homeTeamId,\n        uint256 awayTeamId,\n        uint256 roundId\n    )\n        internal\n        pure\n        returns (uint256 homeSeed, uint256 awaySeed, uint256 drawSeed)\n    {\n        // Generate deterministic pseudo-random seed\n        uint256 seed = uint256(\n            keccak256(abi.encodePacked(homeTeamId, awayTeamId, roundId))\n        );\n\n        // Extract randomness (0-99)\n        uint256 homeStrength = (seed \u003e\u003e 0) % 100;\n        uint256 awayStrength = (seed \u003e\u003e 8) % 100;\n        uint256 drawFactor = (seed \u003e\u003e 16) % 100;\n\n        uint256 totalSeed = SEED_PER_MATCH; // 3000 LEAGUE\n\n        // WIDE VARIANCE SEEDING FOR 1.2x - 1.8x ODDS RANGE  \n        // 6 granular tiers create exciting varied odds across matches\n\n        uint256 diff = homeStrength \u003e awayStrength\n            ? homeStrength - awayStrength\n            : awayStrength - homeStrength;\n\n        uint256 favoriteAlloc;\n        uint256 underdogAlloc;\n        uint256 drawAlloc;\n\n        // More granular tiers = better odds distribution\n        if (diff \u003e 65) {\n            // HUGE FAVORITE: 50/18/32 → 1.16x / 1.94x / 1.56x (EXTREME ODDS!)\n            favoriteAlloc = 50;\n            underdogAlloc = 18;\n            drawAlloc = 32;\n        } else if (diff \u003e 50) {\n            // VERY STRONG: 46/23/31 → 1.21x / 1.78x / 1.61x\n            favoriteAlloc = 46;\n            underdogAlloc = 23;\n            drawAlloc = 31;\n        } else if (diff \u003e 35) {\n            // STRONG: 42/27/31 → 1.26x / 1.67x / 1.61x\n            favoriteAlloc = 42;\n            underdogAlloc = 27;\n            drawAlloc = 31;\n        } else if (diff \u003e 20) {\n            // MODERATE: 38/31/31 → 1.32x / 1.55x / 1.61x\n            favoriteAlloc = 38;\n            underdogAlloc = 31;\n            drawAlloc = 31;\n        } else if (diff \u003e 8) {\n            // SLIGHT: 36/33/31 → 1.39x / 1.48x / 1.61x\n            favoriteAlloc = 36;\n            underdogAlloc = 33;\n            drawAlloc = 31;\n        } else {\n            // BALANCED: 34/34/32 → 1.44x / 1.44x / 1.56x\n            favoriteAlloc = 34;\n            underdogAlloc = 34;\n            drawAlloc = 32;\n        }\n\n        // Allocate pools\n        if (homeStrength \u003e awayStrength) {\n            homeSeed = (totalSeed * favoriteAlloc) / 100;\n            awaySeed = (totalSeed * underdogAlloc) / 100;\n        } else {\n            homeSeed = (totalSeed * underdogAlloc) / 100;\n            awaySeed = (totalSeed * favoriteAlloc) / 100;\n        }\n        drawSeed = (totalSeed * drawAlloc) / 100;\n\n        // Draw-heavy matchups (20% of matches get boosted draws)\n        if (drawFactor \u003e 80) {\n            uint256 drawBoost = (totalSeed * 16) / 100; // Boost draw by 16%\n            drawSeed += drawBoost;\n            homeSeed -= drawBoost / 2;\n            awaySeed -= drawBoost / 2;\n        }\n\n        return (homeSeed, awaySeed, drawSeed);\n    }\n\n    /**\n     * @notice Calculate seeds using TIGHT stats-based distribution (mid-late season)\n     * @dev UPDATED: Uses team stats but keeps odds in 1.3-1.6 range for LP profitability\n     */\n    function _calculateStatsBasedSeeds(\n        uint256 seasonId,\n        uint256 homeTeamId,\n        uint256 awayTeamId\n    )\n        internal\n        view\n        returns (uint256 homeSeed, uint256 awaySeed, uint256 drawSeed)\n    {\n        // Get team stats from game engine\n        IGameEngine.Team memory homeTeam = gameEngine.getTeamStanding(seasonId, homeTeamId);\n        IGameEngine.Team memory awayTeam = gameEngine.getTeamStanding(seasonId, awayTeamId);\n\n        uint256 totalSeed = SEED_PER_MATCH; // 300 LEAGUE\n\n        // Calculate adjusted points (home advantage: +10%)\n        uint256 adjustedHomePoints = (homeTeam.points * 110) / 100;\n        uint256 totalPoints = adjustedHomePoints + awayTeam.points;\n\n        if (totalPoints == 0) {\n            // Fallback: use balanced seeding\n            return (100 ether, 100 ether, 100 ether); // All 1.5x odds\n        }\n\n        // WIDE VARIANCE ALLOCATION (same as pseudo-random)\n        uint256 homeTotalGames = homeTeam.wins + homeTeam.draws + homeTeam.losses;\n        uint256 awayTotalGames = awayTeam.wins + awayTeam.draws + awayTeam.losses;\n\n        if (homeTotalGames \u003e 0 \u0026\u0026 awayTotalGames \u003e 0) {\n            // Point difference percentage\n            uint256 pointDiff = adjustedHomePoints \u003e awayTeam.points\n                ? ((adjustedHomePoints - awayTeam.points) * 100) / totalPoints\n                : ((awayTeam.points - adjustedHomePoints) * 100) / totalPoints;\n\n            if (pointDiff \u003e 30) {\n                // STRONG FAVORITE: 45/25/30 split\n                if (adjustedHomePoints \u003e awayTeam.points) {\n                    homeSeed = (totalSeed * 45) / 100;\n                    awaySeed = (totalSeed * 25) / 100;\n                    drawSeed = (totalSeed * 30) / 100;\n                } else {\n                    homeSeed = (totalSeed * 25) / 100;\n                    awaySeed = (totalSeed * 45) / 100;\n                    drawSeed = (totalSeed * 30) / 100;\n                }\n            } else if (pointDiff \u003e 15) {\n                // MODERATE FAVORITE: 40/30/30 split\n                if (adjustedHomePoints \u003e awayTeam.points) {\n                    homeSeed = (totalSeed * 40) / 100;\n                    awaySeed = (totalSeed * 30) / 100;\n                    drawSeed = (totalSeed * 30) / 100;\n                } else {\n                    homeSeed = (totalSeed * 30) / 100;\n                    awaySeed = (totalSeed * 40) / 100;\n                    drawSeed = (totalSeed * 30) / 100;\n                }\n            } else {\n                // BALANCED: 35/35/30 split\n                homeSeed = (totalSeed * 35) / 100;\n                awaySeed = (totalSeed * 35) / 100;\n                drawSeed = (totalSeed * 30) / 100;\n            }\n        } else {\n            // Fallback to balanced\n            homeSeed = (totalSeed * 35) / 100;\n            awaySeed = (totalSeed * 35) / 100;\n            drawSeed = (totalSeed * 30) / 100;\n        }\n\n        return (homeSeed, awaySeed, drawSeed);\n    }\n\n    /**\n     * @notice Seed match pools at round start with DYNAMIC differentiated odds\n     * @dev Uses hybrid model: pseudo-random for rounds 1-3, stats-based for rounds 4+\n     * @param roundId The round to seed\n     */\n    function seedRoundPools(uint256 roundId) external onlyOwner {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        require(!accounting.seeded, \"Round already seeded\");\n        require(!accounting.settled, \"Round already settled\");\n\n        uint256 totalSeedAmount = 0;\n\n        // Seed each match with DIFFERENTIATED amounts based on team matchup\n        for (uint256 matchIndex = 0; matchIndex \u003c 10; matchIndex++) {\n            (uint256 homeSeed, uint256 awaySeed, uint256 drawSeed) = _calculateMatchSeeds(roundId, matchIndex);\n\n            MatchPool storage pool = accounting.matchPools[matchIndex];\n            pool.homeWinPool = homeSeed;\n            pool.awayWinPool = awaySeed;\n            pool.drawPool = drawSeed;\n            pool.totalPool = homeSeed + awaySeed + drawSeed;\n\n            totalSeedAmount += pool.totalPool;\n            accounting.totalBetVolume += pool.totalPool;\n        }\n\n        // Request seeding from LP pool\n        bool success = liquidityPoolV2.fundSeeding(roundId, totalSeedAmount);\n        require(success, \"LP pool cannot fund seeding - insufficient liquidity\");\n\n        accounting.protocolSeedAmount = totalSeedAmount;\n        accounting.seeded = true;\n\n        // IMMEDIATELY LOCK ODDS after seeding (everyone gets same fixed odds)\n        _lockRoundOddsFromSeeds(roundId, accounting);\n\n        emit RoundSeeded(roundId, totalSeedAmount);\n    }\n\n    /**\n     * @notice Lock odds based on seed ratios (called automatically after seeding)\n     * @dev CRITICAL: Odds are locked at seeding time and NEVER change\n     * @dev Everyone gets paid at these fixed odds, making accounting exact\n     * @param roundId The round to lock odds for\n     * @param accounting Storage reference to round accounting\n     */\n    function _lockRoundOddsFromSeeds(uint256 roundId, RoundAccounting storage accounting) internal {\n        // Lock odds for all 10 matches based on INITIAL SEED ratios\n        for (uint256 i = 0; i \u003c 10; i++) {\n            MatchPool storage pool = accounting.matchPools[i];\n            LockedOdds storage odds = accounting.lockedMatchOdds[i];\n\n            uint256 totalPool = pool.totalPool;\n            require(totalPool \u003e 0, \"Pool not initialized\");\n\n            // FIXED ODDS FORMULA - Compressed to 1.2x - 1.8x range\n            // Formula: odds = 1.0 + (totalPool / outcomePool - 1.0) × compressionFactor\n            //\n            // Allocation examples:\n            // 50% (huge favorite): totalPool/outcomePool = 2.0 → compressed to ~1.25x\n            // 34% (balanced): totalPool/outcomePool = 2.94 → compressed to ~1.50x\n            // 18% (huge underdog): totalPool/outcomePool = 5.56 → compressed to ~1.75x\n            //\n            // Compression factor chosen to map raw 2-5.5 range to target 1.2-1.8 range\n\n            // Raw parimutuel odds (no virtual liquidity)\n            uint256 rawHomeOdds = (totalPool * 1e18) / pool.homeWinPool;\n            uint256 rawAwayOdds = (totalPool * 1e18) / pool.awayWinPool;\n            uint256 rawDrawOdds = (totalPool * 1e18) / pool.drawPool;\n\n            // Compress to target range: 1.2x - 1.8x\n            // Formula: compressed = 1.0 + (raw - 2.0) × 0.17\n            // This maps: 2.0x→1.2x, 3.0x→1.37x, 4.0x→1.54x, 5.5x→1.8x\n            odds.homeOdds = _compressOdds(rawHomeOdds);\n            odds.awayOdds = _compressOdds(rawAwayOdds);\n            odds.drawOdds = _compressOdds(rawDrawOdds);\n            odds.locked = true;\n        }\n\n        emit OddsLocked(roundId, block.timestamp);\n    }\n\n    /**\n     * @notice Compress raw parimutuel odds to target 1.3-1.7x range\n     * @param rawOdds Raw odds from pool ratios (e.g., 3.0e18)\n     * @return Compressed odds in 1.3-1.7x range\n     */\n    function _compressOdds(uint256 rawOdds) internal pure returns (uint256) {\n        // Target range: 1.3x - 1.7x (safe profitable range)\n        // Raw range: ~1.8x - 5.5x (from our 6-tier allocation system)\n\n        // Minimum odds: 1.3x (even huge favorites must pay something)\n        if (rawOdds \u003c 18e17) { // Less than 1.8x raw\n            return 13e17; // 1.3x min\n        }\n\n        // Maximum odds: 1.7x (cap huge underdogs)\n        if (rawOdds \u003e 55e17) { // More than 5.5x raw\n            return 17e17; // 1.7x max\n        }\n\n        // Linear compression formula:\n        // compressed = minOdds + (raw - minRaw) × (maxOdds - minOdds) / (maxRaw - minRaw)\n        // compressed = 1.3 + (raw - 1.8) × (1.7 - 1.3) / (5.5 - 1.8)\n        // compressed = 1.3 + (raw - 1.8) × 0.4 / 3.7\n        // compressed = 1.3 + (raw - 1.8) × 0.108\n\n        uint256 excess = rawOdds - 18e17; // Amount above 1.8x\n        uint256 scaledExcess = (excess * 108) / 1000; // 0.108 factor\n        uint256 compressed = 13e17 + scaledExcess; // Add to min 1.3x\n\n        return compressed;\n    }\n\n    /**\n     * @notice Calculate odds-weighted allocations for parlay bets\n     * @dev Allocates tokens such that each match contributes equally to target payout\n     * @param roundId Current round ID\n     * @param matchIndices Array of match indices\n     * @param outcomes Array of predicted outcomes\n     * @param amountAfterFee User's bet amount after protocol fee\n     * @param parlayMultiplier Locked parlay multiplier\n     * @return allocations Array of allocations per match\n     * @return totalAllocated Total tokens allocated to pools\n     * @return lpBorrowed Amount borrowed from LP (totalAllocated - amountAfterFee)\n     */\n    function _calculateOddsWeightedAllocations(\n        uint256 roundId,\n        uint256[] calldata matchIndices,\n        uint8[] calldata outcomes,\n        uint256 amountAfterFee,\n        uint256 parlayMultiplier\n    ) internal view returns (uint256[] memory allocations, uint256 totalAllocated, uint256 lpBorrowed) {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        allocations = new uint256[](matchIndices.length);\n\n        // Step 1: Calculate target final payout\n        // Base payout = product of all odds\n        uint256 basePayout = amountAfterFee;\n        for (uint256 i = 0; i \u003c matchIndices.length; i++) {\n            LockedOdds storage odds = accounting.lockedMatchOdds[matchIndices[i]];\n            require(odds.locked, \"Odds not locked - seed round first\");\n\n            // Get odds for predicted outcome\n            uint256 matchOdds;\n            if (outcomes[i] == 1) {\n                matchOdds = odds.homeOdds;\n            } else if (outcomes[i] == 2) {\n                matchOdds = odds.awayOdds;\n            } else {\n                matchOdds = odds.drawOdds;\n            }\n\n            // Multiply: basePayout = basePayout × matchOdds / 1e18\n            // Check for overflow before multiplication\n            require(basePayout \u003c= type(uint256).max / matchOdds, \"Parlay calculation overflow\");\n            basePayout = (basePayout * matchOdds) / 1e18;\n        }\n\n        // Apply parlay multiplier\n        // Check for overflow before multiplication\n        require(basePayout \u003c= type(uint256).max / parlayMultiplier, \"Parlay multiplier overflow\");\n        uint256 targetPayout = (basePayout * parlayMultiplier) / 1e18;\n\n        // Step 2: Calculate per-match contribution (equal contribution)\n        uint256 perMatchContribution = targetPayout / matchIndices.length;\n\n        // Step 3: Calculate required allocation for each match (working backwards)\n        totalAllocated = 0;\n        for (uint256 i = 0; i \u003c matchIndices.length; i++) {\n            LockedOdds storage odds = accounting.lockedMatchOdds[matchIndices[i]];\n\n            // Get odds for predicted outcome\n            uint256 matchOdds;\n            if (outcomes[i] == 1) {\n                matchOdds = odds.homeOdds;\n            } else if (outcomes[i] == 2) {\n                matchOdds = odds.awayOdds;\n            } else {\n                matchOdds = odds.drawOdds;\n            }\n\n            // Calculate: allocation = perMatchContribution / matchOdds\n            // allocation × matchOdds = perMatchContribution\n            allocations[i] = (perMatchContribution * 1e18) / matchOdds;\n            totalAllocated += allocations[i];\n        }\n\n        // Step 4: Calculate LP borrowing needed\n        if (totalAllocated \u003e amountAfterFee) {\n            lpBorrowed = totalAllocated - amountAfterFee;\n        } else {\n            lpBorrowed = 0;\n        }\n\n        return (allocations, totalAllocated, lpBorrowed);\n    }\n\n    // ============ Betting Functions ============\n\n    /**\n     * @notice Place a bet on multiple match outcomes\n     * @param matchIndices Array of match indices (0-9)\n     * @param outcomes Array of predicted outcomes (1=HOME, 2=AWAY, 3=DRAW)\n     * @param amount Total LEAGUE to bet (protocol bonus added on top)\n     */\n    function placeBet(\n        uint256[] calldata matchIndices,\n        uint8[] calldata outcomes,\n        uint256 amount\n    ) external nonReentrant returns (uint256 betId) {\n        require(amount \u003e 0, \"Amount must be \u003e 0\");\n        require(amount \u003c= MAX_BET_AMOUNT, \"Bet exceeds maximum\"); // CRITICAL: Cap max bet\n        require(matchIndices.length == outcomes.length, \"Array length mismatch\");\n        require(matchIndices.length \u003e 0 \u0026\u0026 matchIndices.length \u003c= 10, \"Invalid bet count\");\n\n        uint256 currentRoundId = gameEngine.getCurrentRound();\n        require(currentRoundId \u003e 0, \"No active round\");\n        require(!gameEngine.isRoundSettled(currentRoundId), \"Round already settled\");\n\n        // Transfer user's stake using SafeERC20\n        leagueToken.safeTransferFrom(msg.sender, address(this), amount);\n\n        RoundAccounting storage accounting = roundAccounting[currentRoundId];\n\n        // Deduct 5% protocol fee\n        uint256 protocolFee = (amount * PROTOCOL_FEE) / 10000;\n        uint256 amountAfterFee = amount - protocolFee;\n\n        // Transfer fee to treasury using SafeERC20\n        leagueToken.safeTransfer(protocolTreasury, protocolFee);\n\n        accounting.protocolFeeCollected += protocolFee;\n        accounting.totalBetVolume += amountAfterFee;\n        accounting.totalUserDeposits += amountAfterFee; // Track actual user deposits\n\n        // Assign betId FIRST (BUG #1 fix)\n        betId = nextBetId++;\n\n        // Determine if this is a parlay (multi-leg bet)\n        bool isParlay = matchIndices.length \u003e 1;\n\n        // Calculate DYNAMIC parlay multiplier for accurate reservation (BUG #2 fix)\n        // This uses the CURRENT parlay count for tier calculation\n        uint256 parlayMultiplier = _getParlayMultiplierDynamicPreview(\n            matchIndices,\n            currentRoundId,\n            matchIndices.length\n        );\n\n        // CRITICAL: Check LP pool can cover worst-case payout BEFORE accepting bet\n        uint256 maxPossiblePayout = _calculateMaxPayout(amountAfterFee, matchIndices.length, parlayMultiplier);\n        require(\n            liquidityPoolV2.canCoverPayout(maxPossiblePayout),\n            \"Insufficient LP liquidity for this bet\"\n        );\n\n        // INCREMENT parlay count AFTER calculating multiplier (count-based FOMO)\n        // This ensures next bettor sees the tier has moved\n        if (isParlay) {\n            accounting.parlayCount += 1;\n        }\n\n        // Calculate odds-weighted allocations\n        (uint256[] memory allocations, uint256 totalAllocated, uint256 lpBorrowed) = _calculateOddsWeightedAllocations(\n            currentRoundId,\n            matchIndices,\n            outcomes,\n            amountAfterFee,\n            parlayMultiplier\n        );\n\n        // If we need to borrow from LP, do it now\n        if (lpBorrowed \u003e 0) {\n            require(\n                liquidityPoolV2.canCoverPayout(lpBorrowed),\n                \"Insufficient LP liquidity to borrow for bet allocation\"\n            );\n\n            // CRITICAL: Update state BEFORE external call (Checks-Effects-Interactions pattern)\n            accounting.lpBorrowedForBets += lpBorrowed;\n\n            // Transfer borrowed funds from LP to BettingPool\n            liquidityPoolV2.payWinner(address(this), lpBorrowed);\n        }\n\n        // Store bet\n        Bet storage bet = bets[betId];\n        bet.bettor = msg.sender;\n        bet.roundId = currentRoundId;\n        bet.amount = amount;              // Original bet amount (shown in frontend)\n        bet.amountAfterFee = amountAfterFee;  // After 5% fee\n        bet.allocatedAmount = totalAllocated;  // Total allocated to pools\n        bet.lpBorrowedAmount = lpBorrowed;     // Borrowed from LP\n        bet.bonus = 0; // No stake bonus in unified LP model\n        bet.lockedMultiplier = parlayMultiplier;  // CRITICAL FIX: Lock multiplier at bet placement\n        bet.settled = false;\n        bet.claimed = false;\n\n        // Now add predictions and update pools with odds-weighted allocations\n        for (uint256 i = 0; i \u003c matchIndices.length; i++) {\n            uint256 matchIndex = matchIndices[i];\n            uint8 outcome = outcomes[i];\n\n            require(matchIndex \u003c 10, \"Invalid match index\");\n            require(outcome \u003e= 1 \u0026\u0026 outcome \u003c= 3, \"Invalid outcome\");\n\n            uint256 allocation = allocations[i];\n\n            // Add to appropriate match pool\n            MatchPool storage pool = accounting.matchPools[matchIndex];\n\n            if (outcome == 1) {\n                pool.homeWinPool += allocation;\n            } else if (outcome == 2) {\n                pool.awayWinPool += allocation;\n            } else {\n                pool.drawPool += allocation;\n            }\n            pool.totalPool += allocation;\n\n            // Push prediction to storage array\n            bet.predictions.push(Prediction({\n                matchIndex: matchIndex,\n                predictedOutcome: outcome,\n                amountInPool: allocation\n            }));\n        }\n\n        userBets[msg.sender].push(betId);\n\n        emit BetPlaced(\n            betId,\n            msg.sender,\n            currentRoundId,\n            amount,\n            0, // No stake bonus in unified LP model\n            parlayMultiplier,\n            matchIndices,\n            outcomes\n        );\n    }\n\n    /**\n     * @notice Claim winnings for a bet (pull pattern)\n     * @param betId The bet ID to claim\n     * @param minPayout Minimum acceptable payout (slippage protection)\n     */\n    function claimWinnings(uint256 betId, uint256 minPayout) external nonReentrant {\n        Bet storage bet = bets[betId];\n        require(bet.bettor == msg.sender, \"Not your bet\");\n        require(!bet.claimed, \"Already claimed\");\n\n        RoundAccounting storage accounting = roundAccounting[bet.roundId];\n        require(accounting.settled, \"Round not settled\");\n\n        // Calculate if bet won and payout amount (with parlay multiplier)\n        (bool won, uint256 basePayout, uint256 finalPayout) = _calculateBetPayout(betId);\n\n        // Slippage protection: ensure payout meets minimum expectation\n        require(finalPayout \u003e= minPayout, \"Payout below minimum (slippage)\");\n\n        bet.claimed = true;\n        bet.settled = true;\n\n        if (won \u0026\u0026 finalPayout \u003e 0) {\n            // CRITICAL: Check per-round payout cap to prevent excessive payouts\n            require(\n                accounting.totalPaidOut + finalPayout \u003c= MAX_ROUND_PAYOUTS,\n                \"Round payout limit reached\"\n            );\n\n            accounting.totalClaimed += finalPayout;\n            accounting.totalPaidOut += finalPayout; // Track total paid including bonuses\n\n            // Pay from BettingPool's balance first, pull from LP if insufficient\n            uint256 bettingPoolBalance = leagueToken.balanceOf(address(this));\n\n            if (bettingPoolBalance \u003e= finalPayout) {\n                // BettingPool has enough, pay directly using SafeERC20\n                leagueToken.safeTransfer(msg.sender, finalPayout);\n            } else {\n                // BettingPool insufficient, need to pull from LP\n                uint256 shortfall = finalPayout - bettingPoolBalance;\n\n                // Pay what we have from BettingPool using SafeERC20\n                if (bettingPoolBalance \u003e 0) {\n                    leagueToken.safeTransfer(msg.sender, bettingPoolBalance);\n                }\n\n                // Pull shortfall from LP and pay user\n                liquidityPoolV2.payWinner(msg.sender, shortfall);\n            }\n\n            emit WinningsClaimed(\n                betId,\n                msg.sender,\n                basePayout,\n                bet.lockedMultiplier,  // Use locked multiplier instead of recalculating\n                finalPayout\n            );\n        } else {\n            emit BetLost(betId, msg.sender);\n        }\n    }\n\n    // ============ Settlement Functions ============\n\n    /**\n     * @notice Settle round after VRF generates results\n     * @param roundId The round to settle\n     * @dev Only owner can settle to prevent frontrunning and ensure proper timing\n     */\n    function settleRound(uint256 roundId) external onlyOwner nonReentrant {\n        require(gameEngine.isRoundSettled(roundId), \"Round not settled in GameEngine\");\n\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        require(!accounting.settled, \"Already settled\");\n\n        // Calculate winning and losing pools (O(10) - constant time)\n        for (uint256 matchIndex = 0; matchIndex \u003c 10; matchIndex++) {\n            IGameEngine.Match memory matchResult = gameEngine.getMatch(roundId, matchIndex);\n            MatchPool storage pool = accounting.matchPools[matchIndex];\n\n            IGameEngine.MatchOutcome winningOutcome = matchResult.outcome;\n            uint256 winningPool;\n            uint256 losingPool;\n\n            if (winningOutcome == IGameEngine.MatchOutcome.HOME_WIN) {\n                winningPool = pool.homeWinPool;\n                losingPool = pool.awayWinPool + pool.drawPool;\n            } else if (winningOutcome == IGameEngine.MatchOutcome.AWAY_WIN) {\n                winningPool = pool.awayWinPool;\n                losingPool = pool.homeWinPool + pool.drawPool;\n            } else {\n                // DRAW\n                winningPool = pool.drawPool;\n                losingPool = pool.homeWinPool + pool.awayWinPool;\n            }\n\n            accounting.totalWinningPool += winningPool;\n            accounting.totalLosingPool += losingPool;\n        }\n\n        // Calculate total owed to winners (prevents LP exploit)\n        accounting.totalReservedForWinners = _calculateTotalWinningPayouts(roundId);\n\n        accounting.settled = true;\n        accounting.roundEndTime = block.timestamp;\n\n        emit RoundSettled(\n            roundId,\n            accounting.totalWinningPool,\n            accounting.totalLosingPool,\n            accounting.totalReservedForWinners\n        );\n    }\n\n    /**\n     * @notice Distribute net revenue after all claims (or after timeout)\n     * @param roundId The round to finalize\n     */\n    function finalizeRoundRevenue(uint256 roundId) external nonReentrant {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        require(accounting.settled, \"Round not settled\");\n        require(!accounting.revenueDistributed, \"Already distributed\");\n\n        // CORRECT ACCOUNTING:\n        // All user bets and LP seed are in BettingPool contract\n        // Winners were paid directly from LP pool (via payWinner)\n        // Now return remaining funds in BettingPool back to LP\n\n        // Check actual balance in this contract\n        uint256 remainingInContract = leagueToken.balanceOf(address(this));\n\n        uint256 profitToLP = 0;\n        uint256 lossFromLP = 0;\n        uint256 seasonShare = 0;\n\n        if (remainingInContract \u003e 0) {\n            // Season pool gets exactly 2% of ACTUAL USER DEPOSITS (not including LP borrowed)\n            uint256 totalUserBetsBeforeFee = accounting.totalUserDeposits + accounting.protocolFeeCollected;\n            seasonShare = (totalUserBetsBeforeFee * 200) / 10000; // 2%\n\n            // Cap seasonShare to what's actually available\n            if (seasonShare \u003e remainingInContract) {\n                seasonShare = remainingInContract;\n            }\n\n            // LP gets everything else\n            profitToLP = remainingInContract - seasonShare;\n\n            // Transfer LP's share back to LP pool\n            if (profitToLP \u003e 0) {\n                leagueToken.safeTransfer(address(liquidityPoolV2), profitToLP);\n                // Update LP liquidity tracking\n                liquidityPoolV2.returnSeedFunds(profitToLP);\n            }\n\n            // Allocate season pool share\n            if (seasonShare \u003e 0) {\n                seasonRewardPool += seasonShare;\n                // Funds stay in BettingPool contract for season rewards\n            }\n        }\n\n        // Track if LP took a loss (paid out more than collected)\n        uint256 totalInContract = accounting.totalBetVolume + accounting.protocolSeedAmount;\n        uint256 totalPaid = accounting.totalPaidOut;\n\n        if (totalPaid \u003e totalInContract) {\n            lossFromLP = totalPaid - totalInContract;\n        }\n\n        accounting.lpRevenueShare = profitToLP;\n        accounting.seasonRevenueShare = seasonShare;\n        accounting.revenueDistributed = true;\n\n        emit RoundRevenueFinalized(\n            roundId,\n            totalInContract,\n            totalPaid,\n            profitToLP,\n            lossFromLP,\n            seasonShare\n        );\n    }\n\n    // ============ Internal Helper Functions ============\n\n    /**\n     * @notice Calculate multibet stake bonus (added to pools upfront)\n     */\n    function _calculateMultibetBonus(uint256 amount, uint256 numMatches)\n        internal\n        pure\n        returns (uint256)\n    {\n        if (numMatches == 1) return 0;\n        if (numMatches == 2) return (amount * BONUS_2_MATCH) / 10000;\n        if (numMatches == 3) return (amount * BONUS_3_MATCH) / 10000;\n        return (amount * BONUS_4_PLUS) / 10000; // 4+ matches\n    }\n\n    /**\n     * @notice Get parlay payout multiplier based on number of legs\n     * @dev Multipliers are capped to prevent exponential growth\n     * @param numLegs Number of matches in multibet\n     * @return multiplier Multiplier in 1e18 scale (e.g., 1.5e18 = 1.5x)\n     */\n    /**\n     * @notice Get base parlay multiplier based on number of matches (UPDATED: linear 1.15x-1.5x)\n     * @dev Linear progression: 1 match = 1.0x, 2 matches = 1.15x, 10 matches = 1.5x\n     * @param numMatches Number of matches in the parlay\n     * @return multiplier Multiplier in 1e18 scale\n     */\n    function _getParlayMultiplier(uint256 numMatches)\n        internal\n        pure\n        returns (uint256 multiplier)\n    {\n        if (numMatches == 1) return PARLAY_MULTIPLIER_1_MATCH;      // 1.0x\n        if (numMatches == 2) return PARLAY_MULTIPLIER_2_MATCHES;    // 1.15x\n        if (numMatches == 3) return PARLAY_MULTIPLIER_3_MATCHES;    // 1.194x\n        if (numMatches == 4) return PARLAY_MULTIPLIER_4_MATCHES;    // 1.238x\n        if (numMatches == 5) return PARLAY_MULTIPLIER_5_MATCHES;    // 1.281x\n        if (numMatches == 6) return PARLAY_MULTIPLIER_6_MATCHES;    // 1.325x\n        if (numMatches == 7) return PARLAY_MULTIPLIER_7_MATCHES;    // 1.369x\n        if (numMatches == 8) return PARLAY_MULTIPLIER_8_MATCHES;    // 1.413x\n        if (numMatches == 9) return PARLAY_MULTIPLIER_9_MATCHES;    // 1.456x\n        return PARLAY_MULTIPLIER_10_MATCHES;                         // 1.5x (capped at 10)\n    }\n\n    /**\n     * @notice Calculate pool imbalance for a match (Logic2.md)\n     * @dev Measures dominance of largest pool\n     * @param roundId The round ID\n     * @param matchIndex The match index (0-9)\n     * @return imbalance Pool imbalance in basis points (0-10000, where 10000 = 100%)\n     */\n    function _calculatePoolImbalance(uint256 roundId, uint256 matchIndex)\n        internal\n        view\n        returns (uint256 imbalance)\n    {\n        MatchPool storage pool = roundAccounting[roundId].matchPools[matchIndex];\n\n        if (pool.totalPool == 0) return 0;\n\n        // Find max pool\n        uint256 maxPool = pool.homeWinPool;\n        if (pool.awayWinPool \u003e maxPool) maxPool = pool.awayWinPool;\n        if (pool.drawPool \u003e maxPool) maxPool = pool.drawPool;\n\n        // Return as basis points (10000 = 100%)\n        imbalance = (maxPool * 10000) / pool.totalPool;\n\n        return imbalance;\n    }\n\n    /**\n     * @notice Get count-based parlay multiplier (PRIMARY FOMO mechanism)\n     * @dev Returns multiplier based on parlay index in current round\n     * @param parlayIndex The current parlay count (0-indexed)\n     * @return multiplier Multiplier in 1e18 scale\n     */\n    function _getParlayMultiplierByCount(uint256 parlayIndex)\n        internal\n        pure\n        returns (uint256 multiplier)\n    {\n        if (parlayIndex \u003c COUNT_TIER_1) return COUNT_MULT_TIER_1;      // 2.5x (first 10)\n        if (parlayIndex \u003c COUNT_TIER_2) return COUNT_MULT_TIER_2;      // 2.2x (next 10)\n        if (parlayIndex \u003c COUNT_TIER_3) return COUNT_MULT_TIER_3;      // 1.9x (next 10)\n        if (parlayIndex \u003c COUNT_TIER_4) return COUNT_MULT_TIER_4;      // 1.6x (next 10)\n        return COUNT_MULT_TIER_5;                                        // 1.3x (41+)\n    }\n\n    /**\n     * @notice Get reserve-based decay factor (SECONDARY safety valve)\n     * @dev Higher locked reserve = lower multipliers (capital protection)\n     * @return decayFactor Percentage to apply to multiplier (10000 = 100%)\n     */\n    function _getReserveDecayFactor() internal pure returns (uint256 decayFactor) {\n        // In unified LP model, no reserve decay - LP pool manages all risk\n        return 10000; // 100% (no decay)\n    }\n\n    /**\n     * @notice Get liquidity-aware parlay multiplier (PREVIEW for reservation)\n     * @dev Combines 3 layers: count-based tiers + reserve decay + pool imbalance\n     * @dev Used BEFORE bet exists for accurate reservation\n     * @param matchIndices Array of match indices\n     * @param roundId The round ID\n     * @param numLegs Number of legs\n     * @return multiplier Multiplier in 1e18 scale\n     */\n    function _getParlayMultiplierDynamicPreview(\n        uint256[] calldata matchIndices,\n        uint256 roundId,\n        uint256 numLegs\n    )\n        internal\n        view\n        returns (uint256 multiplier)\n    {\n        // Single bets always get 1.0x\n        if (numLegs == 1) return 1e18;\n\n        RoundAccounting storage accounting = roundAccounting[roundId];\n\n        // LAYER 1: Base multiplier based on number of matches (NEW: Linear 1.15x-1.5x)\n        uint256 countBasedMult = _getParlayMultiplier(numLegs);\n\n        // LAYER 2: Pool imbalance gating (ECONOMIC PROTECTION)\n        uint256 totalImbalance = 0;\n        for (uint256 i = 0; i \u003c matchIndices.length; i++) {\n            uint256 imbalance = _calculatePoolImbalance(roundId, matchIndices[i]);\n            totalImbalance += imbalance;\n        }\n        uint256 avgImbalance = totalImbalance / matchIndices.length;\n\n        // If pools are balanced, reduce to minimum regardless of tier\n        if (avgImbalance \u003c MIN_IMBALANCE_FOR_FULL_BONUS) {\n            return MIN_PARLAY_MULTIPLIER; // 1.1x\n        }\n\n        // LAYER 3: Reserve-based decay (SECONDARY SAFETY VALVE)\n        uint256 decayFactor = _getReserveDecayFactor();\n        uint256 finalMultiplier = (countBasedMult * decayFactor) / 10000;\n\n        // Never go below minimum\n        if (finalMultiplier \u003c MIN_PARLAY_MULTIPLIER) {\n            return MIN_PARLAY_MULTIPLIER;\n        }\n\n        return finalMultiplier;\n    }\n\n    /**\n     * @notice Get liquidity-aware parlay multiplier (stored from bet placement)\n     * @dev Uses the same layered model as preview for consistency\n     * @dev This is called during payout - multiplier was locked at bet time\n     * @param betId The bet ID\n     * @return multiplier Multiplier in 1e18 scale\n     */\n    function _getParlayMultiplierDynamic(uint256 betId)\n        internal\n        view\n        returns (uint256 multiplier)\n    {\n        Bet storage bet = bets[betId];\n        uint256 numLegs = bet.predictions.length;\n\n        // Single bets always get 1.0x\n        if (numLegs == 1) return 1e18;\n\n        // NOTE: This recalculates the multiplier at payout time\n        // In production, you might want to store the locked multiplier in the Bet struct\n        // For now, we recalculate using the SAME logic as preview to ensure consistency\n\n        RoundAccounting storage accounting = roundAccounting[bet.roundId];\n\n        // We use the parlay count AT THE TIME OF BET PLACEMENT\n        // This is stored implicitly - the bet was placed when count was X\n        // For simplicity, we recalculate based on imbalance + reserve state\n\n        // Calculate average imbalance across all legs\n        uint256 totalImbalance = 0;\n        for (uint256 i = 0; i \u003c bet.predictions.length; i++) {\n            Prediction memory pred = bet.predictions[i];\n            uint256 imbalance = _calculatePoolImbalance(bet.roundId, pred.matchIndex);\n            totalImbalance += imbalance;\n        }\n        uint256 avgImbalance = totalImbalance / bet.predictions.length;\n\n        // If pools were balanced at bet time, reduce to minimum\n        if (avgImbalance \u003c MIN_IMBALANCE_FOR_FULL_BONUS) {\n            return MIN_PARLAY_MULTIPLIER; // 1.1x\n        }\n\n        // For payout, we use the reserved multiplier\n        // This should match what was calculated at bet placement\n        // NOTE: In V2.2, consider storing multiplier in Bet struct for exact consistency\n\n        // Use the base multiplier from number of legs as fallback\n        uint256 baseMultiplier = _getParlayMultiplier(numLegs);\n        return baseMultiplier \u003e MIN_PARLAY_MULTIPLIER ? baseMultiplier : MIN_PARLAY_MULTIPLIER;\n    }\n\n    /**\n     * @notice Reserve parlay bonus at bet time to prevent insolvency\n     * @dev Reserves max possible bonus (pessimistic estimate)\n     * @return maxBonus The amount reserved from protocol reserve\n     */\n    /**\n     * @notice No longer used - parlay bonuses paid directly from LP pool\n     * @dev Kept for backwards compatibility, always returns 0\n     */\n    function _reserveParlayBonus(uint256, uint256)\n        internal\n        pure\n        returns (uint256)\n    {\n        return 0; // No upfront reservation in unified LP model\n    }\n\n    /**\n     * @notice Calculate bet payout with parlay multiplier\n     * @return won Whether all predictions were correct\n     * @return basePayout Base payout from pools (without multiplier)\n     * @return finalPayout Final payout after parlay multiplier\n     */\n    function _calculateBetPayout(uint256 betId)\n        internal\n        view\n        returns (bool won, uint256 basePayout, uint256 finalPayout)\n    {\n        Bet storage bet = bets[betId];\n        RoundAccounting storage accounting = roundAccounting[bet.roundId];\n\n        bool allCorrect = true;\n        uint256 totalBasePayout = 0;\n\n        for (uint256 i = 0; i \u003c bet.predictions.length; i++) {\n            Prediction memory pred = bet.predictions[i];\n            IGameEngine.Match memory matchResult = gameEngine.getMatch(\n                bet.roundId,\n                pred.matchIndex\n            );\n\n            // Check if prediction is correct\n            IGameEngine.MatchOutcome predictedEnum;\n            if (pred.predictedOutcome == 1) predictedEnum = IGameEngine.MatchOutcome.HOME_WIN;\n            else if (pred.predictedOutcome == 2) predictedEnum = IGameEngine.MatchOutcome.AWAY_WIN;\n            else predictedEnum = IGameEngine.MatchOutcome.DRAW;\n\n            if (matchResult.outcome != predictedEnum) {\n                allCorrect = false;\n                break; // Multibet failed\n            }\n\n            // Use LOCKED ODDS for payout calculation\n            LockedOdds storage odds = accounting.lockedMatchOdds[pred.matchIndex];\n            require(odds.locked, \"Odds not locked yet\");\n\n            // Get the locked odds for the predicted outcome\n            uint256 lockedOdds;\n            if (pred.predictedOutcome == 1) {\n                lockedOdds = odds.homeOdds;\n            } else if (pred.predictedOutcome == 2) {\n                lockedOdds = odds.awayOdds;\n            } else {\n                lockedOdds = odds.drawOdds;\n            }\n\n            // Simple multiplication: amount × locked odds\n            uint256 matchPayout = (pred.amountInPool * lockedOdds) / 1e18;\n\n            totalBasePayout += matchPayout;\n        }\n\n        if (!allCorrect) {\n            return (false, 0, 0);\n        }\n\n        // Apply LOCKED parlay multiplier (CRITICAL FIX: use stored value from bet placement)\n        uint256 parlayMultiplier = bet.lockedMultiplier;\n        uint256 totalFinalPayout = (totalBasePayout * parlayMultiplier) / 1e18;\n\n        // CRITICAL: Cap maximum payout per bet to protect protocol reserve\n        if (totalFinalPayout \u003e MAX_PAYOUT_PER_BET) {\n            totalFinalPayout = MAX_PAYOUT_PER_BET;\n        }\n\n        return (true, totalBasePayout, totalFinalPayout);\n    }\n\n    /**\n     * @notice Calculate total payouts owed to ALL winners using LOCKED ODDS\n     * @dev Loops through 10 matches (O(10) constant time)\n     * @dev Uses locked odds × winning pool for exact payout calculation\n     * @dev NOTE: This calculates BASE payouts only (without parlay multipliers)\n     */\n    function _calculateTotalWinningPayouts(uint256 roundId)\n        internal\n        view\n        returns (uint256 totalOwed)\n    {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n\n        for (uint256 matchIndex = 0; matchIndex \u003c 10; matchIndex++) {\n            IGameEngine.Match memory matchResult = gameEngine.getMatch(roundId, matchIndex);\n            MatchPool storage pool = accounting.matchPools[matchIndex];\n            LockedOdds storage odds = accounting.lockedMatchOdds[matchIndex];\n\n            IGameEngine.MatchOutcome winningOutcome = matchResult.outcome;\n            if (winningOutcome == IGameEngine.MatchOutcome.PENDING) continue;\n\n            uint8 outcomeAsUint8;\n            if (winningOutcome == IGameEngine.MatchOutcome.HOME_WIN) outcomeAsUint8 = 1;\n            else if (winningOutcome == IGameEngine.MatchOutcome.AWAY_WIN) outcomeAsUint8 = 2;\n            else outcomeAsUint8 = 3; // DRAW\n\n            uint256 winningPool = _getWinningPoolAmount(pool, outcomeAsUint8);\n            if (winningPool == 0) continue;\n\n            // Get locked odds for winning outcome\n            uint256 lockedOdds;\n            if (outcomeAsUint8 == 1) {\n                lockedOdds = odds.homeOdds;\n            } else if (outcomeAsUint8 == 2) {\n                lockedOdds = odds.awayOdds;\n            } else {\n                lockedOdds = odds.drawOdds;\n            }\n\n            // Total owed = winning pool × locked odds\n            uint256 totalOwedForMatch = (winningPool * lockedOdds) / 1e18;\n            totalOwed += totalOwedForMatch;\n        }\n\n        return totalOwed;\n    }\n\n    /**\n     * @notice Get the winning pool amount for a given outcome\n     */\n    function _getWinningPoolAmount(MatchPool storage pool, uint8 outcome)\n        internal\n        view\n        returns (uint256)\n    {\n        if (outcome == 1) return pool.homeWinPool;\n        if (outcome == 2) return pool.awayWinPool;\n        return pool.drawPool;\n    }\n\n    // ============ Admin Functions ============\n\n    /**\n     * @notice Fund protocol reserve (required for bonuses and seeding)\n     * @param amount Amount of LEAGUE to add\n     */\n    /**\n     * @notice Calculate maximum possible payout for a bet\n     * @dev Used to check if LP pool can cover potential winnings\n     */\n    function _calculateMaxPayout(uint256 amount, uint256 numMatches, uint256 parlayMultiplier)\n        internal\n        pure\n        returns (uint256)\n    {\n        // Pessimistic estimate: assume best case odds (2x per match in worst case)\n        uint256 maxBasePayout = amount * (2 ** numMatches);\n\n        // Apply parlay multiplier\n        uint256 maxFinalPayout = (maxBasePayout * parlayMultiplier) / 1e18;\n\n        // Apply per-bet cap\n        if (maxFinalPayout \u003e MAX_PAYOUT_PER_BET) {\n            maxFinalPayout = MAX_PAYOUT_PER_BET;\n        }\n\n        return maxFinalPayout;\n    }\n\n    /**\n     * @notice Update rewards distributor address\n     */\n    function setRewardsDistributor(address _distributor) external onlyOwner {\n        require(_distributor != address(0), \"Invalid address\");\n        rewardsDistributor = _distributor;\n    }\n\n    // ============ View Functions ============\n\n    /**\n     * @notice Get the locked odds for a match (fixed at seeding time)\n     * @dev These odds NEVER change after seeding, even as bets come in\n     * @param roundId The round ID\n     * @param matchIndex The match index (0-9)\n     * @return homeOdds Home win odds (e.g., 1.5e18 = 1.5x)\n     * @return awayOdds Away win odds\n     * @return drawOdds Draw odds\n     * @return locked Whether odds are locked\n     */\n    function getLockedOdds(uint256 roundId, uint256 matchIndex)\n        external\n        view\n        returns (\n            uint256 homeOdds,\n            uint256 awayOdds,\n            uint256 drawOdds,\n            bool locked\n        )\n    {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        LockedOdds storage odds = accounting.lockedMatchOdds[matchIndex];\n\n        return (odds.homeOdds, odds.awayOdds, odds.drawOdds, odds.locked);\n    }\n\n    /**\n     * @notice Get preview of match odds before any bets (for frontend)\n     * @dev Shows initial seeded odds for a match based on team matchup\n     * @param roundId The round ID\n     * @param matchIndex The match index (0-9)\n     * @return homeOdds Home win odds (e.g., 1.2e18 = 1.2x)\n     * @return awayOdds Away win odds\n     * @return drawOdds Draw odds\n     */\n    function previewMatchOdds(uint256 roundId, uint256 matchIndex)\n        external\n        view\n        returns (\n            uint256 homeOdds,\n            uint256 awayOdds,\n            uint256 drawOdds\n        )\n    {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        MatchPool storage pool = accounting.matchPools[matchIndex];\n\n        // Use CURRENT pool state (includes all bets placed so far)\n        uint256 homePool = pool.homeWinPool;\n        uint256 awayPool = pool.awayWinPool;\n        uint256 drawPool = pool.drawPool;\n        uint256 totalPool = pool.totalPool;\n\n        // If not seeded yet, calculate what the initial odds would be\n        if (totalPool == 0) {\n            (homePool, awayPool, drawPool) = _calculateMatchSeeds(roundId, matchIndex);\n            totalPool = homePool + awayPool + drawPool;\n        }\n\n        // Apply virtual liquidity to dampen price impact\n        uint256 virtualLiquidity = SEED_PER_MATCH * VIRTUAL_LIQUIDITY_MULTIPLIER; // 3000 LEAGUE virtual\n\n        // Add virtual liquidity to each pool\n        uint256 virtualHomePool = homePool + (virtualLiquidity / 3);\n        uint256 virtualAwayPool = awayPool + (virtualLiquidity / 3);\n        uint256 virtualDrawPool = drawPool + (virtualLiquidity / 3);\n        uint256 virtualTotalPool = virtualHomePool + virtualAwayPool + virtualDrawPool;\n\n        // Calculate dampened market odds\n        homeOdds = (virtualTotalPool * 1e18) / virtualHomePool;\n        awayOdds = (virtualTotalPool * 1e18) / virtualAwayPool;\n        drawOdds = (virtualTotalPool * 1e18) / virtualDrawPool;\n\n        return (homeOdds, awayOdds, drawOdds);\n    }\n\n    /**\n     * @notice Get odds for all 10 matches in a round\n     * @param roundId The round ID\n     * @return homeOdds Array of home win odds (1e18 scale)\n     * @return awayOdds Array of away win odds (1e18 scale)\n     * @return drawOdds Array of draw odds (1e18 scale)\n     */\n    function getAllMatchOdds(uint256 roundId)\n        external\n        view\n        returns (\n            uint256[10] memory homeOdds,\n            uint256[10] memory awayOdds,\n            uint256[10] memory drawOdds\n        )\n    {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n\n        for (uint256 i = 0; i \u003c 10; i++) {\n            MatchPool storage pool = accounting.matchPools[i];\n\n            uint256 homePool = pool.homeWinPool;\n            uint256 awayPool = pool.awayWinPool;\n            uint256 drawPool = pool.drawPool;\n            uint256 totalPool = pool.totalPool;\n\n            // If not seeded yet, calculate initial odds\n            if (totalPool == 0) {\n                (homePool, awayPool, drawPool) = _calculateMatchSeeds(roundId, i);\n                totalPool = homePool + awayPool + drawPool;\n            }\n\n            // Apply virtual liquidity to dampen price impact\n            uint256 virtualLiquidity = SEED_PER_MATCH * VIRTUAL_LIQUIDITY_MULTIPLIER; // 3000 LEAGUE\n\n            // Add virtual liquidity to each pool\n            uint256 virtualHomePool = homePool + (virtualLiquidity / 3);\n            uint256 virtualAwayPool = awayPool + (virtualLiquidity / 3);\n            uint256 virtualDrawPool = drawPool + (virtualLiquidity / 3);\n            uint256 virtualTotalPool = virtualHomePool + virtualAwayPool + virtualDrawPool;\n\n            // Calculate dampened odds\n            homeOdds[i] = (virtualTotalPool * 1e18) / virtualHomePool;\n            awayOdds[i] = (virtualTotalPool * 1e18) / virtualAwayPool;\n            drawOdds[i] = (virtualTotalPool * 1e18) / virtualDrawPool;\n        }\n    }\n\n    /**\n     * @notice Get current parlay multiplier for a bet (preview before placing)\n     * @dev Shows users what multiplier they'll get with FOMO tier visibility\n     * @param roundId The round ID\n     * @param matchIndices Array of match indices\n     * @param numLegs Number of legs in the parlay\n     * @return currentMultiplier The multiplier they would get now (1e18 scale)\n     * @return currentTier Current count-based tier (1-5)\n     * @return parlaysLeftInTier How many parlays left in current tier\n     * @return nextTierMultiplier What multiplier drops to in next tier\n     */\n    function getCurrentParlayMultiplier(\n        uint256 roundId,\n        uint256[] calldata matchIndices,\n        uint256 numLegs\n    )\n        external\n        view\n        returns (\n            uint256 currentMultiplier,\n            uint256 currentTier,\n            uint256 parlaysLeftInTier,\n            uint256 nextTierMultiplier\n        )\n    {\n        if (numLegs == 1) {\n            return (1e18, 0, 0, 0); // Single bets don't use tiers\n        }\n\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        uint256 parlayCount = accounting.parlayCount;\n\n        // Determine current tier\n        if (parlayCount \u003c COUNT_TIER_1) {\n            currentTier = 1;\n            parlaysLeftInTier = COUNT_TIER_1 - parlayCount;\n            nextTierMultiplier = COUNT_MULT_TIER_2;\n        } else if (parlayCount \u003c COUNT_TIER_2) {\n            currentTier = 2;\n            parlaysLeftInTier = COUNT_TIER_2 - parlayCount;\n            nextTierMultiplier = COUNT_MULT_TIER_3;\n        } else if (parlayCount \u003c COUNT_TIER_3) {\n            currentTier = 3;\n            parlaysLeftInTier = COUNT_TIER_3 - parlayCount;\n            nextTierMultiplier = COUNT_MULT_TIER_4;\n        } else if (parlayCount \u003c COUNT_TIER_4) {\n            currentTier = 4;\n            parlaysLeftInTier = COUNT_TIER_4 - parlayCount;\n            nextTierMultiplier = COUNT_MULT_TIER_5;\n        } else {\n            currentTier = 5;\n            parlaysLeftInTier = 0; // Final tier\n            nextTierMultiplier = COUNT_MULT_TIER_5;\n        }\n\n        // Calculate actual multiplier using layered model\n        currentMultiplier = _getParlayMultiplierDynamicPreview(\n            matchIndices,\n            roundId,\n            numLegs\n        );\n\n        return (currentMultiplier, currentTier, parlaysLeftInTier, nextTierMultiplier);\n    }\n\n    function getMatchPoolData(uint256 roundId, uint256 matchIndex)\n        external\n        view\n        returns (\n            uint256 homePool,\n            uint256 awayPool,\n            uint256 drawPool,\n            uint256 totalPool\n        )\n    {\n        MatchPool storage pool = roundAccounting[roundId].matchPools[matchIndex];\n        return (pool.homeWinPool, pool.awayWinPool, pool.drawPool, pool.totalPool);\n    }\n\n    function getBet(uint256 betId)\n        external\n        view\n        returns (\n            address bettor,\n            uint256 roundId,\n            uint256 amount,\n            uint256 bonus,\n            uint256 lockedMultiplier,\n            bool settled,\n            bool claimed\n        )\n    {\n        Bet storage bet = bets[betId];\n        return (\n            bet.bettor,\n            bet.roundId,\n            bet.amount,\n            bet.bonus,\n            bet.lockedMultiplier,\n            bet.settled,\n            bet.claimed\n        );\n    }\n\n    function getUserBets(address user) external view returns (uint256[] memory) {\n        return userBets[user];\n    }\n\n    function getBetPredictions(uint256 betId) external view returns (Prediction[] memory) {\n        return bets[betId].predictions;\n    }\n\n    /**\n     * @notice Get round accounting data including parlay count\n     * @param roundId The round ID\n     * @return totalBetVolume Total amount bet in the round (including bonuses)\n     * @return totalReservedForWinners Total amount reserved for winners\n     * @return protocolRevenueShare Protocol's revenue share\n     * @return seasonRevenueShare Season pool's revenue share\n     * @return parlayCount Number of parlays placed in the round\n     */\n    function getRoundAccounting(uint256 roundId)\n        external\n        view\n        returns (\n            uint256 totalBetVolume,\n            uint256 totalReservedForWinners,\n            uint256 protocolRevenueShare,\n            uint256 seasonRevenueShare,\n            uint256 parlayCount\n        )\n    {\n        RoundAccounting storage accounting = roundAccounting[roundId];\n        return (\n            accounting.totalBetVolume,\n            accounting.totalReservedForWinners,\n            accounting.protocolRevenueShare,\n            accounting.seasonRevenueShare,\n            accounting.parlayCount\n        );\n    }\n\n    /**\n     * @notice Calculate current market odds for a match outcome\n     * @param roundId The round ID\n     * @param matchIndex The match index (0-9)\n     * @param outcome The outcome (1=HOME, 2=AWAY, 3=DRAW)\n     * @return odds The current market odds in 1e18 scale (e.g., 2.5e18 = 2.5x)\n     */\n    function getMarketOdds(uint256 roundId, uint256 matchIndex, uint8 outcome)\n        external\n        view\n        returns (uint256 odds)\n    {\n        MatchPool storage pool = roundAccounting[roundId].matchPools[matchIndex];\n\n        uint256 winningPool = _getWinningPoolAmount(pool, outcome);\n        uint256 losingPool = pool.totalPool - winningPool;\n\n        if (winningPool == 0) return 3e18; // Fallback: fair VRF odds (33.33% = 3.0x)\n\n        // Apply virtual liquidity to dampen price impact\n        // This makes pools behave like they have VIRTUAL_LIQUIDITY_MULTIPLIER times more depth\n        uint256 virtualLiquidity = SEED_PER_MATCH * VIRTUAL_LIQUIDITY_MULTIPLIER; // 3000 LEAGUE virtual\n\n        // Add virtual liquidity proportionally (33.33% per outcome for balanced distribution)\n        uint256 virtualWinningPool = winningPool + (virtualLiquidity / 3);\n        uint256 virtualLosingPool = losingPool + (virtualLiquidity * 2 / 3);\n\n        // Calculate odds with dampened pools\n        uint256 distributedLosingPool = (virtualLosingPool * WINNER_SHARE) / 10000;\n        odds = 1e18 + (distributedLosingPool * 1e18) / virtualWinningPool;\n\n        return odds;\n    }\n\n    /**\n     * @notice Preview bet payout (including parlay multiplier)\n     * @dev Uses locked multiplier from bet placement for accurate preview\n     */\n    function previewBetPayout(uint256 betId)\n        external\n        view\n        returns (bool won, uint256 basePayout, uint256 finalPayout, uint256 parlayMultiplier)\n    {\n        (won, basePayout, finalPayout) = _calculateBetPayout(betId);\n        parlayMultiplier = bets[betId].lockedMultiplier;  // Use locked multiplier\n        return (won, basePayout, finalPayout, parlayMultiplier);\n    }\n\n    // ============ Season Reward Management (H-2 Fix) ============\n\n    /**\n     * @notice Distribute season rewards to specified address\n     * @param recipient Address to receive season rewards\n     * @param amount Amount to distribute\n     * @dev Only owner can distribute season rewards\n     */\n    function distributeSeasonRewards(address recipient, uint256 amount) external onlyOwner {\n        require(amount \u003c= seasonRewardPool, \"Amount exceeds season pool\");\n        require(recipient != address(0), \"Invalid recipient\");\n\n        seasonRewardPool -= amount;\n        leagueToken.safeTransfer(recipient, amount);\n\n        emit SeasonRewardsDistributed(recipient, amount);\n    }\n\n    /**\n     * @notice Emergency recovery of season pool funds\n     * @param amount Amount to recover\n     * @dev Only owner - use if season never completes or predictor contract fails\n     */\n    function emergencyRecoverSeasonPool(uint256 amount) external onlyOwner {\n        require(amount \u003c= seasonRewardPool, \"Amount exceeds season pool\");\n\n        seasonRewardPool -= amount;\n        leagueToken.safeTransfer(owner(), amount);\n\n        emit SeasonPoolRecovered(owner(), amount);\n    }\n\n    // ============ Events for Season Reward Management ============\n\n    event SeasonRewardsDistributed(address indexed recipient, uint256 amount);\n    event SeasonPoolRecovered(address indexed owner, uint256 amount);\n}\n, there are many features to look at, Locked odds (1.25x - 1.95x range)\nOdds-weighted allocation (Solidity's fund splitting logic)\nLP share system\nRisk management caps\nAdvanced profit tracking\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhallelx2%2Fphantomzero-vrf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhallelx2%2Fphantomzero-vrf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhallelx2%2Fphantomzero-vrf/lists"}