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 /// Daily jackpot pool — credited on ticket/fee inflows; debited on daily disbursement.
20 pub daily_jackpot: u64,
21 /// Weekly accrual bucket — credited all week; debited on weekly disbursement, then
22 /// swept to `stability_reserve` via `AdminFinalizeWeekly`.
23 pub weekly_jackpot: u64,
24 /// Buyback reserve (10% of ticket sales).
25 pub buyback: u64,
26 /// Close price from the most recently settled period — used as the open reference
27 /// for the next settlement. Zero until `Initialize` seeds it.
28 pub last_close_price: i64,
29 /// Pyth exponent matching `last_close_price`.
30 pub last_close_expo: i32,
31 pub _pad_close: [u8; 4],
32 /// `publish_time` from the Pyth feed at last settlement.
33 pub last_close_publish_time: i64,
34 /// Stability reserve — excess weekly accrual + 60% of routed trading fees (brief v2).
35 pub stability_reserve: u64,
36}
37
38impl Treasury {
39 pub fn pda() -> (Pubkey, u8) {
40 treasury_pda()
41 }
42
43 /// Body length before `stability_reserve` (deployed accounts pre-0.3.12).
44 pub const LEGACY_BODY_LEN: usize = 48;
45
46 /// Decode raw treasury account data (8-byte Steel discriminator + body).
47 ///
48 /// Legacy accounts are shorter; `stability_reserve` reads as 0 until the first
49 /// `AdminSetTreasuryPools` realloc + write.
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::LEGACY_BODY_LEN {
58 #[repr(C)]
59 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
60 struct Legacy {
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 }
69 let leg: &Legacy = bytemuck::from_bytes(&body[..Self::LEGACY_BODY_LEN]);
70 return Ok(Self {
71 daily_jackpot: leg.daily_jackpot,
72 weekly_jackpot: leg.weekly_jackpot,
73 buyback: leg.buyback,
74 last_close_price: leg.last_close_price,
75 last_close_expo: leg.last_close_expo,
76 _pad_close: leg._pad_close,
77 last_close_publish_time: leg.last_close_publish_time,
78 stability_reserve: 0,
79 });
80 }
81 Err("treasury account too short")
82 }
83}
84
85account!(StreakAccount, Treasury);