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