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 let new_score = self.score();
190
191 pool.total_staked -= amount;
193 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
194
195 amount
196 }
197
198 pub fn update_rewards(&mut self, pool: &Pool) {
199 if pool.stake_rewards_factor > self.rewards_factor {
201 let accumulated_rewards = pool.stake_rewards_factor - self.rewards_factor;
202 if accumulated_rewards < Numeric::ZERO {
203 panic!("Accumulated rewards is negative");
204 }
205 let score = self.score();
207 let personal_rewards = accumulated_rewards * Numeric::from_u64(score);
208 self.rewards += personal_rewards.to_u64();
209 self.lifetime_rewards += personal_rewards.to_u64();
210 }
211
212 self.rewards_factor = pool.stake_rewards_factor;
214 }
215}
216
217account!(OilAccount, Stake);
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_multiplier_exact_lookup_values() {
225 assert_eq!(Stake::calculate_multiplier(7), 1.18);
227 assert_eq!(Stake::calculate_multiplier(30), 1.78);
228 assert_eq!(Stake::calculate_multiplier(90), 3.35);
229 assert_eq!(Stake::calculate_multiplier(180), 5.69);
230 assert_eq!(Stake::calculate_multiplier(365), 10.5);
231 assert_eq!(Stake::calculate_multiplier(730), 20.0);
232 }
233
234 #[test]
235 fn test_multiplier_no_lock() {
236 assert_eq!(Stake::calculate_multiplier(0), 1.0);
238 }
239
240 #[test]
241 fn test_multiplier_below_7_days() {
242 assert_eq!(Stake::calculate_multiplier(1), 1.0 + (1.0 / 7.0) * 0.18);
244 assert_eq!(Stake::calculate_multiplier(3), 1.0 + (3.0 / 7.0) * 0.18);
245 assert_eq!(Stake::calculate_multiplier(6), 1.0 + (6.0 / 7.0) * 0.18);
246
247 assert!(Stake::calculate_multiplier(6) < 1.18);
249 }
250
251 #[test]
252 fn test_multiplier_interpolation_between_lookup_points() {
253 let multiplier_15 = Stake::calculate_multiplier(15);
255 assert!(multiplier_15 > 1.18);
256 assert!(multiplier_15 < 1.78);
257
258 let multiplier_60 = Stake::calculate_multiplier(60);
260 assert!(multiplier_60 > 1.78);
261 assert!(multiplier_60 < 3.35);
262
263 let multiplier_135 = Stake::calculate_multiplier(135);
265 assert!(multiplier_135 > 3.35);
266 assert!(multiplier_135 < 5.69);
267
268 let multiplier_272 = Stake::calculate_multiplier(272);
270 assert!(multiplier_272 > 5.69);
271 assert!(multiplier_272 < 10.5);
272
273 let multiplier_547 = Stake::calculate_multiplier(547);
275 assert!(multiplier_547 > 10.5);
276 assert!(multiplier_547 < 20.0);
277 }
278
279 #[test]
280 fn test_multiplier_above_730_days() {
281 assert_eq!(Stake::calculate_multiplier(730), 20.0);
283 assert_eq!(Stake::calculate_multiplier(1000), 20.0);
284 assert_eq!(Stake::calculate_multiplier(u64::MAX), 20.0);
285 }
286
287 #[test]
288 fn test_score_calculation() {
289 let balance = 1000u64;
291 let multiplier = Stake::calculate_multiplier(0);
292 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
293 assert_eq!(expected_score, 1000);
294
295 let multiplier = Stake::calculate_multiplier(7);
297 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
298 assert_eq!(expected_score, 1180);
299
300 let multiplier = Stake::calculate_multiplier(730);
302 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
303 assert_eq!(expected_score, 20000);
304 }
305
306 #[test]
307 fn test_score_with_large_balance() {
308 let max_balance = 2_100_000u64;
310 let multiplier = Stake::calculate_multiplier(730); let score = ((max_balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
312
313 assert_eq!(score, 42_000_000);
315 assert!(score < u64::MAX);
316 }
317
318 #[test]
319 fn test_multiplier_linear_interpolation_accuracy() {
320 let m7 = Stake::calculate_multiplier(7);
322 let m30 = Stake::calculate_multiplier(30);
323 let m15 = Stake::calculate_multiplier(15);
324
325 let ratio = (15.0 - 7.0) / (30.0 - 7.0);
327 let expected = m7 + ratio * (m30 - m7);
328
329 assert!((m15 - expected).abs() < 0.0001);
331 }
332
333 #[test]
334 fn test_multiplier_monotonic_increase() {
335 let mut prev_multiplier = 0.0;
337 for days in [0, 1, 7, 15, 30, 60, 90, 135, 180, 272, 365, 547, 730, 1000] {
338 let multiplier = Stake::calculate_multiplier(days);
339 assert!(multiplier >= prev_multiplier,
340 "Multiplier should not decrease: {} days = {}, previous = {}",
341 days, multiplier, prev_multiplier);
342 prev_multiplier = multiplier;
343 }
344 }
345
346 #[test]
347 fn test_multiplier_boundaries() {
348 assert_eq!(Stake::calculate_multiplier(0), 1.0);
350 assert!(Stake::calculate_multiplier(1) > 1.0);
351 assert!(Stake::calculate_multiplier(1) < 1.18);
352 assert_eq!(Stake::calculate_multiplier(7), 1.18);
353 assert_eq!(Stake::calculate_multiplier(730), 20.0);
354 assert_eq!(Stake::calculate_multiplier(731), 20.0);
355 }
356}