Skip to main content

streak_api/state/
treasury.rs

1//! Protocol USDC custody (`treasury` PDA).
2//!
3//! The treasury serves two roles:
4//!
5//! 1. **USDC custodian** — the treasury ATA holds all deposited USDC. Deposits come in via
6//!    `BuyTicket` / `AdminRouteFees`; USDC out via `AdminDisburseFromTreasury` (executor-signed).
7//!
8//! 2. **Price chain** — `last_close_*` carries the Pyth close price from the most recently
9//!    settled period. `AdminInstantSettlement` reads it as the open reference and writes the
10//!    new close price back, forming a self-perpetuating chain. Zero until `Initialize` seeds it.
11
12use steel::*;
13
14use super::{treasury_pda, StreakAccount};
15
16#[repr(C)]
17#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
18pub struct Treasury {
19    /// Today's playable daily vault (T+1 locked at UTC midnight via `AdminRotateDailyVault`).
20    pub daily_jackpot: u64,
21    /// Weekly accrual bucket — debited on weekly disbursement, then finalized to stability.
22    pub weekly_jackpot: u64,
23    /// Buyback reserve (10% of ticket sales).
24    pub buyback: u64,
25    /// Close price from the most recently settled period — used as the open reference
26    /// for the next settlement. Zero until `Initialize` seeds it.
27    pub last_close_price: i64,
28    /// Pyth exponent matching `last_close_price`.
29    pub last_close_expo: i32,
30    pub _pad_close: [u8; 4],
31    /// `publish_time` from the Pyth feed at last settlement.
32    pub last_close_publish_time: i64,
33    /// Stability reserve — excess weekly accrual + 60% of routed trading fees (brief v2).
34    pub stability_reserve: u64,
35    /// T+1 accrual bucket — ticket 70% during UTC day D; rotated into `daily_jackpot` at day end.
36    pub daily_accrual: u64,
37}
38
39impl Treasury {
40    pub fn pda() -> (Pubkey, u8) {
41        treasury_pda()
42    }
43
44    /// Body length before `stability_reserve` (deployed accounts pre-0.3.12).
45    pub const LEGACY_BODY_LEN: usize = 48;
46    /// Body length before `daily_accrual` (accounts with stability, pre-0.3.17).
47    pub const PRE_DAILY_ACCRUAL_BODY_LEN: usize = 56;
48
49    /// Decode raw treasury account data (8-byte Steel discriminator + body).
50    pub fn decode_account_data(data: &[u8]) -> Result<Self, &'static str> {
51        const DISC: usize = 8;
52        let body = data.get(DISC..).ok_or("treasury account empty")?;
53        let full = std::mem::size_of::<Self>();
54        if body.len() >= full {
55            return Ok(*bytemuck::from_bytes(&body[..full]));
56        }
57        if body.len() >= Self::PRE_DAILY_ACCRUAL_BODY_LEN {
58            #[repr(C)]
59            #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
60            struct WithStability {
61                daily_jackpot: u64,
62                weekly_jackpot: u64,
63                buyback: u64,
64                last_close_price: i64,
65                last_close_expo: i32,
66                _pad_close: [u8; 4],
67                last_close_publish_time: i64,
68                stability_reserve: u64,
69            }
70            let ws: &WithStability =
71                bytemuck::from_bytes(&body[..Self::PRE_DAILY_ACCRUAL_BODY_LEN]);
72            return Ok(Self {
73                daily_jackpot: ws.daily_jackpot,
74                weekly_jackpot: ws.weekly_jackpot,
75                buyback: ws.buyback,
76                last_close_price: ws.last_close_price,
77                last_close_expo: ws.last_close_expo,
78                _pad_close: ws._pad_close,
79                last_close_publish_time: ws.last_close_publish_time,
80                stability_reserve: ws.stability_reserve,
81                daily_accrual: 0,
82            });
83        }
84        if body.len() >= Self::LEGACY_BODY_LEN {
85            #[repr(C)]
86            #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
87            struct Legacy {
88                daily_jackpot: u64,
89                weekly_jackpot: u64,
90                buyback: u64,
91                last_close_price: i64,
92                last_close_expo: i32,
93                _pad_close: [u8; 4],
94                last_close_publish_time: i64,
95            }
96            let leg: &Legacy = bytemuck::from_bytes(&body[..Self::LEGACY_BODY_LEN]);
97            return Ok(Self {
98                daily_jackpot: leg.daily_jackpot,
99                weekly_jackpot: leg.weekly_jackpot,
100                buyback: leg.buyback,
101                last_close_price: leg.last_close_price,
102                last_close_expo: leg.last_close_expo,
103                _pad_close: leg._pad_close,
104                last_close_publish_time: leg.last_close_publish_time,
105                stability_reserve: 0,
106                daily_accrual: 0,
107            });
108        }
109        Err("treasury account too short")
110    }
111}
112
113account!(StreakAccount, Treasury);