oil_api/state/
round.rs

1use serde::{Deserialize, Serialize};
2use steel::*;
3
4use crate::state::round_pda;
5
6use super::OilAccount;
7
8#[repr(C)]
9#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize, Default)]
10pub struct Round {
11    /// The round number.
12    pub id: u64,
13
14    /// The amount of SOL deployed in each square.
15    pub deployed: [u64; 25],
16
17    /// The hash of the end slot, provided by solana, used for random number generation.
18    pub slot_hash: [u8; 32],
19
20    /// The count of miners on each square.
21    pub count: [u64; 25],
22
23    /// The slot at which claims for this round account end.
24    pub expires_at: u64,
25
26    /// The account to which rent should be returned when this account is closed.
27    pub rent_payer: Pubkey,
28
29    /// The top miner of the round.
30    pub top_miner: Pubkey,
31
32    /// The amount of OIL to distribute to the top miner.
33    pub top_miner_reward: u64,
34
35    /// The total amount of SOL deployed in the round.
36    pub total_deployed: u64,
37
38    /// The total number of unique miners that played in the round.
39    pub total_miners: u64,
40
41    /// The total amount of SOL put in the OIL vault.
42    pub total_vaulted: u64,
43
44    /// The total amount of SOL won by miners for the round.
45    pub total_winnings: u64,
46
47    /// The amount of SOL in the gusher.
48    pub gusher_sol: u64,
49
50    // ===== Pool fields (added for pool feature) =====
51    
52    /// The amount of pooled SOL deployed in each square.
53    /// Used to calculate pool's share of winnings.
54    pub deployed_pooled: [u64; 25],
55
56    /// The total amount of pooled SOL deployed in the round.
57    pub total_pooled: u64,
58
59    /// The SOL rewards allocated to the pool for this round.
60    /// Calculated at reset: (pool's winning share / total winning) * total_winnings
61    pub pool_rewards_sol: u64,
62
63    /// The OIL rewards allocated to the pool for this round.
64    /// Calculated at reset: (pool's winning share / total winning) * oil_minted
65    pub pool_rewards_oil: u64,
66
67    /// The number of unique miners who deployed as pooled this round.
68    pub pool_members: u64,
69
70    /// The cumulative SOL deployed by solo miners before pool deployed to each square.
71    /// Used to determine if pool won the lottery (GODL-style).
72    /// Set when first pool deployment happens to each square.
73    pub pool_cumulative: [u64; 25],
74}
75
76impl Round {
77    pub fn pda(&self) -> (Pubkey, u8) {
78        round_pda(self.id)
79    }
80
81    pub fn rng(&self) -> Option<u64> {
82        if self.slot_hash == [0; 32] || self.slot_hash == [u8::MAX; 32] {
83            return None;
84        }
85        let r1 = u64::from_le_bytes(self.slot_hash[0..8].try_into().unwrap());
86        let r2 = u64::from_le_bytes(self.slot_hash[8..16].try_into().unwrap());
87        let r3 = u64::from_le_bytes(self.slot_hash[16..24].try_into().unwrap());
88        let r4 = u64::from_le_bytes(self.slot_hash[24..32].try_into().unwrap());
89        let r = r1 ^ r2 ^ r3 ^ r4;
90        Some(r)
91    }
92
93    pub fn winning_square(&self, rng: u64) -> usize {
94        (rng % 25) as usize
95    }
96
97    pub fn top_miner_sample(&self, rng: u64, winning_square: usize) -> u64 {
98        if self.deployed[winning_square] == 0 {
99            return 0;
100        }
101        rng.reverse_bits() % self.deployed[winning_square]
102    }
103
104    pub fn calculate_total_winnings(&self, winning_square: usize) -> u64 {
105        let mut total_winnings = 0;
106        for (i, &deployed) in self.deployed.iter().enumerate() {
107            if i != winning_square {
108                total_winnings += deployed;
109            }
110        }
111        total_winnings
112    }
113
114    pub fn is_split_reward(&self, rng: u64) -> bool {
115        // One out of four rounds get split rewards.
116        let rng = rng.reverse_bits().to_le_bytes();
117        let r1 = u16::from_le_bytes(rng[0..2].try_into().unwrap());
118        let r2 = u16::from_le_bytes(rng[2..4].try_into().unwrap());
119        let r3 = u16::from_le_bytes(rng[4..6].try_into().unwrap());
120        let r4 = u16::from_le_bytes(rng[6..8].try_into().unwrap());
121        let r = r1 ^ r2 ^ r3 ^ r4;
122        r % 2 == 0
123    }
124
125    pub fn did_hit_gusher_sol_only(&self, rng: u64) -> bool {
126        rng.reverse_bits() % 325 == 0
127    }
128}
129
130account!(OilAccount, Round);
131
132#[cfg(test)]
133mod tests {
134    use solana_program::rent::Rent;
135
136    use super::*;
137
138    #[test]
139    fn test_rent() {
140        let size_of_round = 8 + std::mem::size_of::<Round>();
141        let required_rent = Rent::default().minimum_balance(size_of_round);
142        println!("Round account size: {} bytes", size_of_round);
143        println!("Required rent: {} lamports ({} SOL)", required_rent, required_rent as f64 / 1_000_000_000.0);
144        
145        // Verify the account size matches expected size (1008 bytes for v4 with pool_cumulative field)
146        // 8 bytes discriminator + 1000 bytes struct = 1008 bytes total
147        assert_eq!(size_of_round, 1008, "Round account size should be 1008 bytes (v4 with pool_cumulative, no padding)");
148        
149        // Verify rent is reasonable (should be > 0 and < 10 SOL for a 816 byte account)
150        assert!(required_rent > 0, "Required rent should be greater than 0");
151        assert!(required_rent < 10_000_000_000, "Required rent should be less than 10 SOL");
152    }
153}