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