oil_api/state/well.rs
1use serde::{Deserialize, Serialize};
2use steel::*;
3
4use crate::state::well_pda;
5
6use super::{OilAccount, Auction};
7
8/// Well account (one per well)
9/// PDA: [WELL, well_id]
10/// Tracks current auction state for a well
11#[repr(C)]
12#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
13pub struct Well {
14 /// Well ID (0-3) - which well this is for
15 pub well_id: u64,
16
17 /// Current epoch ID (increments each auction: 0, 1, 2, 3, etc.)
18 /// Starts at 0, increments when bid happens
19 pub epoch_id: u64,
20
21 /// Current bidder/owner (Pubkey::default() if unowned)
22 pub current_bidder: Pubkey,
23
24 /// Initial price for current epoch (in lamports)
25 /// Doubles from current price when bid happens
26 pub init_price: u64,
27
28 /// Mining per second (MPS) - current mining rate (OIL per second, in atomic units)
29 /// This is the base rate adjusted for halvings
30 pub mps: u64,
31
32 /// Epoch start time (timestamp when current epoch started)
33 pub epoch_start_time: u64,
34
35 /// Accumulated OIL mined by current owner (not yet claimed)
36 pub accumulated_oil: u64,
37
38 /// Last time accumulated_oil was updated
39 pub last_update_time: u64,
40
41 /// Number of halvings that have occurred (for rate calculation)
42 pub halving_count: u64,
43
44 /// Total OIL ever mined from this well (lifetime)
45 pub lifetime_oil_mined: u64,
46
47 /// Flag indicating if owned by pool (1) or solo owner (0)
48 /// When pool wins (pool_total >= current_price), this is set to 1
49 pub is_pool_owned: u64,
50
51 /// Total OIL mined by current operator (doesn't reset when claimed, only when ownership changes)
52 /// Repurposed from buffer_a
53 pub operator_total_oil_mined: u64,
54
55 /// Last epoch ID that was synced (similar to checkpoint_id in Driller)
56 /// Used to enforce sync_auction_state before set_bid (like checkpoint before deploy)
57 /// Repurposed from buffer_b
58 pub last_synced_epoch_id: u64,
59
60 /// Accumulated OIL from a pool that was outbid by a solo bidder
61 /// When a solo bid outbids a pool, the pool's accumulated_oil is stored here
62 /// Pool contributors can claim their proportional share from this amount
63 /// Repurposed from buffer_c
64 pub pool_accumulated_oil: u64,
65
66 /// Original pool_total from when a pool was outbid (for calculating proportional shares)
67 /// When a solo bid outbids a pool, the original pool_total is stored here
68 /// Repurposed from buffer_d
69 pub outbid_pool_total: u64,
70
71 /// 86% SOL from bidder when a pool is outbid (for proportional distribution to pool contributors)
72 /// When a solo bid outbids a pool, the 86% SOL amount is stored here
73 /// Pool contributors can claim their proportional share from this amount
74 pub pool_outbid_sol: u64,
75}
76
77impl Well {
78 pub fn pda(well_id: u64) -> (Pubkey, u8) {
79 well_pda(well_id)
80 }
81
82 /// Calculate current price for this epoch (Dutch auction - price decreases over time)
83 pub fn current_price(&self, auction: &Auction, clock: &Clock) -> u64 {
84 // If well has no owner (never been bid on), show starting price
85 use solana_program::pubkey::Pubkey;
86 if self.current_bidder == Pubkey::default() && self.is_pool_owned == 0 {
87 return self.init_price; // Return starting price for unowned wells
88 }
89
90 let elapsed = clock.unix_timestamp.saturating_sub(self.epoch_start_time as i64);
91 let duration = auction.auction_duration_seconds as i64;
92
93 if elapsed >= duration {
94 return 0; // Auction expired, free to claim
95 }
96
97 // Linear decay: price = init_price * (1 - elapsed / duration)
98 // Applies to both solo-owned and pool-owned wells
99 let remaining = duration - elapsed;
100 (self.init_price as u128 * remaining as u128 / duration as u128) as u64
101 }
102
103 /// Update accumulated OIL for this epoch state
104 pub fn update_accumulated_oil(&mut self, clock: &Clock) {
105 // Skip if no owner
106 if self.current_bidder == Pubkey::default() && self.is_pool_owned == 0 {
107 return;
108 }
109
110 let last_update = self.last_update_time as i64;
111 let elapsed = clock.unix_timestamp.saturating_sub(last_update);
112 if elapsed <= 0 {
113 return;
114 }
115
116 // Calculate OIL mined: rate * time
117 let oil_mined = self.mps
118 .checked_mul(elapsed as u64)
119 .unwrap_or(0);
120
121 self.accumulated_oil = self.accumulated_oil
122 .checked_add(oil_mined)
123 .unwrap_or(u64::MAX);
124
125 self.lifetime_oil_mined = self.lifetime_oil_mined
126 .checked_add(oil_mined)
127 .unwrap_or(u64::MAX);
128
129 // Track total mined by current operator (persists even after claiming)
130 self.operator_total_oil_mined = self.operator_total_oil_mined
131 .checked_add(oil_mined)
132 .unwrap_or(u64::MAX);
133
134 self.last_update_time = clock.unix_timestamp as u64;
135 }
136
137 /// Check and apply halving if needed, updating mining rate
138 /// Supply-based halving: halvings occur when total_auction_oil_minted reaches thresholds
139 /// Thresholds: 1M, 2.5M, 5M, 10M, 20M, 40M, etc. (exponential)
140 /// Uses 25% reduction (multiply by 0.75) per halving for sustainable supply control
141 pub fn check_and_apply_halving(&mut self, auction: &mut Auction) {
142 // Check if we should apply halvings based on total OIL minted
143 let halvings_to_apply = auction.should_apply_halving();
144
145 if halvings_to_apply > 0 {
146 // Apply halvings: 25% reduction (multiply by 0.75) per halving
147 // Formula: new_rate = old_rate * 0.75 = old_rate * 3 / 4
148 for _ in 0..halvings_to_apply {
149 self.mps = (self.mps * 3) / 4; // 25% reduction (multiply by 0.75)
150 self.halving_count += 1;
151 }
152
153 // Update auction halving count (only update once per halving event)
154 // Note: This will be called for each epoch state, but we only update once
155 auction.halving_count += halvings_to_apply;
156 }
157 }
158}
159
160account!(OilAccount, Well);
161