1#![allow(unexpected_cfgs)]
2#[cfg(not(target_os = "solana"))]
8pub mod agent;
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_not_atomic(
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_not_atomic(
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_not_atomic(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_not_atomic(
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_not_atomic(
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)]
307 pub min_liquidation_abs: u64,
308 #[serde(default = "default_min_deposit")]
309 pub min_initial_deposit: u64,
310 #[serde(default = "default_mm_req")]
311 pub min_nonzero_mm_req: u64,
312 #[serde(default = "default_im_req")]
313 pub min_nonzero_im_req: u64,
314 #[serde(default)]
315 pub insurance_floor: u64,
316}
317
318fn default_warmup() -> u64 {
319 100
320}
321fn default_mm_bps() -> u64 {
322 500
323}
324fn default_im_bps() -> u64 {
325 1000
326}
327fn default_max_accounts() -> u64 {
328 64
329}
330fn default_crank_staleness() -> u64 {
331 u64::MAX
332}
333fn default_mm_req() -> u64 {
334 1
335}
336fn default_im_req() -> u64 {
337 2
338}
339fn default_min_deposit() -> u64 {
340 2
341}
342impl ParamsConfig {
343 pub fn to_risk_params(&self) -> RiskParams {
344 RiskParams {
345 warmup_period_slots: self.warmup_period_slots,
346 maintenance_margin_bps: self.maintenance_margin_bps,
347 initial_margin_bps: self.initial_margin_bps,
348 trading_fee_bps: self.trading_fee_bps,
349 max_accounts: self.max_accounts,
350 new_account_fee: U128::new(self.new_account_fee as u128),
351 maintenance_fee_per_slot: U128::new(self.maintenance_fee_per_slot as u128),
352 max_crank_staleness_slots: self.max_crank_staleness_slots,
353 liquidation_fee_bps: self.liquidation_fee_bps,
354 liquidation_fee_cap: U128::new(self.liquidation_fee_cap as u128),
355 min_liquidation_abs: U128::new(self.min_liquidation_abs as u128),
356 min_initial_deposit: U128::new(self.min_initial_deposit as u128),
357 min_nonzero_mm_req: self.min_nonzero_mm_req as u128,
358 min_nonzero_im_req: self.min_nonzero_im_req as u128,
359 insurance_floor: U128::new(self.insurance_floor as u128),
360 }
361 }
362}
363
364impl Default for ParamsConfig {
365 fn default() -> Self {
366 Self {
367 warmup_period_slots: default_warmup(),
368 maintenance_margin_bps: default_mm_bps(),
369 initial_margin_bps: default_im_bps(),
370 trading_fee_bps: 0,
371 max_accounts: default_max_accounts(),
372 new_account_fee: 0,
373 maintenance_fee_per_slot: 0,
374 max_crank_staleness_slots: default_crank_staleness(),
375 liquidation_fee_bps: 0,
376 liquidation_fee_cap: 0,
377 min_liquidation_abs: 0,
378 min_initial_deposit: default_min_deposit(),
379 min_nonzero_mm_req: default_mm_req(),
380 min_nonzero_im_req: default_im_req(),
381 insurance_floor: 0,
382 }
383 }
384}
385
386#[derive(Debug)]
388pub enum EngineError {
389 UnknownAccount(String),
390 AccountLimitReached(usize),
391 InvalidAccountName(String),
392 InvalidAmount,
393 InvalidOraclePrice(u64),
394 Risk(RiskError),
395}
396
397impl std::fmt::Display for EngineError {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 match self {
400 Self::UnknownAccount(name) => write!(f, "unknown account: {name}"),
401 Self::AccountLimitReached(max) => write!(f, "account limit reached ({max})"),
402 Self::InvalidAccountName(name) => write!(f, "invalid account name: {name:?}"),
403 Self::InvalidAmount => write!(f, "amount must be non-zero"),
404 Self::InvalidOraclePrice(p) => write!(f, "invalid oracle price: {p}"),
405 Self::Risk(e) => write!(f, "risk engine error: {e:?}"),
406 }
407 }
408}
409
410impl std::error::Error for EngineError {}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn default_engine() -> NamedEngine {
417 let params = ParamsConfig::default().to_risk_params();
418 NamedEngine::new(params, 0, 1000)
419 }
420
421 #[test]
422 fn deposit_withdraw_roundtrip() {
423 let mut eng = default_engine();
424 eng.deposit("alice", 100_000).unwrap();
425 let snap = eng.snapshot();
426 assert_eq!(snap.vault, 100_000);
427 assert_eq!(snap.accounts[0].capital, 100_000);
428
429 eng.withdraw("alice", 50_000).unwrap();
430 let snap = eng.snapshot();
431 assert_eq!(snap.accounts[0].capital, 50_000);
432 }
433
434 #[test]
435 fn account_overflow_rejected() {
436 let params = ParamsConfig {
437 max_accounts: 2,
438 ..Default::default()
439 };
440 let risk = params.to_risk_params();
441 let mut eng = NamedEngine::new(risk, 0, 1000);
442
443 eng.deposit("a", 100).unwrap();
444 eng.deposit("b", 100).unwrap();
445
446 let err = eng.deposit("c", 100).unwrap_err();
447 assert!(matches!(err, EngineError::AccountLimitReached(_)));
448 }
449
450 #[test]
451 fn name_resolution() {
452 let mut eng = default_engine();
453 assert!(eng.resolve("alice").is_err());
454
455 eng.deposit("alice", 1000).unwrap();
456 assert_eq!(eng.resolve("alice").unwrap(), 0);
457 }
458
459 #[test]
460 fn zero_amount_rejected() {
461 let mut eng = default_engine();
462 let err = eng.deposit("alice", 0).unwrap_err();
463 assert!(matches!(err, EngineError::InvalidAmount));
464 }
465
466 #[test]
467 fn trade_basic() {
468 let mut eng = default_engine();
469 eng.deposit("alice", 100_000).unwrap();
470 eng.deposit("bob", 100_000).unwrap();
471 eng.crank(1000, 1).unwrap();
472 eng.trade("alice", "bob", 10 * POS_SCALE as i128, 1000)
473 .unwrap();
474
475 let snap = eng.snapshot();
476 assert_eq!(snap.vault, 200_000);
477 assert!(snap.conservation);
478 }
479
480 #[test]
481 fn snapshot_serialization_roundtrip() {
482 let mut eng = default_engine();
483 eng.deposit("alice", 50_000).unwrap();
484 let snap = eng.snapshot();
485
486 let json = serde_json::to_string(&snap).unwrap();
487 let deser: EngineSnapshot = serde_json::from_str(&json).unwrap();
488 assert_eq!(deser.vault, snap.vault);
489 assert_eq!(deser.accounts.len(), 1);
490 assert_eq!(deser.accounts[0].name, "alice");
491 }
492
493 #[test]
494 fn empty_name_rejected() {
495 let mut eng = default_engine();
496 let err = eng.deposit("", 100).unwrap_err();
497 assert!(matches!(err, EngineError::InvalidAccountName(_)));
498 }
499
500 #[test]
501 fn duplicate_account_idempotent() {
502 let mut eng = default_engine();
503 eng.deposit("alice", 1000).unwrap();
504 eng.deposit("alice", 2000).unwrap();
505 assert_eq!(eng.resolve("alice").unwrap(), 0);
506 let snap = eng.snapshot();
507 assert_eq!(snap.accounts.len(), 1);
508 assert_eq!(snap.accounts[0].capital, 3000);
509 }
510
511 #[test]
512 fn invalid_oracle_price_rejected() {
513 let mut eng = default_engine();
514 assert!(eng.set_oracle(0).is_err());
515 }
516
517 #[test]
518 fn oracle_price_above_max_rejected() {
519 let mut eng = default_engine();
520 assert!(eng.set_oracle(MAX_ORACLE_PRICE + 1).is_err());
521 assert!(eng.set_oracle(MAX_ORACLE_PRICE).is_ok());
522 }
523
524 #[test]
525 fn withdraw_unknown_account_rejected() {
526 let mut eng = default_engine();
527 let err = eng.withdraw("ghost", 100).unwrap_err();
528 assert!(matches!(err, EngineError::UnknownAccount(_)));
529 }
530
531 #[test]
532 fn withdraw_zero_rejected() {
533 let mut eng = default_engine();
534 eng.deposit("alice", 1000).unwrap();
535 let err = eng.withdraw("alice", 0).unwrap_err();
536 assert!(matches!(err, EngineError::InvalidAmount));
537 }
538
539 #[test]
540 fn long_account_name_rejected() {
541 let mut eng = default_engine();
542 let name = "a".repeat(65);
543 let err = eng.deposit(&name, 100).unwrap_err();
544 assert!(matches!(err, EngineError::InvalidAccountName(_)));
545 }
546
547 #[test]
548 fn max_length_account_name_accepted() {
549 let mut eng = default_engine();
550 let name = "a".repeat(64);
551 eng.deposit(&name, 100).unwrap();
552 assert_eq!(eng.resolve(&name).unwrap(), 0);
553 }
554
555 #[test]
556 fn trade_unknown_account_rejected() {
557 let mut eng = default_engine();
558 eng.deposit("alice", 100_000).unwrap();
559 let err = eng
560 .trade("alice", "ghost", 10 * POS_SCALE as i128, 1000)
561 .unwrap_err();
562 assert!(matches!(err, EngineError::UnknownAccount(_)));
563 }
564
565 #[test]
566 fn liquidate_unknown_account_rejected() {
567 let mut eng = default_engine();
568 let err = eng.liquidate("ghost").unwrap_err();
569 assert!(matches!(err, EngineError::UnknownAccount(_)));
570 }
571
572 #[test]
573 fn settle_unknown_account_rejected() {
574 let mut eng = default_engine();
575 let err = eng.settle("ghost").unwrap_err();
576 assert!(matches!(err, EngineError::UnknownAccount(_)));
577 }
578
579 #[test]
580 fn set_slot_and_funding_rate() {
581 let mut eng = default_engine();
582 eng.set_slot(42);
583 assert_eq!(eng.current_slot, 42);
584 eng.set_funding_rate(-100);
585 assert_eq!(eng.last_funding_rate, -100);
586 }
587
588 #[test]
589 fn crank_updates_oracle_and_slot() {
590 let mut eng = default_engine();
591 eng.deposit("alice", 100_000).unwrap();
592 eng.crank(2000, 50).unwrap();
593 assert_eq!(eng.last_oracle_price, 2000);
594 assert_eq!(eng.current_slot, 50);
595 }
596
597 #[test]
598 fn snapshot_empty_engine() {
599 let eng = default_engine();
600 let snap = eng.snapshot();
601 assert_eq!(snap.vault, 0);
602 assert_eq!(snap.accounts.len(), 0);
603 assert!(snap.conservation);
604 }
605
606 #[test]
607 fn error_display() {
608 let e = EngineError::UnknownAccount("bob".to_string());
609 assert_eq!(e.to_string(), "unknown account: bob");
610
611 let e = EngineError::AccountLimitReached(64);
612 assert_eq!(e.to_string(), "account limit reached (64)");
613
614 let e = EngineError::InvalidAccountName("".to_string());
615 assert_eq!(e.to_string(), "invalid account name: \"\"");
616
617 let e = EngineError::InvalidAmount;
618 assert_eq!(e.to_string(), "amount must be non-zero");
619
620 let e = EngineError::InvalidOraclePrice(0);
621 assert_eq!(e.to_string(), "invalid oracle price: 0");
622 }
623
624 #[test]
625 fn params_config_default_roundtrip() {
626 let params = ParamsConfig::default();
627 let json = serde_json::to_string(¶ms).unwrap();
628 let deser: ParamsConfig = serde_json::from_str(&json).unwrap();
629 assert_eq!(deser.maintenance_margin_bps, params.maintenance_margin_bps);
630 assert_eq!(deser.initial_margin_bps, params.initial_margin_bps);
631 assert_eq!(deser.warmup_period_slots, params.warmup_period_slots);
632 }
633
634 #[test]
635 fn params_config_from_empty_json() {
636 let deser: ParamsConfig = serde_json::from_str("{}").unwrap();
637 assert_eq!(deser.maintenance_margin_bps, default_mm_bps());
638 assert_eq!(deser.initial_margin_bps, default_im_bps());
639 assert_eq!(deser.warmup_period_slots, default_warmup());
640 assert_eq!(deser.max_accounts, default_max_accounts());
641 }
642}
643
644#[cfg(kani)]
645mod proofs {
646 use super::*;
647
648 fn bounded_engine() -> NamedEngine {
649 let params = ParamsConfig {
650 max_accounts: MAX_ACCOUNTS as u64,
651 ..Default::default()
652 }
653 .to_risk_params();
654 NamedEngine::new(params, 0, 1000)
655 }
656
657 #[kani::proof]
660 fn prove_deposit_zero_always_fails() {
661 let mut eng = bounded_engine();
662 assert!(eng.deposit("alice", 0).is_err());
663 }
664
665 #[kani::proof]
666 fn prove_withdraw_unknown_account_fails() {
667 let eng = bounded_engine();
668 assert!(eng.resolve("nonexistent").is_err());
669 }
670
671 #[kani::proof]
672 fn prove_set_oracle_bounds() {
673 let mut eng = bounded_engine();
674 let price: u64 = kani::any();
675 let result = eng.set_oracle(price);
676 if price == 0 || price > MAX_ORACLE_PRICE {
677 assert!(result.is_err());
678 } else {
679 assert!(result.is_ok());
680 assert_eq!(eng.last_oracle_price, price);
681 }
682 }
683
684 #[kani::proof]
685 fn prove_ensure_account_name_validation() {
686 let mut eng = bounded_engine();
687 assert!(eng.deposit("", 100).is_err());
688 }
689
690 #[kani::proof]
691 fn prove_haircut_ratio_no_div_by_zero() {
692 let snap = EngineSnapshot {
693 vault: 0,
694 insurance_fund: 0,
695 haircut_numerator: kani::any(),
696 haircut_denominator: 0,
697 conservation: true,
698 current_slot: 0,
699 last_oracle_price: 1000,
700 last_funding_rate: 0,
701 side_mode_long: String::new(),
702 side_mode_short: String::new(),
703 oi_long_q: 0,
704 oi_short_q: 0,
705 lifetime_liquidations: 0,
706 accounts: vec![],
707 };
708 let h = snap.haircut_ratio_f64();
709 assert!(h == 1.0);
710 }
711
712 #[kani::proof]
713 fn prove_haircut_ratio_bounded() {
714 let num: u128 = kani::any();
715 let den: u128 = kani::any();
716 kani::assume(den > 0);
717 kani::assume(num <= den);
718
719 let snap = EngineSnapshot {
720 vault: 0,
721 insurance_fund: 0,
722 haircut_numerator: num,
723 haircut_denominator: den,
724 conservation: true,
725 current_slot: 0,
726 last_oracle_price: 1000,
727 last_funding_rate: 0,
728 side_mode_long: String::new(),
729 side_mode_short: String::new(),
730 oi_long_q: 0,
731 oi_short_q: 0,
732 lifetime_liquidations: 0,
733 accounts: vec![],
734 };
735 let h = snap.haircut_ratio_f64();
736 assert!(h >= 0.0 && h <= 1.0);
737 }
738
739 #[kani::proof]
740 fn prove_set_slot_always_succeeds() {
741 let mut eng = bounded_engine();
742 let slot: u64 = kani::any();
743 eng.set_slot(slot);
744 assert_eq!(eng.current_slot, slot);
745 }
746
747 #[kani::proof]
748 fn prove_set_funding_rate_always_succeeds() {
749 let mut eng = bounded_engine();
750 let rate: i64 = kani::any();
751 eng.set_funding_rate(rate);
752 assert_eq!(eng.last_funding_rate, rate);
753 }
754
755 #[kani::proof]
756 fn prove_params_to_risk_params_no_panic() {
757 let bps: u64 = kani::any();
758 kani::assume(bps <= 10_000);
759 let params = ParamsConfig {
760 maintenance_margin_bps: bps,
761 initial_margin_bps: bps,
762 max_accounts: MAX_ACCOUNTS as u64,
763 ..Default::default()
764 };
765 let _rp = params.to_risk_params();
766 }
767
768 #[kani::proof]
771 #[kani::unwind(6)]
772 fn prove_deposit_never_panics() {
773 let mut eng = bounded_engine();
774 let amount: u128 = kani::any();
775 kani::assume(amount <= MAX_VAULT_TVL);
776 let _ = eng.deposit("alice", amount);
777 }
778
779 #[kani::proof]
780 #[kani::unwind(6)]
781 fn prove_deposit_increases_vault() {
782 let mut eng = bounded_engine();
783 let amount: u128 = kani::any();
784 kani::assume(amount > 0 && amount <= 1_000_000_000);
785
786 let vault_before = eng.engine.vault.get();
787 let result = eng.deposit("alice", amount);
788 if result.is_ok() {
789 let vault_after = eng.engine.vault.get();
790 assert!(vault_after >= vault_before + amount);
791 }
792 }
793}