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    // Auto-fold any remaining pre-open (committed) stakes into the live pool at settlement.
51    // This allows the listing-grid model where users pre-bet on future windows without
52    // requiring a separate merge sweep before every settlement.
53    market.total_up = market
54        .total_up
55        .checked_add(market.committed_up)
56        .ok_or(StreakError::Overflow)?;
57    market.total_down = market
58        .total_down
59        .checked_add(market.committed_down)
60        .ok_or(StreakError::Overflow)?;
61    market.committed_up = 0;
62    market.committed_down = 0;
63
64    let winner_total = if outcome == Market::SIDE_UP {
65        market.total_up
66    } else {
67        market.total_down
68    };
69    let loser_pot = if outcome == Market::SIDE_UP {
70        market.total_down
71    } else {
72        market.total_up
73    };
74
75    let reward_pool = if winner_total == 0 {
76        0u64
77    } else {
78        treasury.daily_jackpot.min(loser_pot)
79    };
80
81    if reward_pool > 0 {
82        treasury.daily_jackpot = treasury
83            .daily_jackpot
84            .checked_sub(reward_pool)
85            .ok_or(StreakError::InsufficientTreasury)?;
86    }
87
88    market.loser_pot = loser_pot;
89    market.reward_pool = reward_pool;
90    market.winning_total = winner_total;
91    market.outcome = outcome;
92    market.status = Market::STATUS_SETTLED;
93
94    // Empty winning side — bump monotonic **`next_period`** floor (no DISTRIBUTE queue).
95    // If **`winning_total > 0`**, payout then **`MARKET_LINE_RELEASE`** also bumps (**`max`** semantics).
96    if winner_total == 0 {
97        treasury.bump_next_period_floor(market.series_id, period)?;
98    }
99
100    Ok(())
101}