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 {
62 if lock_duration_days == 0 {
63 return 1.0;
64 }
65
66 let lookup: [(u64, f64); 6] = [
68 (7, 1.18),
69 (30, 1.78),
70 (90, 3.35),
71 (180, 5.69),
72 (365, 10.5),
73 (730, 20.0),
74 ];
75
76 let days = lock_duration_days.min(730);
78
79 for &(d, m) in &lookup {
81 if days == d {
82 return m;
83 }
84 }
85
86 for i in 0..lookup.len() - 1 {
88 let (d1, m1) = lookup[i];
89 let (d2, m2) = lookup[i + 1];
90
91 if days >= d1 && days <= d2 {
92 let ratio = (days - d1) as f64 / (d2 - d1) as f64;
93 return m1 + (m2 - m1) * ratio;
94 }
95 }
96
97 if days > 730 {
99 return 20.0;
100 }
101
102 if days < 7 {
104 return 1.0 + (days as f64 / 7.0) * 0.18;
105 }
106
107 1.0 }
109
110 pub fn score(&self) -> u64 {
112 let multiplier = Self::calculate_multiplier(self.lock_duration_days);
113 ((self.balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64
116 }
117
118 pub fn is_locked(&self, clock: &Clock) -> bool {
120 self.lock_ends_at > 0 && (clock.unix_timestamp as u64) < self.lock_ends_at
121 }
122
123 pub fn remaining_lock_seconds(&self, clock: &Clock) -> u64 {
125 if self.lock_ends_at == 0 || (clock.unix_timestamp as u64) >= self.lock_ends_at {
126 return 0;
127 }
128 self.lock_ends_at - (clock.unix_timestamp as u64)
129 }
130
131 pub fn claim(&mut self, amount: u64, clock: &Clock, pool: &Pool) -> u64 {
132 self.update_rewards(pool);
133 let amount = self.rewards.min(amount);
134 self.rewards -= amount;
135 self.last_claim_at = clock.unix_timestamp;
136 amount
137 }
138
139 pub fn deposit(
140 &mut self,
141 amount: u64,
142 clock: &Clock,
143 pool: &mut Pool,
144 sender: &TokenAccount,
145 ) -> u64 {
146 self.update_rewards(pool);
147
148 let old_score = self.score();
150
151 let amount = sender.amount().min(amount);
152 self.balance += amount;
153 self.last_deposit_at = clock.unix_timestamp;
154
155 let new_score = self.score();
157
158 pool.total_staked += amount;
160 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
161
162 amount
163 }
164
165 pub fn withdraw(&mut self, amount: u64, clock: &Clock, pool: &mut Pool) -> u64 {
166 self.update_rewards(pool);
167
168 let old_score = self.score();
170
171 let amount = self.balance.min(amount);
172 self.balance -= amount;
173 self.last_withdraw_at = clock.unix_timestamp;
174
175 let new_score = self.score();
177
178 pool.total_staked -= amount;
180 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
181
182 amount
183 }
184
185 pub fn update_rewards(&mut self, pool: &Pool) {
186 if pool.stake_rewards_factor > self.rewards_factor {
188 let accumulated_rewards = pool.stake_rewards_factor - self.rewards_factor;
189 if accumulated_rewards < Numeric::ZERO {
190 panic!("Accumulated rewards is negative");
191 }
192 let score = self.score();
194 let personal_rewards = accumulated_rewards * Numeric::from_u64(score);
195 self.rewards += personal_rewards.to_u64();
196 self.lifetime_rewards += personal_rewards.to_u64();
197 }
198
199 self.rewards_factor = pool.stake_rewards_factor;
201 }
202}
203
204account!(OilAccount, Stake);
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_multiplier_exact_lookup_values() {
212 assert_eq!(Stake::calculate_multiplier(7), 1.18);
214 assert_eq!(Stake::calculate_multiplier(30), 1.78);
215 assert_eq!(Stake::calculate_multiplier(90), 3.35);
216 assert_eq!(Stake::calculate_multiplier(180), 5.69);
217 assert_eq!(Stake::calculate_multiplier(365), 10.5);
218 assert_eq!(Stake::calculate_multiplier(730), 20.0);
219 }
220
221 #[test]
222 fn test_multiplier_no_lock() {
223 assert_eq!(Stake::calculate_multiplier(0), 1.0);
225 }
226
227 #[test]
228 fn test_multiplier_below_7_days() {
229 assert_eq!(Stake::calculate_multiplier(1), 1.0 + (1.0 / 7.0) * 0.18);
231 assert_eq!(Stake::calculate_multiplier(3), 1.0 + (3.0 / 7.0) * 0.18);
232 assert_eq!(Stake::calculate_multiplier(6), 1.0 + (6.0 / 7.0) * 0.18);
233
234 assert!(Stake::calculate_multiplier(6) < 1.18);
236 }
237
238 #[test]
239 fn test_multiplier_interpolation_between_lookup_points() {
240 let multiplier_15 = Stake::calculate_multiplier(15);
242 assert!(multiplier_15 > 1.18);
243 assert!(multiplier_15 < 1.78);
244
245 let multiplier_60 = Stake::calculate_multiplier(60);
247 assert!(multiplier_60 > 1.78);
248 assert!(multiplier_60 < 3.35);
249
250 let multiplier_135 = Stake::calculate_multiplier(135);
252 assert!(multiplier_135 > 3.35);
253 assert!(multiplier_135 < 5.69);
254
255 let multiplier_272 = Stake::calculate_multiplier(272);
257 assert!(multiplier_272 > 5.69);
258 assert!(multiplier_272 < 10.5);
259
260 let multiplier_547 = Stake::calculate_multiplier(547);
262 assert!(multiplier_547 > 10.5);
263 assert!(multiplier_547 < 20.0);
264 }
265
266 #[test]
267 fn test_multiplier_above_730_days() {
268 assert_eq!(Stake::calculate_multiplier(730), 20.0);
270 assert_eq!(Stake::calculate_multiplier(1000), 20.0);
271 assert_eq!(Stake::calculate_multiplier(u64::MAX), 20.0);
272 }
273
274 #[test]
275 fn test_score_calculation() {
276 let balance = 1000u64;
282 let multiplier = Stake::calculate_multiplier(0);
283 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
284 assert_eq!(expected_score, 1000);
285
286 let multiplier = Stake::calculate_multiplier(7);
288 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
289 assert_eq!(expected_score, 1180);
290
291 let multiplier = Stake::calculate_multiplier(730);
293 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
294 assert_eq!(expected_score, 20000);
295 }
296
297 #[test]
298 fn test_score_with_large_balance() {
299 let max_balance = 2_100_000u64;
301 let multiplier = Stake::calculate_multiplier(730); let score = ((max_balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
303
304 assert_eq!(score, 42_000_000);
306 assert!(score < u64::MAX);
307 }
308
309 #[test]
310 fn test_multiplier_linear_interpolation_accuracy() {
311 let m7 = Stake::calculate_multiplier(7);
314 let m30 = Stake::calculate_multiplier(30);
315 let m15 = Stake::calculate_multiplier(15);
316
317 let ratio = (15.0 - 7.0) / (30.0 - 7.0);
319 let expected = m7 + ratio * (m30 - m7);
320
321 assert!((m15 - expected).abs() < 0.0001);
323 }
324
325 #[test]
326 fn test_multiplier_monotonic_increase() {
327 let mut prev_multiplier = 0.0;
329 for days in [0, 1, 7, 15, 30, 60, 90, 135, 180, 272, 365, 547, 730, 1000] {
330 let multiplier = Stake::calculate_multiplier(days);
331 assert!(multiplier >= prev_multiplier,
332 "Multiplier should not decrease: {} days = {}, previous = {}",
333 days, multiplier, prev_multiplier);
334 prev_multiplier = multiplier;
335 }
336 }
337
338 #[test]
339 fn test_multiplier_boundaries() {
340 assert_eq!(Stake::calculate_multiplier(0), 1.0);
342 assert!(Stake::calculate_multiplier(1) > 1.0);
343 assert!(Stake::calculate_multiplier(1) < 1.18);
344 assert_eq!(Stake::calculate_multiplier(7), 1.18);
345 assert_eq!(Stake::calculate_multiplier(730), 20.0);
346 assert_eq!(Stake::calculate_multiplier(731), 20.0);
347 }
348}