1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
use serde::{Deserialize, Serialize};
use steel::*;
use crate::state::well_pda;
use super::{OilAccount, Auction};
/// Well account (one per well)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct Well {
/// Well ID (0-3) - which well this is for
pub well_id: u64,
/// Current epoch ID (increments each auction: 0, 1, 2, 3, etc.)
pub epoch_id: u64,
/// Current bidder/owner (Pubkey::default() if unowned)
pub current_bidder: Pubkey,
/// Initial price for current epoch (in lamports)
pub init_price: u64,
/// Mining per second (MPS) - current mining rate (OIL per second, in atomic units)
pub mps: u64,
/// Epoch start time (timestamp when current epoch started)
pub epoch_start_time: u64,
/// Accumulated OIL mined by current owner (not yet claimed)
pub accumulated_oil: u64,
/// Last time accumulated_oil was updated
pub last_update_time: u64,
/// Number of halvings that have occurred (for rate calculation)
pub halving_count: u64,
/// Total OIL ever mined from this well (lifetime)
pub lifetime_oil_mined: u64,
/// Total OIL mined by current operator (doesn't reset when claimed, only when ownership changes)
pub operator_total_oil_mined: u64,
/// Buffer field (for future use) - previously is_pool_owned
pub buffer_c: u64,
/// Total contributed FOGO for current epoch (tracks native SOL balance in Well PDA's system account)
/// Incremented on each contribution, decremented when pool bids
/// Reset to 0 when epoch ends
pub total_contributed: u64,
/// Pool bid cost - stores the bid_amount when pool bids
/// Used to calculate original_total when pool gets outbid
/// Reset to 0 when epoch ends
pub pool_bid_cost: u64,
}
impl Well {
pub fn pda(well_id: u64) -> (Pubkey, u8) {
well_pda(well_id)
}
pub fn current_price(&self, auction: &Auction, clock: &Clock) -> u64 {
use crate::consts::AUCTION_FLOOR_PRICE;
// If well has no owner (never been bid on), show starting price
use solana_program::pubkey::Pubkey;
if self.current_bidder == Pubkey::default() {
return self.init_price; // Return starting price for unowned wells
}
let elapsed = clock.unix_timestamp.saturating_sub(self.epoch_start_time as i64);
let duration = auction.auction_duration_seconds as i64;
if elapsed >= duration {
return AUCTION_FLOOR_PRICE; // Auction expired, price is at floor
}
// Linear decay: price = floor + (init_price - floor) * (remaining / duration)
let remaining = duration - elapsed;
let price_range = self.init_price.saturating_sub(AUCTION_FLOOR_PRICE);
let decayed_amount = (price_range as u128 * remaining as u128 / duration as u128) as u64;
AUCTION_FLOOR_PRICE + decayed_amount
}
/// Calculate the effective mining rate at a given point in time based on base rate and halvings
fn calculate_rate_at_time(&self, base_rate: u64, halving_count_at_time: u64) -> u64 {
if halving_count_at_time == 0 {
return base_rate;
}
// Apply first halving (50% reduction)
let mut rate = base_rate / 2;
// Apply subsequent halvings (25% reduction each)
for _ in 1..halving_count_at_time {
rate = (rate * 3) / 4;
}
rate
}
/// Calculate how many halvings had occurred by a given timestamp
///
/// Halving schedule:
/// - First halving: 14 days after initialization (at last_halving_time + FIRST_HALVING_PERIOD_SECONDS when halving_count = 0)
/// - Subsequent halvings: Every 28 days after the first halving
///
/// When halving_count > 0, last_halving_time is when the most recent halving occurred.
fn halving_count_at_time(auction: &Auction, timestamp: u64) -> u64 {
if auction.halving_count == 0 {
// No halvings have occurred yet
// last_halving_time is the initialization time
let first_halving_time = auction.last_halving_time + Auction::FIRST_HALVING_PERIOD_SECONDS;
if timestamp < first_halving_time {
return 0;
}
// First halving should have occurred but hasn't been applied yet
// Return 0 to be safe - it will be applied when someone interacts
return 0;
}
// Calculate when the first halving occurred
// If halving_count = 1: first halving was at last_halving_time
// If halving_count = 2: first halving was 28 days before last_halving_time
// If halving_count = 3: first halving was 56 days before last_halving_time, etc.
let first_halving_time = if auction.halving_count == 1 {
auction.last_halving_time
} else {
// First halving was (halving_count - 1) * halving_period_seconds before the last halving
auction.last_halving_time.saturating_sub(
(auction.halving_count - 1) * auction.halving_period_seconds
)
};
if timestamp < first_halving_time {
return 0;
}
// Calculate how many halvings occurred by this timestamp
// First halving is at first_halving_time, subsequent are every halving_period_seconds
let time_since_first = timestamp.saturating_sub(first_halving_time);
// First halving counts as 1, then add subsequent halvings (every halving_period_seconds)
let halving_count = 1 + (time_since_first / auction.halving_period_seconds);
// Cap at the actual halving_count (can't have more halvings than have occurred)
halving_count.min(auction.halving_count)
}
pub fn update_accumulated_oil(&mut self, auction: &Auction, clock: &Clock) {
// Skip if no owner
use solana_program::pubkey::Pubkey;
if self.current_bidder == Pubkey::default() {
return;
}
let last_update = self.last_update_time as i64;
let current_time = clock.unix_timestamp as u64;
let elapsed = clock.unix_timestamp.saturating_sub(last_update);
if elapsed <= 0 {
return;
}
// Get base rate for this well (we need well_id, but we can derive it from self.well_id)
let base_rate = auction.base_mining_rates[self.well_id as usize];
// Calculate halving counts at start and end of period
let halving_count_at_start = Self::halving_count_at_time(auction, last_update as u64);
let halving_count_at_end = Self::halving_count_at_time(auction, current_time);
// Calculate rate at start of period
let rate_at_start = self.calculate_rate_at_time(base_rate, halving_count_at_start);
// If no halving occurred during this period, use simple calculation
if halving_count_at_start == halving_count_at_end {
let oil_mined = rate_at_start.checked_mul(elapsed as u64).unwrap_or(0);
self.accumulated_oil = self.accumulated_oil
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
self.lifetime_oil_mined = self.lifetime_oil_mined
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
self.operator_total_oil_mined = self.operator_total_oil_mined
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
self.last_update_time = current_time;
return;
}
// Halving(s) occurred during this period - calculate in segments
// Calculate when first halving occurred (needed for segment calculation)
let first_halving_time = if auction.halving_count == 0 {
auction.last_halving_time + Auction::FIRST_HALVING_PERIOD_SECONDS
} else if auction.halving_count == 1 {
auction.last_halving_time
} else {
auction.last_halving_time.saturating_sub(
(auction.halving_count - 1) * auction.halving_period_seconds
)
};
let mut total_oil = 0u64;
let mut segment_start = last_update as u64;
let mut current_halving_count = halving_count_at_start;
while segment_start < current_time && current_halving_count < halving_count_at_end {
// Calculate when the next halving occurred
let next_halving_time = if current_halving_count == 0 {
first_halving_time
} else {
// Subsequent halvings are every halving_period_seconds after the first
first_halving_time + (current_halving_count as u64 * auction.halving_period_seconds)
};
let segment_end = next_halving_time.min(current_time);
let segment_time = segment_end.saturating_sub(segment_start);
let segment_rate = self.calculate_rate_at_time(base_rate, current_halving_count);
total_oil = total_oil.checked_add(
segment_rate.checked_mul(segment_time).unwrap_or(0)
).unwrap_or(u64::MAX);
segment_start = segment_end;
current_halving_count += 1;
}
// Calculate remaining time after all halvings in this period
if segment_start < current_time {
let remaining_time = current_time.saturating_sub(segment_start);
let final_rate = self.calculate_rate_at_time(base_rate, halving_count_at_end);
total_oil = total_oil.checked_add(
final_rate.checked_mul(remaining_time).unwrap_or(0)
).unwrap_or(u64::MAX);
}
let oil_mined = total_oil;
self.accumulated_oil = self.accumulated_oil
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
self.lifetime_oil_mined = self.lifetime_oil_mined
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
// Track total mined by current operator (persists even after claiming)
self.operator_total_oil_mined = self.operator_total_oil_mined
.checked_add(oil_mined)
.unwrap_or(u64::MAX);
self.last_update_time = current_time;
}
/// Apply all halvings that have already occurred (based on auction.halving_count)
/// This is used when resetting well.mps to base rate in a new epoch
///
/// Safety: This function assumes well.mps has just been reset to base rate.
/// It applies all halvings that have occurred according to auction.halving_count.
pub fn apply_existing_halvings(&mut self, auction: &Auction) {
if auction.halving_count == 0 {
// No halvings have occurred yet, keep base rate
self.halving_count = 0;
return;
}
// Apply first halving (50% reduction)
self.mps = self.mps / 2;
// Apply subsequent halvings (25% reduction each)
// auction.halving_count includes the first halving, so subtract 1 for subsequent halvings
for _ in 0..(auction.halving_count - 1) {
self.mps = (self.mps * 3) / 4; // 25% reduction (multiply by 0.75)
}
// Sync well's halving_count to match auction's
self.halving_count = auction.halving_count;
}
pub fn check_and_apply_halving(&mut self, auction: &mut Auction, clock: &Clock) {
// First, sync existing halvings if this well is behind
// This ensures all wells stay in sync with the global halving state
while self.halving_count < auction.halving_count {
if self.halving_count == 0 {
// Apply first halving (50% reduction)
self.mps = self.mps / 2;
self.halving_count = 1;
} else {
// Apply subsequent halvings (25% reduction each)
self.mps = (self.mps * 3) / 4;
self.halving_count += 1;
}
}
// Then check if we should apply NEW halvings based on current time
let current_time = clock.unix_timestamp as u64;
let (halvings_to_apply, is_first_halving) = auction.should_apply_halving(current_time);
if halvings_to_apply > 0 {
if is_first_halving {
// First halving: 50% reduction
self.mps = self.mps / 2; // 50% reduction
self.halving_count += 1;
auction.halving_count += 1;
auction.last_halving_time = current_time;
} else {
// Subsequent halvings: 25% reduction each (multiply by 0.75)
for _ in 0..halvings_to_apply {
self.mps = (self.mps * 3) / 4; // 25% reduction (multiply by 0.75)
self.halving_count += 1;
auction.halving_count += 1;
}
// Update auction last_halving_time to current time
auction.last_halving_time = current_time;
}
}
}
}
account!(OilAccount, Well);