Skip to main content

Algorithms

Role of HyperPredictV1Pair

HyperPredictV1Pair is an on-chain pair tied to a single underlying asset and trading interval (intervalSeconds). Each pair is responsible for:

  • letting the operator (an automation bot) pull prices from the Pyth oracle and progress the rounds (epoch) sequentially
  • accepting bettors' stakes and positions (Bull/Bear) and storing them inside the ledger and userRounds maps
  • calculating the reward base (rewardBaseCalAmount) and the amount to distribute (rewardAmount) whenever a round closes successfully
  • protecting funds during invalid rounds or oracle outages and allowing refunds when necessary

The sections below organize the round lifecycle, Pyth integration, safety mechanisms, and reward algorithms in that order.

Round Lifecycle

Each round has three phases, Start → Lock → Close, and every phase is separated by intervalSeconds. The bet window spans startTimestamp through lockTimestamp, prices are observed until closeTimestamp, and executeRound advances the system to the next step.

Normal round timeline

  • Call genesisStartRound and genesisLockRound once each to bootstrap the very first round.
  • After genesis, executeRound handles three actions in a single transaction for round n:
    1. Lock round n-1 using the latest oracle price.
    2. Close round n-2 and fix its final price.
    3. Open round n for new bets.
  • To allow a safe halt even when consecutive calls are delayed, the factory enforces a global bufferSeconds. Any phase that fails to finish within bufferSeconds is rejected.

Pyth Oracle Integration and Fallbacks

_getPriceFromOracle relies on Pyth’s getPriceNoOlderThan, so only prices published within bufferSeconds are accepted. Safety is ensured by:

  • storing the publish time (publishTime) as oracleLatestRoundId, preventing the operator from accidentally using stale data
  • normalizing Pyth’s expo to PYTH_PRICE_DECIMALS (=8) and adding overflow checks even for negative values
  • keeping oracleCalled as false whenever the price cannot be fetched or updates lag beyond bufferSeconds, which triggers the refund path described later

Missing oracle round

When price retrieval fails, the round is marked as “missing.” Because it never satisfies the claimable condition, bettors can reclaim their full stake via the refundable path. The operator can safely resume by calling pause/unpause or by replaying the genesis steps.

Bet Intake and State Management

  • betBull and betBear are protected by whenNotPaused and nonReentrant, and the notContract modifier restricts access to EOAs only.
  • For each round, stakes are added to round.totalAmount and to the side-specific buckets (bullAmount/bearAmount); mixing positions within the same round is prohibited.
  • A bet must be greater than or equal to factory.minBetAmount() to block dust spam.
  • ledger[epoch][user] stores the bet amount and its claimed status, both of which can be paginated through getUserRounds.
  • If a round ends with oracleCalled == false, a user can request a refund once block.timestamp > closeTimestamp + bufferSeconds.

Reward Calculation Algorithm

Rewards for round n-2 are finalized when the operator calls executeRound. The contract uses the following equations:

treasuryAmt = total * treasuryFee / 10000
rewardAmount = total - treasuryAmt
rewardBaseCalAmount = closePrice > lockPrice ? bullAmount : bearAmount
  • treasuryFee, treasuryFeeWithReferral, and referralFee are configured at the factory level (see hyperpredict-contract-v1/config/HyperPredictV1Factory.ts). On BSC the values are 3%, 1%, and 1% respectively.
  • When closePrice == lockPrice (a draw), rewardAmount becomes 0 and everyone receives a refund.
  • The winning player’s base payout is userAmount / rewardBaseCalAmount * rewardAmount. This equation always uses treasuryFee (3% on BSC) to keep the math deterministic on-chain, even if a bettor later qualifies for referral rebates.
  • When a round is invalidated, _calculateRewards exits early and the pot is refundable.

Referral Allocation

Referral handling is deferred to claim time and runs inside _applyReferralBonus only when the bettor has a registered referrer:

  • referralFee (1%) defines the pool that funds the referrer. The user’s share is referrerBonus = total * referralFee / 10000 * userBet / rewardBaseCalAmount, which is subtracted from the winner’s payout and transferred to the referrer.
  • To offset that subtraction and to make the treasury effectively charge only treasuryFeeWithReferral (1%) whenever a referral exists, winners also receive a rebate. The rebate pool is (treasuryFee - treasuryFeeWithReferral) (2% on BSC) and the winner’s portion is winnerBonus = total * (treasuryFee - treasuryFeeWithReferral) / 10000 * userBet / rewardBaseCalAmount. This amount is debited from the accumulated treasury balance when the claim executes.
  • The final reward paid to the bettor is baseReward + winnerBonus - referrerBonus. When no referrer is present, _applyReferralBonus exits early and the bettor simply receives baseReward, so the treasury keeps the full 3%.

This lazy calculation ensures that bettors with a referral net out to the cheaper fee schedule (1% treasury + 1% referral), while bettors without a referral remain on the 3% flat treasury fee.

Claim-Time Execution

When a user calls claim, _claim evaluates each epoch in order:

  1. If oracleCalled == true and the entry is claimable, compute the baseReward for the winning side.
  2. Run _applyReferralBonus to settle the referral split and finalize the winnerBonus plus any referrer reward.
  3. If oracleCalled == false while refundable holds, simply return the original stake.
  4. Sum every addedReward, send the total in native currency, and if referral bonuses accumulated, send those in a separate transfer while emitting ReferralPaid.

Concrete Example

A: Stake 100 USDC on Bull
B: Stake 100 USDC on Bear
C: Referrer (invited A)
treasuryFee = 3%
treasuryFeeWithReferral = 1%
referralFee = 1%
Round result: Bull wins
  • totalAmount = 200 USDC
  • treasuryAmount (initial) = 200 * 0.03 = 6 USDC
  • rewardAmount = 200 - 6 = 194 USDC
  • rewardBaseCalAmount = bullAmount = 100 USDC
  • A’s baseReward = 100 / 100 * 194 = 194 USDC
  • Referral pool referralPool = 200 * 0.01 = 2 USDC so referrerBonus = 2 USDC
  • Treasury rebate pool treasuryRefundPool = 200 * (0.03 - 0.01) = 4 USDC so winnerBonus = 4 USDC
  • Winner receives 194 + 4 - 2 = 196 USDC
  • Referrer receives 2 USDC
  • Treasury keeps 6 - 4 = 2 USDC, which matches the 1% schedule for referred bets

If the bettor had no referrer, _applyReferralBonus would skip the rebate path entirely: A would claim just the baseReward of 194 USDC and the treasury would retain the full 3% (6 USDC). B still loses because their position finished out of the money unless the oracle failed, in which case both sides can refund their stakes.

Safety and Fail-Safes

  • Role separation: onlyOperator drives the round progression, while onlyAdmin handles treasury actions (claimTreasury, recoverToken).
  • Pausable + Genesis: Abnormal conditions can be halted with pause, then restarted cleanly via genesisStartRound/genesisLockRound, avoiding duplicate or undefined rounds.
  • ReentrancyGuard & non-contract gate: Every function, including payout paths, is non-reentrant, and EOAs only are allowed, blocking multicall contracts.
  • Buffer enforcement: _safeLockRound and _safeEndRound reject operations outside bufferSeconds, stopping timing games and price manipulation.
  • Fund protection: claimable/refundable conditions prevent double claims while guaranteeing principal returns during oracle downtime.
  • Oracle input validation: getPriceNoOlderThan discards stale data and sanitizes negative values or overflow scenarios.

Together these mechanisms ensure HyperPredictV1Pair can run an entirely on-chain prediction market safely, autonomously, and with transparent payout and referral accounting.