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 claim(&mut self, amount: u64, clock: &Clock, pool: &Pool) -> u64 {
130 self.update_rewards(pool);
131 let amount = self.rewards.min(amount);
132 self.rewards -= amount;
133 self.last_claim_at = clock.unix_timestamp;
134 amount
135 }
136
137 pub fn deposit(
138 &mut self,
139 amount: u64,
140 clock: &Clock,
141 pool: &mut Pool,
142 sender: &TokenAccount,
143 ) -> u64 {
144 self.update_rewards(pool);
145
146 let old_score = self.score();
148
149 let amount = sender.amount().min(amount);
150 self.balance += amount;
151 self.last_deposit_at = clock.unix_timestamp;
152
153 let new_score = self.score();
155
156 pool.total_staked += amount;
158 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
159
160 amount
161 }
162
163 pub fn withdraw(&mut self, amount: u64, clock: &Clock, pool: &mut Pool) -> u64 {
164 self.update_rewards(pool);
165
166 let old_score = self.score();
168
169 let amount = self.balance.min(amount);
170 self.balance -= amount;
171 self.last_withdraw_at = clock.unix_timestamp;
172
173 let new_score = self.score();
175
176 pool.total_staked -= amount;
178 pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
179
180 amount
181 }
182
183 pub fn update_rewards(&mut self, pool: &Pool) {
184 if pool.stake_rewards_factor > self.rewards_factor {
186 let accumulated_rewards = pool.stake_rewards_factor - self.rewards_factor;
187 if accumulated_rewards < Numeric::ZERO {
188 panic!("Accumulated rewards is negative");
189 }
190 let score = self.score();
192 let personal_rewards = accumulated_rewards * Numeric::from_u64(score);
193 self.rewards += personal_rewards.to_u64();
194 self.lifetime_rewards += personal_rewards.to_u64();
195 }
196
197 self.rewards_factor = pool.stake_rewards_factor;
199 }
200}
201
202account!(OilAccount, Stake);
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_multiplier_exact_lookup_values() {
210 assert_eq!(Stake::calculate_multiplier(7), 1.18);
212 assert_eq!(Stake::calculate_multiplier(30), 1.78);
213 assert_eq!(Stake::calculate_multiplier(90), 3.35);
214 assert_eq!(Stake::calculate_multiplier(180), 5.69);
215 assert_eq!(Stake::calculate_multiplier(365), 10.5);
216 assert_eq!(Stake::calculate_multiplier(730), 20.0);
217 }
218
219 #[test]
220 fn test_multiplier_no_lock() {
221 assert_eq!(Stake::calculate_multiplier(0), 1.0);
223 }
224
225 #[test]
226 fn test_multiplier_below_7_days() {
227 assert_eq!(Stake::calculate_multiplier(1), 1.0 + (1.0 / 7.0) * 0.18);
229 assert_eq!(Stake::calculate_multiplier(3), 1.0 + (3.0 / 7.0) * 0.18);
230 assert_eq!(Stake::calculate_multiplier(6), 1.0 + (6.0 / 7.0) * 0.18);
231
232 assert!(Stake::calculate_multiplier(6) < 1.18);
234 }
235
236 #[test]
237 fn test_multiplier_interpolation_between_lookup_points() {
238 let multiplier_15 = Stake::calculate_multiplier(15);
240 assert!(multiplier_15 > 1.18);
241 assert!(multiplier_15 < 1.78);
242
243 let multiplier_60 = Stake::calculate_multiplier(60);
245 assert!(multiplier_60 > 1.78);
246 assert!(multiplier_60 < 3.35);
247
248 let multiplier_135 = Stake::calculate_multiplier(135);
250 assert!(multiplier_135 > 3.35);
251 assert!(multiplier_135 < 5.69);
252
253 let multiplier_272 = Stake::calculate_multiplier(272);
255 assert!(multiplier_272 > 5.69);
256 assert!(multiplier_272 < 10.5);
257
258 let multiplier_547 = Stake::calculate_multiplier(547);
260 assert!(multiplier_547 > 10.5);
261 assert!(multiplier_547 < 20.0);
262 }
263
264 #[test]
265 fn test_multiplier_above_730_days() {
266 assert_eq!(Stake::calculate_multiplier(730), 20.0);
268 assert_eq!(Stake::calculate_multiplier(1000), 20.0);
269 assert_eq!(Stake::calculate_multiplier(u64::MAX), 20.0);
270 }
271
272 #[test]
273 fn test_score_calculation() {
274 let balance = 1000u64;
276 let multiplier = Stake::calculate_multiplier(0);
277 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
278 assert_eq!(expected_score, 1000);
279
280 let multiplier = Stake::calculate_multiplier(7);
282 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
283 assert_eq!(expected_score, 1180);
284
285 let multiplier = Stake::calculate_multiplier(730);
287 let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
288 assert_eq!(expected_score, 20000);
289 }
290
291 #[test]
292 fn test_score_with_large_balance() {
293 let max_balance = 2_100_000u64;
295 let multiplier = Stake::calculate_multiplier(730); let score = ((max_balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
297
298 assert_eq!(score, 42_000_000);
300 assert!(score < u64::MAX);
301 }
302
303 #[test]
304 fn test_multiplier_linear_interpolation_accuracy() {
305 let m7 = Stake::calculate_multiplier(7);
307 let m30 = Stake::calculate_multiplier(30);
308 let m15 = Stake::calculate_multiplier(15);
309
310 let ratio = (15.0 - 7.0) / (30.0 - 7.0);
312 let expected = m7 + ratio * (m30 - m7);
313
314 assert!((m15 - expected).abs() < 0.0001);
316 }
317
318 #[test]
319 fn test_multiplier_monotonic_increase() {
320 let mut prev_multiplier = 0.0;
322 for days in [0, 1, 7, 15, 30, 60, 90, 135, 180, 272, 365, 547, 730, 1000] {
323 let multiplier = Stake::calculate_multiplier(days);
324 assert!(multiplier >= prev_multiplier,
325 "Multiplier should not decrease: {} days = {}, previous = {}",
326 days, multiplier, prev_multiplier);
327 prev_multiplier = multiplier;
328 }
329 }
330
331 #[test]
332 fn test_multiplier_boundaries() {
333 assert_eq!(Stake::calculate_multiplier(0), 1.0);
335 assert!(Stake::calculate_multiplier(1) > 1.0);
336 assert!(Stake::calculate_multiplier(1) < 1.18);
337 assert_eq!(Stake::calculate_multiplier(7), 1.18);
338 assert_eq!(Stake::calculate_multiplier(730), 20.0);
339 assert_eq!(Stake::calculate_multiplier(731), 20.0);
340 }
341}