Skip to main content

streak_api/
settlement.rs

1//! Settlement helpers shared by **`Market`** ix (**`ADMIN_INSTANT`** and **`FINALIZE`** **`settlement_kind`** branches).
2
3use solana_program::{account_info::AccountInfo, program_error::ProgramError};
4
5use crate::error::StreakError;
6use crate::pyth::{load_fresh_price_hermes, outcome_from_open_close, Price};
7use crate::state::{Market, Treasury};
8
9/// UP/DOWN from **`Market::open_*`** vs fresh Hermes **`PriceUpdateV2`** close price.
10///
11/// Returns `Ok(None)` if the price move is below the void threshold (< 0.01%); the caller
12/// should abort settlement and invoke `AdminVoidMarket` instead.
13pub fn oracle_outcome_market_vs_pyth(
14    market: &Market,
15    pyth_price_feed_info: &AccountInfo<'_>,
16    unix_timestamp: i64,
17) -> Result<Option<u8>, ProgramError> {
18    if !market.has_open_oracle_snapshot() {
19        return Err(StreakError::MarketNotOracleAnchored.into());
20    }
21
22    let feed_id: [u8; 32] = market.pyth_price_feed.to_bytes();
23    let open_px = Price {
24        price: market.open_ref_price,
25        conf: 0,
26        expo: market.open_ref_expo,
27        publish_time: market.open_ref_publish_time,
28    };
29    let close_px = load_fresh_price_hermes(pyth_price_feed_info, &feed_id, unix_timestamp)?;
30    outcome_from_open_close(open_px, close_px)
31}
32
33/// Applies oracle outcome and settles **`Market`**. Jackpot subsidy **`reward_pool`** is derived on-chain:
34/// **`min(Treasury::daily_jackpot, loser_pot)`** when **`winner_total > 0`**, else **`0`** (no instruction argument).
35pub fn apply_settlement_outcome(
36    market: &mut Market,
37    treasury: &mut Treasury,
38    period: u64,
39    outcome: u8,
40) -> Result<(), ProgramError> {
41    if market.period != period {
42        return Err(StreakError::BadMarketState.into());
43    }
44    if market.status == Market::STATUS_VOIDED {
45        return Err(StreakError::MarketVoided.into());
46    }
47    if market.status != Market::STATUS_OPEN {
48        return Err(StreakError::BadMarketState.into());
49    }
50    if market.committed_up != 0 || market.committed_down != 0 {
51        return Err(StreakError::UnresolvedCommittedStakes.into());
52    }
53
54    let winner_total = if outcome == Market::SIDE_UP {
55        market.total_up
56    } else {
57        market.total_down
58    };
59    let loser_pot = if outcome == Market::SIDE_UP {
60        market.total_down
61    } else {
62        market.total_up
63    };
64
65    let reward_pool = if winner_total == 0 {
66        0u64
67    } else {
68        treasury.daily_jackpot.min(loser_pot)
69    };
70
71    if reward_pool > 0 {
72        treasury.daily_jackpot = treasury
73            .daily_jackpot
74            .checked_sub(reward_pool)
75            .ok_or(StreakError::InsufficientTreasury)?;
76    }
77
78    market.loser_pot = loser_pot;
79    market.reward_pool = reward_pool;
80    market.winning_total = winner_total;
81    market.outcome = outcome;
82    market.status = Market::STATUS_SETTLED;
83
84    // Empty winning side — bump monotonic **`next_period`** floor (no DISTRIBUTE queue).
85    // If **`winning_total > 0`**, payout then **`MARKET_LINE_RELEASE`** also bumps (**`max`** semantics).
86    if winner_total == 0 {
87        treasury.bump_next_period_floor(market.series_id, period)?;
88    }
89
90    Ok(())
91}