1#![allow(unexpected_cfgs)]
2#[cfg(not(target_os = "solana"))]
8pub mod agent;
9pub mod percolator;
10pub mod scenario;
11
12pub use percolator::{
13 Account, CrankOutcome, InsuranceFund, LiquidationPolicy, RiskEngine, RiskError, RiskParams,
14 SideMode, ADL_ONE, I128, MAX_ACCOUNTS, MAX_ORACLE_PRICE, MAX_VAULT_TVL, POS_SCALE, U128,
15};
16
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19
20pub struct NamedEngine {
25 pub engine: RiskEngine,
26 pub accounts: BTreeMap<String, u16>,
27 pub current_slot: u64,
28 pub last_oracle_price: u64,
29 pub last_funding_rate: i64,
30 next_idx: u16,
31}
32
33impl NamedEngine {
34 pub fn new(params: RiskParams, init_slot: u64, init_oracle_price: u64) -> Self {
36 let engine = RiskEngine::new_with_market(params, init_slot, init_oracle_price);
37 Self {
38 engine,
39 accounts: BTreeMap::new(),
40 current_slot: init_slot,
41 last_oracle_price: init_oracle_price,
42 last_funding_rate: 0,
43 next_idx: 0,
44 }
45 }
46
47 pub fn resolve(&self, name: &str) -> Result<u16, EngineError> {
49 self.accounts
50 .get(name)
51 .copied()
52 .ok_or_else(|| EngineError::UnknownAccount(name.to_string()))
53 }
54
55 fn ensure_account(&mut self, name: &str) -> Result<u16, EngineError> {
56 if let Some(&idx) = self.accounts.get(name) {
57 return Ok(idx);
58 }
59 if name.is_empty() || name.len() > 64 {
60 return Err(EngineError::InvalidAccountName(name.to_string()));
61 }
62 let limit = self.engine.params.max_accounts as usize;
63 if (self.next_idx as usize) >= limit {
64 return Err(EngineError::AccountLimitReached(limit));
65 }
66 let idx = self.next_idx;
67 self.next_idx += 1;
68 self.accounts.insert(name.to_string(), idx);
69 Ok(idx)
70 }
71
72 pub fn deposit(&mut self, name: &str, amount: u128) -> Result<(), EngineError> {
74 if amount == 0 {
75 return Err(EngineError::InvalidAmount);
76 }
77 let idx = self.ensure_account(name)?;
78 self.engine
79 .deposit(idx, amount, self.last_oracle_price, self.current_slot)
80 .map_err(EngineError::Risk)
81 }
82
83 pub fn withdraw(&mut self, name: &str, amount: u128) -> Result<(), EngineError> {
85 if amount == 0 {
86 return Err(EngineError::InvalidAmount);
87 }
88 let idx = self.resolve(name)?;
89 self.engine
90 .withdraw(
91 idx,
92 amount,
93 self.last_oracle_price,
94 self.current_slot,
95 self.last_funding_rate,
96 )
97 .map_err(EngineError::Risk)
98 }
99
100 pub fn trade(
102 &mut self,
103 long: &str,
104 short: &str,
105 size_q: i128,
106 exec_price: u64,
107 ) -> Result<(), EngineError> {
108 let a = self.resolve(long)?;
109 let b = self.resolve(short)?;
110 self.engine
111 .execute_trade(
112 a,
113 b,
114 self.last_oracle_price,
115 self.current_slot,
116 size_q,
117 exec_price,
118 self.last_funding_rate,
119 )
120 .map_err(EngineError::Risk)
121 }
122
123 pub fn crank(&mut self, oracle_price: u64, slot: u64) -> Result<CrankOutcome, EngineError> {
125 self.last_oracle_price = oracle_price;
126 self.current_slot = slot;
127 self.engine
128 .keeper_crank(slot, oracle_price, &[], 0, self.last_funding_rate)
129 .map_err(EngineError::Risk)
130 }
131
132 pub fn liquidate(&mut self, name: &str) -> Result<bool, EngineError> {
134 let idx = self.resolve(name)?;
135 self.engine
136 .liquidate_at_oracle(
137 idx,
138 self.current_slot,
139 self.last_oracle_price,
140 LiquidationPolicy::FullClose,
141 self.last_funding_rate,
142 )
143 .map_err(EngineError::Risk)
144 }
145
146 pub fn settle(&mut self, name: &str) -> Result<(), EngineError> {
148 let idx = self.resolve(name)?;
149 self.engine
150 .settle_account(
151 idx,
152 self.last_oracle_price,
153 self.current_slot,
154 self.last_funding_rate,
155 )
156 .map_err(EngineError::Risk)
157 }
158
159 pub fn set_oracle(&mut self, oracle_price: u64) -> Result<(), EngineError> {
160 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
161 return Err(EngineError::InvalidOraclePrice(oracle_price));
162 }
163 self.last_oracle_price = oracle_price;
164 Ok(())
165 }
166
167 pub fn set_slot(&mut self, slot: u64) {
168 self.current_slot = slot;
169 }
170
171 pub fn set_funding_rate(&mut self, rate: i64) {
172 self.last_funding_rate = rate;
173 }
174
175 pub fn snapshot(&self) -> EngineSnapshot {
177 let (h_num, h_den) = self.engine.haircut_ratio();
178 let conservation = self.engine.check_conservation();
179
180 let mut account_snaps = Vec::new();
181 for (name, &idx) in &self.accounts {
182 if self.engine.is_used(idx as usize) {
183 let acct = &self.engine.accounts[idx as usize];
184 let eff_pos = self.engine.effective_pos_q(idx as usize);
185 let eff_pnl = self.engine.effective_matured_pnl(idx as usize);
186 let equity_maint = self.engine.account_equity_maint_raw(acct);
187 let equity_init = self.engine.account_equity_init_raw(acct, idx as usize);
188 let above_mm = self.engine.is_above_maintenance_margin(
189 acct,
190 idx as usize,
191 self.last_oracle_price,
192 );
193 let above_im =
194 self.engine
195 .is_above_initial_margin(acct, idx as usize, self.last_oracle_price);
196 let notional = self.engine.notional(idx as usize, self.last_oracle_price);
197 account_snaps.push(AccountSnapshot {
198 name: name.clone(),
199 index: idx,
200 capital: acct.capital.get(),
201 pnl: acct.pnl,
202 reserved_pnl: acct.reserved_pnl,
203 effective_position_q: eff_pos,
204 effective_matured_pnl: eff_pnl,
205 equity_maint,
206 equity_init,
207 above_maintenance_margin: above_mm,
208 above_initial_margin: above_im,
209 notional,
210 fee_credits: acct.fee_credits.get(),
211 });
212 }
213 }
214
215 EngineSnapshot {
216 vault: self.engine.vault.get(),
217 insurance_fund: self.engine.insurance_fund.balance.get(),
218 haircut_numerator: h_num,
219 haircut_denominator: h_den,
220 conservation,
221 current_slot: self.current_slot,
222 last_oracle_price: self.last_oracle_price,
223 last_funding_rate: self.last_funding_rate,
224 side_mode_long: format!("{:?}", SideMode::from_u8(self.engine.side_mode_long)),
225 side_mode_short: format!("{:?}", SideMode::from_u8(self.engine.side_mode_short)),
226 oi_long_q: self.engine.oi_eff_long_q,
227 oi_short_q: self.engine.oi_eff_short_q,
228 lifetime_liquidations: self.engine.lifetime_liquidations,
229 accounts: account_snaps,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct EngineSnapshot {
237 pub vault: u128,
238 pub insurance_fund: u128,
239 pub haircut_numerator: u128,
240 pub haircut_denominator: u128,
241 pub conservation: bool,
242 pub current_slot: u64,
243 pub last_oracle_price: u64,
244 pub last_funding_rate: i64,
245 pub side_mode_long: String,
246 pub side_mode_short: String,
247 pub oi_long_q: u128,
248 pub oi_short_q: u128,
249 pub lifetime_liquidations: u64,
250 pub accounts: Vec<AccountSnapshot>,
251}
252
253impl EngineSnapshot {
254 pub fn haircut_ratio_f64(&self) -> f64 {
255 if self.haircut_denominator == 0 {
256 1.0
257 } else {
258 self.haircut_numerator as f64 / self.haircut_denominator as f64
259 }
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AccountSnapshot {
266 pub name: String,
267 pub index: u16,
268 pub capital: u128,
269 pub pnl: i128,
270 pub reserved_pnl: u128,
271 pub effective_position_q: i128,
272 pub effective_matured_pnl: u128,
273 pub equity_maint: i128,
274 pub equity_init: i128,
275 pub above_maintenance_margin: bool,
276 pub above_initial_margin: bool,
277 pub notional: u128,
278 pub fee_credits: i128,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ParamsConfig {
287 #[serde(default = "default_warmup")]
288 pub warmup_period_slots: u64,
289 #[serde(default = "default_mm_bps")]
290 pub maintenance_margin_bps: u64,
291 #[serde(default = "default_im_bps")]
292 pub initial_margin_bps: u64,
293 #[serde(default)]
294 pub trading_fee_bps: u64,
295 #[serde(default = "default_max_accounts")]
296 pub max_accounts: u64,
297 #[serde(default)]
298 pub new_account_fee: u64,
299 #[serde(default)]
300 pub maintenance_fee_per_slot: u64,
301 #[serde(default = "default_crank_staleness")]
302 pub max_crank_staleness_slots: u64,
303 #[serde(default)]
304 pub liquidation_fee_bps: u64,
305 #[serde(default)]
306 pub liquidation_fee_cap: u64,
307 #[serde(default = "default_liq_buffer")]
308 pub liquidation_buffer_bps: u64,
309 #[serde(default)]
310 pub min_liquidation_abs: u64,
311 #[serde(default = "default_min_deposit")]
312 pub min_initial_deposit: u64,
313 #[serde(default = "default_mm_req")]
314 pub min_nonzero_mm_req: u64,
315 #[serde(default = "default_im_req")]
316 pub min_nonzero_im_req: u64,
317 #[serde(default)]
318 pub insurance_floor: u64,
319}
320
321fn default_warmup() -> u64 {
322 100
323}
324fn default_mm_bps() -> u64 {
325 500
326}
327fn default_im_bps() -> u64 {
328 1000
329}
330fn default_max_accounts() -> u64 {
331 64
332}
333fn default_crank_staleness() -> u64 {
334 u64::MAX
335}
336fn default_mm_req() -> u64 {
337 1
338}
339fn default_im_req() -> u64 {
340 2
341}
342fn default_min_deposit() -> u64 {
343 2
344}
345fn default_liq_buffer() -> u64 {
346 50
347}
348
349impl ParamsConfig {
350 pub fn to_risk_params(&self) -> RiskParams {
351 RiskParams {
352 warmup_period_slots: self.warmup_period_slots,
353 maintenance_margin_bps: self.maintenance_margin_bps,
354 initial_margin_bps: self.initial_margin_bps,
355 trading_fee_bps: self.trading_fee_bps,
356 max_accounts: self.max_accounts,
357 new_account_fee: U128::new(self.new_account_fee as u128),
358 maintenance_fee_per_slot: U128::new(self.maintenance_fee_per_slot as u128),
359 max_crank_staleness_slots: self.max_crank_staleness_slots,
360 liquidation_fee_bps: self.liquidation_fee_bps,
361 liquidation_fee_cap: U128::new(self.liquidation_fee_cap as u128),
362 liquidation_buffer_bps: self.liquidation_buffer_bps,
363 min_liquidation_abs: U128::new(self.min_liquidation_abs as u128),
364 min_initial_deposit: U128::new(self.min_initial_deposit as u128),
365 min_nonzero_mm_req: self.min_nonzero_mm_req as u128,
366 min_nonzero_im_req: self.min_nonzero_im_req as u128,
367 insurance_floor: U128::new(self.insurance_floor as u128),
368 }
369 }
370}
371
372impl Default for ParamsConfig {
373 fn default() -> Self {
374 Self {
375 warmup_period_slots: default_warmup(),
376 maintenance_margin_bps: default_mm_bps(),
377 initial_margin_bps: default_im_bps(),
378 trading_fee_bps: 0,
379 max_accounts: default_max_accounts(),
380 new_account_fee: 0,
381 maintenance_fee_per_slot: 0,
382 max_crank_staleness_slots: default_crank_staleness(),
383 liquidation_fee_bps: 0,
384 liquidation_fee_cap: 0,
385 liquidation_buffer_bps: default_liq_buffer(),
386 min_liquidation_abs: 0,
387 min_initial_deposit: default_min_deposit(),
388 min_nonzero_mm_req: default_mm_req(),
389 min_nonzero_im_req: default_im_req(),
390 insurance_floor: 0,
391 }
392 }
393}
394
395#[derive(Debug)]
397pub enum EngineError {
398 UnknownAccount(String),
399 AccountLimitReached(usize),
400 InvalidAccountName(String),
401 InvalidAmount,
402 InvalidOraclePrice(u64),
403 Risk(RiskError),
404}
405
406impl std::fmt::Display for EngineError {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 match self {
409 Self::UnknownAccount(name) => write!(f, "unknown account: {name}"),
410 Self::AccountLimitReached(max) => write!(f, "account limit reached ({max})"),
411 Self::InvalidAccountName(name) => write!(f, "invalid account name: {name:?}"),
412 Self::InvalidAmount => write!(f, "amount must be non-zero"),
413 Self::InvalidOraclePrice(p) => write!(f, "invalid oracle price: {p}"),
414 Self::Risk(e) => write!(f, "risk engine error: {e:?}"),
415 }
416 }
417}
418
419impl std::error::Error for EngineError {}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 fn default_engine() -> NamedEngine {
426 let params = ParamsConfig::default().to_risk_params();
427 NamedEngine::new(params, 0, 1000)
428 }
429
430 #[test]
431 fn deposit_withdraw_roundtrip() {
432 let mut eng = default_engine();
433 eng.deposit("alice", 100_000).unwrap();
434 let snap = eng.snapshot();
435 assert_eq!(snap.vault, 100_000);
436 assert_eq!(snap.accounts[0].capital, 100_000);
437
438 eng.withdraw("alice", 50_000).unwrap();
439 let snap = eng.snapshot();
440 assert_eq!(snap.accounts[0].capital, 50_000);
441 }
442
443 #[test]
444 fn account_overflow_rejected() {
445 let params = ParamsConfig {
446 max_accounts: 2,
447 ..Default::default()
448 };
449 let risk = params.to_risk_params();
450 let mut eng = NamedEngine::new(risk, 0, 1000);
451
452 eng.deposit("a", 100).unwrap();
453 eng.deposit("b", 100).unwrap();
454
455 let err = eng.deposit("c", 100).unwrap_err();
456 assert!(matches!(err, EngineError::AccountLimitReached(_)));
457 }
458
459 #[test]
460 fn name_resolution() {
461 let mut eng = default_engine();
462 assert!(eng.resolve("alice").is_err());
463
464 eng.deposit("alice", 1000).unwrap();
465 assert_eq!(eng.resolve("alice").unwrap(), 0);
466 }
467
468 #[test]
469 fn zero_amount_rejected() {
470 let mut eng = default_engine();
471 let err = eng.deposit("alice", 0).unwrap_err();
472 assert!(matches!(err, EngineError::InvalidAmount));
473 }
474
475 #[test]
476 fn trade_basic() {
477 let mut eng = default_engine();
478 eng.deposit("alice", 100_000).unwrap();
479 eng.deposit("bob", 100_000).unwrap();
480 eng.crank(1000, 1).unwrap();
481 eng.trade("alice", "bob", 10 * POS_SCALE as i128, 1000)
482 .unwrap();
483
484 let snap = eng.snapshot();
485 assert_eq!(snap.vault, 200_000);
486 assert!(snap.conservation);
487 }
488
489 #[test]
490 fn snapshot_serialization_roundtrip() {
491 let mut eng = default_engine();
492 eng.deposit("alice", 50_000).unwrap();
493 let snap = eng.snapshot();
494
495 let json = serde_json::to_string(&snap).unwrap();
496 let deser: EngineSnapshot = serde_json::from_str(&json).unwrap();
497 assert_eq!(deser.vault, snap.vault);
498 assert_eq!(deser.accounts.len(), 1);
499 assert_eq!(deser.accounts[0].name, "alice");
500 }
501
502 #[test]
503 fn empty_name_rejected() {
504 let mut eng = default_engine();
505 let err = eng.deposit("", 100).unwrap_err();
506 assert!(matches!(err, EngineError::InvalidAccountName(_)));
507 }
508
509 #[test]
510 fn duplicate_account_idempotent() {
511 let mut eng = default_engine();
512 eng.deposit("alice", 1000).unwrap();
513 eng.deposit("alice", 2000).unwrap();
514 assert_eq!(eng.resolve("alice").unwrap(), 0);
515 let snap = eng.snapshot();
516 assert_eq!(snap.accounts.len(), 1);
517 assert_eq!(snap.accounts[0].capital, 3000);
518 }
519
520 #[test]
521 fn invalid_oracle_price_rejected() {
522 let mut eng = default_engine();
523 assert!(eng.set_oracle(0).is_err());
524 }
525
526 #[test]
527 fn oracle_price_above_max_rejected() {
528 let mut eng = default_engine();
529 assert!(eng.set_oracle(MAX_ORACLE_PRICE + 1).is_err());
530 assert!(eng.set_oracle(MAX_ORACLE_PRICE).is_ok());
531 }
532
533 #[test]
534 fn withdraw_unknown_account_rejected() {
535 let mut eng = default_engine();
536 let err = eng.withdraw("ghost", 100).unwrap_err();
537 assert!(matches!(err, EngineError::UnknownAccount(_)));
538 }
539
540 #[test]
541 fn withdraw_zero_rejected() {
542 let mut eng = default_engine();
543 eng.deposit("alice", 1000).unwrap();
544 let err = eng.withdraw("alice", 0).unwrap_err();
545 assert!(matches!(err, EngineError::InvalidAmount));
546 }
547
548 #[test]
549 fn long_account_name_rejected() {
550 let mut eng = default_engine();
551 let name = "a".repeat(65);
552 let err = eng.deposit(&name, 100).unwrap_err();
553 assert!(matches!(err, EngineError::InvalidAccountName(_)));
554 }
555
556 #[test]
557 fn max_length_account_name_accepted() {
558 let mut eng = default_engine();
559 let name = "a".repeat(64);
560 eng.deposit(&name, 100).unwrap();
561 assert_eq!(eng.resolve(&name).unwrap(), 0);
562 }
563
564 #[test]
565 fn trade_unknown_account_rejected() {
566 let mut eng = default_engine();
567 eng.deposit("alice", 100_000).unwrap();
568 let err = eng
569 .trade("alice", "ghost", 10 * POS_SCALE as i128, 1000)
570 .unwrap_err();
571 assert!(matches!(err, EngineError::UnknownAccount(_)));
572 }
573
574 #[test]
575 fn liquidate_unknown_account_rejected() {
576 let mut eng = default_engine();
577 let err = eng.liquidate("ghost").unwrap_err();
578 assert!(matches!(err, EngineError::UnknownAccount(_)));
579 }
580
581 #[test]
582 fn settle_unknown_account_rejected() {
583 let mut eng = default_engine();
584 let err = eng.settle("ghost").unwrap_err();
585 assert!(matches!(err, EngineError::UnknownAccount(_)));
586 }
587
588 #[test]
589 fn set_slot_and_funding_rate() {
590 let mut eng = default_engine();
591 eng.set_slot(42);
592 assert_eq!(eng.current_slot, 42);
593 eng.set_funding_rate(-100);
594 assert_eq!(eng.last_funding_rate, -100);
595 }
596
597 #[test]
598 fn crank_updates_oracle_and_slot() {
599 let mut eng = default_engine();
600 eng.deposit("alice", 100_000).unwrap();
601 eng.crank(2000, 50).unwrap();
602 assert_eq!(eng.last_oracle_price, 2000);
603 assert_eq!(eng.current_slot, 50);
604 }
605
606 #[test]
607 fn snapshot_empty_engine() {
608 let eng = default_engine();
609 let snap = eng.snapshot();
610 assert_eq!(snap.vault, 0);
611 assert_eq!(snap.accounts.len(), 0);
612 assert!(snap.conservation);
613 }
614
615 #[test]
616 fn error_display() {
617 let e = EngineError::UnknownAccount("bob".to_string());
618 assert_eq!(e.to_string(), "unknown account: bob");
619
620 let e = EngineError::AccountLimitReached(64);
621 assert_eq!(e.to_string(), "account limit reached (64)");
622
623 let e = EngineError::InvalidAccountName("".to_string());
624 assert_eq!(e.to_string(), "invalid account name: \"\"");
625
626 let e = EngineError::InvalidAmount;
627 assert_eq!(e.to_string(), "amount must be non-zero");
628
629 let e = EngineError::InvalidOraclePrice(0);
630 assert_eq!(e.to_string(), "invalid oracle price: 0");
631 }
632
633 #[test]
634 fn params_config_default_roundtrip() {
635 let params = ParamsConfig::default();
636 let json = serde_json::to_string(¶ms).unwrap();
637 let deser: ParamsConfig = serde_json::from_str(&json).unwrap();
638 assert_eq!(deser.maintenance_margin_bps, params.maintenance_margin_bps);
639 assert_eq!(deser.initial_margin_bps, params.initial_margin_bps);
640 assert_eq!(deser.warmup_period_slots, params.warmup_period_slots);
641 }
642
643 #[test]
644 fn params_config_from_empty_json() {
645 let deser: ParamsConfig = serde_json::from_str("{}").unwrap();
646 assert_eq!(deser.maintenance_margin_bps, default_mm_bps());
647 assert_eq!(deser.initial_margin_bps, default_im_bps());
648 assert_eq!(deser.warmup_period_slots, default_warmup());
649 assert_eq!(deser.max_accounts, default_max_accounts());
650 }
651}
652
653#[cfg(kani)]
654mod proofs {
655 use super::*;
656
657 fn bounded_engine() -> NamedEngine {
658 let params = ParamsConfig {
659 max_accounts: MAX_ACCOUNTS as u64,
660 ..Default::default()
661 }
662 .to_risk_params();
663 NamedEngine::new(params, 0, 1000)
664 }
665
666 #[kani::proof]
669 fn prove_deposit_zero_always_fails() {
670 let mut eng = bounded_engine();
671 assert!(eng.deposit("alice", 0).is_err());
672 }
673
674 #[kani::proof]
675 fn prove_withdraw_unknown_account_fails() {
676 let eng = bounded_engine();
677 assert!(eng.resolve("nonexistent").is_err());
678 }
679
680 #[kani::proof]
681 fn prove_set_oracle_bounds() {
682 let mut eng = bounded_engine();
683 let price: u64 = kani::any();
684 let result = eng.set_oracle(price);
685 if price == 0 || price > MAX_ORACLE_PRICE {
686 assert!(result.is_err());
687 } else {
688 assert!(result.is_ok());
689 assert_eq!(eng.last_oracle_price, price);
690 }
691 }
692
693 #[kani::proof]
694 fn prove_ensure_account_name_validation() {
695 let mut eng = bounded_engine();
696 assert!(eng.deposit("", 100).is_err());
697 }
698
699 #[kani::proof]
700 fn prove_haircut_ratio_no_div_by_zero() {
701 let snap = EngineSnapshot {
702 vault: 0,
703 insurance_fund: 0,
704 haircut_numerator: kani::any(),
705 haircut_denominator: 0,
706 conservation: true,
707 current_slot: 0,
708 last_oracle_price: 1000,
709 last_funding_rate: 0,
710 side_mode_long: String::new(),
711 side_mode_short: String::new(),
712 oi_long_q: 0,
713 oi_short_q: 0,
714 lifetime_liquidations: 0,
715 accounts: vec![],
716 };
717 let h = snap.haircut_ratio_f64();
718 assert!(h == 1.0);
719 }
720
721 #[kani::proof]
722 fn prove_haircut_ratio_bounded() {
723 let num: u128 = kani::any();
724 let den: u128 = kani::any();
725 kani::assume(den > 0);
726 kani::assume(num <= den);
727
728 let snap = EngineSnapshot {
729 vault: 0,
730 insurance_fund: 0,
731 haircut_numerator: num,
732 haircut_denominator: den,
733 conservation: true,
734 current_slot: 0,
735 last_oracle_price: 1000,
736 last_funding_rate: 0,
737 side_mode_long: String::new(),
738 side_mode_short: String::new(),
739 oi_long_q: 0,
740 oi_short_q: 0,
741 lifetime_liquidations: 0,
742 accounts: vec![],
743 };
744 let h = snap.haircut_ratio_f64();
745 assert!(h >= 0.0 && h <= 1.0);
746 }
747
748 #[kani::proof]
749 fn prove_set_slot_always_succeeds() {
750 let mut eng = bounded_engine();
751 let slot: u64 = kani::any();
752 eng.set_slot(slot);
753 assert_eq!(eng.current_slot, slot);
754 }
755
756 #[kani::proof]
757 fn prove_set_funding_rate_always_succeeds() {
758 let mut eng = bounded_engine();
759 let rate: i64 = kani::any();
760 eng.set_funding_rate(rate);
761 assert_eq!(eng.last_funding_rate, rate);
762 }
763
764 #[kani::proof]
765 fn prove_params_to_risk_params_no_panic() {
766 let bps: u64 = kani::any();
767 kani::assume(bps <= 10_000);
768 let params = ParamsConfig {
769 maintenance_margin_bps: bps,
770 initial_margin_bps: bps,
771 max_accounts: MAX_ACCOUNTS as u64,
772 ..Default::default()
773 };
774 let _rp = params.to_risk_params();
775 }
776
777 #[kani::proof]
780 #[kani::unwind(6)]
781 fn prove_deposit_never_panics() {
782 let mut eng = bounded_engine();
783 let amount: u128 = kani::any();
784 kani::assume(amount <= MAX_VAULT_TVL);
785 let _ = eng.deposit("alice", amount);
786 }
787
788 #[kani::proof]
789 #[kani::unwind(6)]
790 fn prove_deposit_increases_vault() {
791 let mut eng = bounded_engine();
792 let amount: u128 = kani::any();
793 kani::assume(amount > 0 && amount <= 1_000_000_000);
794
795 let vault_before = eng.engine.vault.get();
796 let result = eng.deposit("alice", amount);
797 if result.is_ok() {
798 let vault_after = eng.engine.vault.get();
799 assert!(vault_after >= vault_before + amount);
800 }
801 }
802}