1use serde::{Deserialize, Serialize};
2use steel::*;
3
4use crate::state::{stake_pda, Pool};
5
6use super::OilAccount;
7
8#[repr(C)]
9#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
10pub struct Stake {
11 pub authority: Pubkey,
13
14 pub balance: u64,
16
17 pub lock_duration_days: u64,
19
20 pub lock_ends_at: u64,
22
23 pub buffer_c: u64,
25
26 pub buffer_d: u64,
28
29 pub buffer_e: u64,
31
32 pub last_claim_at: i64,
34
35 pub last_deposit_at: i64,
37
38 pub last_withdraw_at: i64,
40
41 pub rewards_factor: Numeric,
43
44 pub rewards: u64,
46
47 pub lifetime_rewards: u64,
49
50 pub buffer_f: u64,
52}
53
54impl Stake {
55 pub fn pda(&self) -> (Pubkey, u8) {
56 stake_pda(self.authority)
57 }
58
59 pub fn calculate_multiplier(lock_duration_days: u64) -> f64 {
61 if lock_duration_days == 0 {
62 return 1.0;
63 }
64
65 let lookup: [(u64, f64); 6] = [
67 (7, 1.18),
68 (30, 1.78),
69 (90, 3.35),
70 (180, 5.69),
71 (365, 10.5),
72 (730, 20.0),
73 ];
74
75 let days = lock_duration_days.min(730);
77
78 for &(d, m) in &lookup {
80 if days == d {
81 return m;
82 }
83 }
84
85 for i in 0..lookup.len() - 1 {
87 let (d1, m1) = lookup[i];
88 let (d2, m2) = lookup[i + 1];
89
90 if days >= d1 && days <= d2 {
91 let ratio = (days - d1) as f64 / (d2 - d1) as f64;
92 return m1 + (m2 - m1) * ratio;
93 }
94 }
95
96 if days > 730 {
98 return 20.0;
99 }
100
101 if days < 7 {
103 return 1.0 + (days as f64 / 7.0) * 0.18;
104 }
105
106 1.0 }
108
109 pub fn score(&self) -> u64 {
111 let multiplier = Self::calculate_multiplier(self.lock_duration_days);
112 ((self.balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64
114 }
115
116 pub fn is_locked(&self, clock: &Clock) -> bool {
118 self.lock_ends_at > 0 && (clock.unix_timestamp as u64) < self.lock_ends_at
119 }
120
121 pub fn remaining_lock_seconds(&self, clock: &Clock) -> u64 {
123 if self.lock_ends_at == 0 || (clock.unix_timestamp as u64) >= self.lock_ends_at {
124 return 0;
125 }
126 self.lock_ends_at - (clock.unix_timestamp as u64)
127 }
128
129 pub fn calculate_penalty_percent(lock_duration_days: u64) -> u64 {
133 match lock_duration_days {
134 0 => 0, 1..=7 => 5, 8..=30 => 10, 31..=180 => 25, 181..=365 => 40, 366..=730 => 60, _ => 60, }
142 }
143
144 pub fn claim(&mut self, amount: u64, clock: &Clock, pool: &Pool) -> u64 {
145 self.update_rewards(pool);
146 let amount = self.rewards.min(amount);
147 self.rewards -= amount;
148 self.last_claim_at = clock.unix_timestamp;
149 amount
150 }
151
152 pub fn deposit(
153 &mut self,
154 amount: u64,
155 clock: &Clock,
156 pool: &mut Pool,
157 sender: &TokenAccount,
158 ) -> u64 {
159 self.update_rewards(pool);
160
161 let old_score = self.score();
163
164 let amount = sender.amount().min(amount);
165 self.balance += amount;
166 self.last_deposit_at = clock.unix_timestamp;
167
168 let new_score = self.score();
170
171 pool.total_staked += amount;
173 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
174
175 amount
176 }
177
178 pub fn withdraw(&mut self, amount: u64, clock: &Clock, pool: &mut Pool) -> u64 {
179 self.update_rewards(pool);
180
181 let old_score = self.score();
183
184 let amount = self.balance.min(amount);
185 self.balance -= amount;
186 self.last_withdraw_at = clock.unix_timestamp;
187
188 if self.balance == 0 {
191 self.lock_duration_days = 0;
192 self.lock_ends_at = 0;
193 }
194
195 let new_score = self.score();
197
198 pool.total_staked -= amount;
200 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
201
202 amount
203 }
204
205 pub fn update_rewards(&mut self, pool: &Pool) {
206 if pool.stake_rewards_factor > self.rewards_factor {
208 let accumulated_rewards = pool.stake_rewards_factor - self.rewards_factor;
209 if accumulated_rewards < Numeric::ZERO {
210 panic!("Accumulated rewards is negative");
211 }
212 let score = self.score();
214 let personal_rewards = accumulated_rewards * Numeric::from_u64(score);
215 self.rewards += personal_rewards.to_u64();
216 self.lifetime_rewards += personal_rewards.to_u64();
217 }
218
219 self.rewards_factor = pool.stake_rewards_factor;
221 }
222}
223
224account!(OilAccount, Stake);
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_multiplier_exact_lookup_values() {
232 assert_eq!(Stake::calculate_multiplier(7), 1.18);
234 assert_eq!(Stake::calculate_multiplier(30), 1.78);
235 assert_eq!(Stake::calculate_multiplier(90), 3.35);
236 assert_eq!(Stake::calculate_multiplier(180), 5.69);
237 assert_eq!(Stake::calculate_multiplier(365), 10.5);
238 assert_eq!(Stake::calculate_multiplier(730), 20.0);
239 }
240
241 #[test]
242 fn test_multiplier_no_lock() {
243 assert_eq!(Stake::calculate_multiplier(0), 1.0);
245 }
246
247 #[test]
248 fn test_multiplier_below_7_days() {
249 assert_eq!(Stake::calculate_multiplier(1), 1.0 + (1.0 / 7.0) * 0.18);
251 assert_eq!(Stake::calculate_multiplier(3), 1.0 + (3.0 / 7.0) * 0.18);
252 assert_eq!(Stake::calculate_multiplier(6), 1.0 + (6.0 / 7.0) * 0.18);
253
254 assert!(Stake::calculate_multiplier(6) < 1.18);
256 }
257
258 #[test]
259 fn test_multiplier_interpolation_between_lookup_points() {
260 let multiplier_15 = Stake::calculate_multiplier(15);
262 assert!(multiplier_15 > 1.18);
263 assert!(multiplier_15 < 1.78);
264
265 let multiplier_60 = Stake::calculate_multiplier(60);
267 assert!(multiplier_60 > 1.78);
268 assert!(multiplier_60 < 3.35);
269
270 let multiplier_135 = Stake::calculate_multiplier(135);
272 assert!(multiplier_135 > 3.35);
273 assert!(multiplier_135 < 5.69);
274
275 let multiplier_272 = Stake::calculate_multiplier(272);
277 assert!(multiplier_272 > 5.69);
278 assert!(multiplier_272 < 10.5);
279
280 let multiplier_547 = Stake::calculate_multiplier(547);
282 assert!(multiplier_547 > 10.5);
283 assert!(multiplier_547 < 20.0);
284 }
285
286 #[test]
287 fn test_multiplier_above_730_days() {
288 assert_eq!(Stake::calculate_multiplier(730), 20.0);
290 assert_eq!(Stake::calculate_multiplier(1000), 20.0);
291 assert_eq!(Stake::calculate_multiplier(u64::MAX), 20.0);
292 }
293
294 #[test]
295 fn test_score_calculation() {
296 let balance = 1000u64;
298 let multiplier = Stake::calculate_multiplier(0);
299 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
300 assert_eq!(expected_score, 1000);
301
302 let multiplier = Stake::calculate_multiplier(7);
304 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
305 assert_eq!(expected_score, 1180);
306
307 let multiplier = Stake::calculate_multiplier(730);
309 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
310 assert_eq!(expected_score, 20000);
311 }
312
313 #[test]
314 fn test_score_with_large_balance() {
315 let max_balance = 2_100_000u64;
317 let multiplier = Stake::calculate_multiplier(730); let score = ((max_balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
319
320 assert_eq!(score, 42_000_000);
322 assert!(score < u64::MAX);
323 }
324
325 #[test]
326 fn test_multiplier_linear_interpolation_accuracy() {
327 let m7 = Stake::calculate_multiplier(7);
329 let m30 = Stake::calculate_multiplier(30);
330 let m15 = Stake::calculate_multiplier(15);
331
332 let ratio = (15.0 - 7.0) / (30.0 - 7.0);
334 let expected = m7 + ratio * (m30 - m7);
335
336 assert!((m15 - expected).abs() < 0.0001);
338 }
339
340 #[test]
341 fn test_multiplier_monotonic_increase() {
342 let mut prev_multiplier = 0.0;
344 for days in [0, 1, 7, 15, 30, 60, 90, 135, 180, 272, 365, 547, 730, 1000] {
345 let multiplier = Stake::calculate_multiplier(days);
346 assert!(multiplier >= prev_multiplier,
347 "Multiplier should not decrease: {} days = {}, previous = {}",
348 days, multiplier, prev_multiplier);
349 prev_multiplier = multiplier;
350 }
351 }
352
353 #[test]
354 fn test_multiplier_boundaries() {
355 assert_eq!(Stake::calculate_multiplier(0), 1.0);
357 assert!(Stake::calculate_multiplier(1) > 1.0);
358 assert!(Stake::calculate_multiplier(1) < 1.18);
359 assert_eq!(Stake::calculate_multiplier(7), 1.18);
360 assert_eq!(Stake::calculate_multiplier(730), 20.0);
361 assert_eq!(Stake::calculate_multiplier(731), 20.0);
362 }
363}