1#![no_std]
13#![cfg_attr(not(feature = "solana"), forbid(unsafe_code))]
14#[cfg(kani)]
17extern crate kani;
18
19macro_rules! test_visible {
32 (
33 $(#[$meta:meta])*
34 fn $name:ident($($args:tt)*) $(-> $ret:ty)? $body:block
35 ) => {
36 $(#[$meta])*
37 #[cfg(any(feature = "test", kani))]
38 pub fn $name($($args)*) $(-> $ret)? $body
39
40 $(#[$meta])*
41 #[cfg(not(any(feature = "test", kani)))]
42 fn $name($($args)*) $(-> $ret)? $body
43 };
44}
45
46#[cfg(kani)]
51pub const MAX_ACCOUNTS: usize = 4;
52
53#[cfg(all(feature = "test", not(kani)))]
54pub const MAX_ACCOUNTS: usize = 64;
55
56#[cfg(all(not(kani), not(feature = "test")))]
57pub const MAX_ACCOUNTS: usize = 4096;
58
59pub const BITMAP_WORDS: usize = (MAX_ACCOUNTS + 63) / 64;
60pub const MAX_ROUNDING_SLACK: u128 = MAX_ACCOUNTS as u128;
61const ACCOUNT_IDX_MASK: usize = MAX_ACCOUNTS - 1;
62
63pub const GC_CLOSE_BUDGET: u32 = 32;
64pub const ACCOUNTS_PER_CRANK: u16 = 128;
65pub const LIQ_BUDGET_PER_CRANK: u16 = 64;
66
67pub const POS_SCALE: u128 = 1_000_000;
69
70pub const ADL_ONE: u128 = 1_000_000;
72
73pub const MIN_A_SIDE: u128 = 1_000;
75
76pub const MAX_ORACLE_PRICE: u64 = 1_000_000_000_000;
78
79pub const MAX_FUNDING_DT: u64 = u16::MAX as u64;
81
82pub const MAX_ABS_FUNDING_BPS_PER_SLOT: i64 = 10_000;
84
85pub const MAX_VAULT_TVL: u128 = 10_000_000_000_000_000;
87pub const MAX_POSITION_ABS_Q: u128 = 100_000_000_000_000;
88pub const MAX_ACCOUNT_NOTIONAL: u128 = 100_000_000_000_000_000_000;
89pub const MAX_TRADE_SIZE_Q: u128 = MAX_POSITION_ABS_Q; pub const MAX_OI_SIDE_Q: u128 = 100_000_000_000_000;
91pub const MAX_MATERIALIZED_ACCOUNTS: u64 = 1_000_000;
92pub const MAX_ACCOUNT_POSITIVE_PNL: u128 = 100_000_000_000_000_000_000_000_000_000_000;
93pub const MAX_PNL_POS_TOT: u128 = 100_000_000_000_000_000_000_000_000_000_000_000_000;
94pub const MAX_TRADING_FEE_BPS: u64 = 10_000;
95pub const MAX_MARGIN_BPS: u64 = 10_000;
96pub const MAX_LIQUIDATION_FEE_BPS: u64 = 10_000;
97pub const MAX_PROTOCOL_FEE_ABS: u128 = 1_000_000_000_000_000_000_000_000_000_000_000_000; pub const MAX_MAINTENANCE_FEE_PER_SLOT: u128 = 10_000_000_000_000_000; use super::i128_types::{I128, U128};
105
106use super::wide_math::{
111 ceil_div_positive_checked, fee_debt_u128_checked, floor_div_signed_conservative_i128,
112 mul_div_ceil_u128, mul_div_floor_u128, mul_div_floor_u256_with_rem, saturating_mul_u128_u64,
113 wide_mul_div_ceil_u128_or_over_i128max, wide_mul_div_floor_u128,
114 wide_signed_mul_div_floor_from_k_pair, OverI128Magnitude, I256, U256,
115};
116
117#[repr(u8)]
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum SideMode {
133 Normal = 0,
134 DrainOnly = 1,
135 ResetPending = 2,
136}
137
138impl SideMode {
139 pub const NORMAL: u8 = 0;
140 pub const DRAIN_ONLY: u8 = 1;
141 pub const RESET_PENDING: u8 = 2;
142
143 pub fn from_u8(v: u8) -> Self {
144 match v {
145 0 => SideMode::Normal,
146 1 => SideMode::DrainOnly,
147 2 => SideMode::ResetPending,
148 _ => SideMode::Normal, }
150 }
151
152 pub fn to_u8(self) -> u8 {
153 self as u8
154 }
155}
156
157pub struct InstructionContext {
159 pub pending_reset_long: bool,
160 pub pending_reset_short: bool,
161}
162
163impl InstructionContext {
164 pub fn new() -> Self {
165 Self {
166 pending_reset_long: false,
167 pending_reset_short: false,
168 }
169 }
170}
171
172#[repr(C)]
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub struct Account {
176 pub account_id: u64,
177 pub capital: U128,
178 pub kind: u8, pub pnl: i128,
182
183 pub reserved_pnl: u128,
185
186 pub warmup_started_at_slot: u64,
188
189 pub warmup_slope_per_step: u128,
191
192 pub position_basis_q: i128,
194
195 pub adl_a_basis: u128,
197
198 pub adl_k_snap: i128,
200
201 pub adl_epoch_snap: u64,
203
204 pub matcher_program: [u8; 32],
206 pub matcher_context: [u8; 32],
207
208 pub owner: [u8; 32],
210
211 pub fee_credits: I128,
213 pub last_fee_slot: u64,
214
215 pub fees_earned_total: U128,
217}
218
219impl Account {
220 pub const KIND_USER: u8 = 0;
221 pub const KIND_LP: u8 = 1;
222
223 pub fn is_lp(&self) -> bool {
224 self.kind == Self::KIND_LP
225 }
226
227 pub fn is_user(&self) -> bool {
228 self.kind == Self::KIND_USER
229 }
230}
231
232fn empty_account() -> Account {
233 Account {
234 account_id: 0,
235 capital: U128::ZERO,
236 kind: Account::KIND_USER,
237 pnl: 0i128,
238 reserved_pnl: 0u128,
239 warmup_started_at_slot: 0,
240 warmup_slope_per_step: 0u128,
241 position_basis_q: 0i128,
242 adl_a_basis: ADL_ONE,
243 adl_k_snap: 0i128,
244 adl_epoch_snap: 0,
245 matcher_program: [0; 32],
246 matcher_context: [0; 32],
247 owner: [0; 32],
248 fee_credits: I128::ZERO,
249 last_fee_slot: 0,
250 fees_earned_total: U128::ZERO,
251 }
252}
253
254#[repr(C)]
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub struct InsuranceFund {
258 pub balance: U128,
259}
260
261#[repr(C)]
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub struct RiskParams {
265 pub warmup_period_slots: u64,
266 pub maintenance_margin_bps: u64,
267 pub initial_margin_bps: u64,
268 pub trading_fee_bps: u64,
269 pub max_accounts: u64,
270 pub new_account_fee: U128,
271 pub maintenance_fee_per_slot: U128,
272 pub max_crank_staleness_slots: u64,
273 pub liquidation_fee_bps: u64,
274 pub liquidation_fee_cap: U128,
275 pub liquidation_buffer_bps: u64,
276 pub min_liquidation_abs: U128,
277 pub min_initial_deposit: U128,
278 pub min_nonzero_mm_req: u128,
280 pub min_nonzero_im_req: u128,
281 pub insurance_floor: U128,
283}
284
285#[repr(C)]
287#[derive(Clone, Debug, PartialEq, Eq)]
288pub struct RiskEngine {
289 pub vault: U128,
290 pub insurance_fund: InsuranceFund,
291 pub params: RiskParams,
292 pub current_slot: u64,
293
294 pub funding_rate_bps_per_slot_last: i64,
296
297 pub last_crank_slot: u64,
299 pub max_crank_staleness_slots: u64,
300
301 pub c_tot: U128,
303 pub pnl_pos_tot: u128,
304 pub pnl_matured_pos_tot: u128,
305
306 pub liq_cursor: u16,
308 pub gc_cursor: u16,
309 pub last_full_sweep_start_slot: u64,
310 pub last_full_sweep_completed_slot: u64,
311 pub crank_cursor: u16,
312 pub sweep_start_idx: u16,
313
314 pub lifetime_liquidations: u64,
316
317 pub adl_mult_long: u128,
319 pub adl_mult_short: u128,
320 pub adl_coeff_long: i128,
321 pub adl_coeff_short: i128,
322 pub adl_epoch_long: u64,
323 pub adl_epoch_short: u64,
324 pub adl_epoch_start_k_long: i128,
325 pub adl_epoch_start_k_short: i128,
326 pub oi_eff_long_q: u128,
327 pub oi_eff_short_q: u128,
328 pub side_mode_long: u8,
330 pub side_mode_short: u8,
331 pub stored_pos_count_long: u64,
332 pub stored_pos_count_short: u64,
333 pub stale_account_count_long: u64,
334 pub stale_account_count_short: u64,
335
336 pub phantom_dust_bound_long_q: u128,
338 pub phantom_dust_bound_short_q: u128,
339
340 pub materialized_account_count: u64,
342
343 pub last_oracle_price: u64,
345 pub last_market_slot: u64,
347 pub funding_price_sample_last: u64,
349
350 pub insurance_floor: u128,
352
353 pub used: [u64; BITMAP_WORDS],
355 pub num_used_accounts: u16,
356 pub next_account_id: u64,
357 pub free_head: u16,
358 pub next_free: [u16; MAX_ACCOUNTS],
359 pub accounts: [Account; MAX_ACCOUNTS],
360}
361
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub enum RiskError {
368 InsufficientBalance,
369 Undercollateralized,
370 Unauthorized,
371 InvalidMatchingEngine,
372 PnlNotWarmedUp,
373 Overflow,
374 AccountNotFound,
375 NotAnLPAccount,
376 PositionSizeMismatch,
377 AccountKindMismatch,
378 SideBlocked,
379 CorruptState,
380}
381
382pub type Result<T> = core::result::Result<T, RiskError>;
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum LiquidationPolicy {
387 FullClose,
388 ExactPartial(u128), }
390
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub struct CrankOutcome {
394 pub advanced: bool,
395 pub slots_forgiven: u64,
396 pub caller_settle_ok: bool,
397 pub force_realize_needed: bool,
398 pub panic_needed: bool,
399 pub num_liquidations: u32,
400 pub num_liq_errors: u16,
401 pub num_gc_closed: u32,
402 pub last_cursor: u16,
403 pub sweep_complete: bool,
404}
405
406#[inline]
411fn add_u128(a: u128, b: u128) -> u128 {
412 a.checked_add(b).expect("add_u128 overflow")
413}
414
415#[inline]
416fn sub_u128(a: u128, b: u128) -> u128 {
417 a.checked_sub(b).expect("sub_u128 underflow")
418}
419
420#[inline]
421fn mul_u128(a: u128, b: u128) -> u128 {
422 a.checked_mul(b).expect("mul_u128 overflow")
423}
424
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
427pub enum Side {
428 Long,
429 Short,
430}
431
432fn side_of_i128(v: i128) -> Option<Side> {
433 if v == 0 {
434 None
435 } else if v > 0 {
436 Some(Side::Long)
437 } else {
438 Some(Side::Short)
439 }
440}
441
442fn opposite_side(s: Side) -> Side {
443 match s {
444 Side::Long => Side::Short,
445 Side::Short => Side::Long,
446 }
447}
448
449fn i128_clamp_pos(v: i128) -> u128 {
451 if v > 0 {
452 v as u128
453 } else {
454 0u128
455 }
456}
457
458impl RiskEngine {
463 fn validate_params(params: &RiskParams) {
466 assert!(
468 (params.max_accounts as usize) <= MAX_ACCOUNTS && params.max_accounts > 0,
469 "max_accounts must be in 1..=MAX_ACCOUNTS"
470 );
471
472 assert!(
474 params.maintenance_margin_bps <= params.initial_margin_bps,
475 "maintenance_margin_bps must be <= initial_margin_bps (spec §1.4)"
476 );
477 assert!(
478 params.initial_margin_bps <= 10_000,
479 "initial_margin_bps must be <= 10_000"
480 );
481
482 assert!(
484 params.trading_fee_bps <= 10_000,
485 "trading_fee_bps must be <= 10_000"
486 );
487 assert!(
488 params.liquidation_fee_bps <= 10_000,
489 "liquidation_fee_bps must be <= 10_000"
490 );
491
492 assert!(
494 params.min_nonzero_mm_req > 0,
495 "min_nonzero_mm_req must be > 0"
496 );
497 assert!(
498 params.min_nonzero_mm_req < params.min_nonzero_im_req,
499 "min_nonzero_mm_req must be strictly less than min_nonzero_im_req"
500 );
501 assert!(
502 params.min_nonzero_im_req <= params.min_initial_deposit.get(),
503 "min_nonzero_im_req must be <= min_initial_deposit (spec §1.4)"
504 );
505
506 assert!(
508 params.min_initial_deposit.get() > 0,
509 "min_initial_deposit must be > 0 (spec §1.4)"
510 );
511 assert!(
512 params.min_initial_deposit.get() <= MAX_VAULT_TVL,
513 "min_initial_deposit must be <= MAX_VAULT_TVL"
514 );
515
516 assert!(
518 params.min_liquidation_abs.get() <= params.liquidation_fee_cap.get(),
519 "min_liquidation_abs must be <= liquidation_fee_cap (spec §1.4)"
520 );
521 assert!(
522 params.liquidation_fee_cap.get() <= MAX_PROTOCOL_FEE_ABS,
523 "liquidation_fee_cap must be <= MAX_PROTOCOL_FEE_ABS (spec §1.4)"
524 );
525
526 assert!(
528 params.maintenance_fee_per_slot.get() <= MAX_MAINTENANCE_FEE_PER_SLOT,
529 "maintenance_fee_per_slot must be <= MAX_MAINTENANCE_FEE_PER_SLOT (spec §8.2.1)"
530 );
531
532 assert!(
534 params.insurance_floor.get() <= MAX_VAULT_TVL,
535 "insurance_floor must be <= MAX_VAULT_TVL (spec §1.4)"
536 );
537 }
538
539 #[cfg(any(feature = "test", kani))]
542 pub fn new(params: RiskParams) -> Self {
543 Self::new_with_market(params, 0, 1)
544 }
545
546 pub fn new_with_market(params: RiskParams, init_slot: u64, init_oracle_price: u64) -> Self {
549 Self::validate_params(¶ms);
550 assert!(
551 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
552 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
553 );
554 let mut engine = Self {
555 vault: U128::ZERO,
556 insurance_fund: InsuranceFund {
557 balance: U128::ZERO,
558 },
559 params,
560 current_slot: init_slot,
561 funding_rate_bps_per_slot_last: 0,
562 last_crank_slot: 0,
563 max_crank_staleness_slots: params.max_crank_staleness_slots,
564 c_tot: U128::ZERO,
565 pnl_pos_tot: 0u128,
566 pnl_matured_pos_tot: 0u128,
567 liq_cursor: 0,
568 gc_cursor: 0,
569 last_full_sweep_start_slot: 0,
570 last_full_sweep_completed_slot: 0,
571 crank_cursor: 0,
572 sweep_start_idx: 0,
573 lifetime_liquidations: 0,
574 adl_mult_long: ADL_ONE,
575 adl_mult_short: ADL_ONE,
576 adl_coeff_long: 0i128,
577 adl_coeff_short: 0i128,
578 adl_epoch_long: 0,
579 adl_epoch_short: 0,
580 adl_epoch_start_k_long: 0i128,
581 adl_epoch_start_k_short: 0i128,
582 oi_eff_long_q: 0u128,
583 oi_eff_short_q: 0u128,
584 side_mode_long: SideMode::NORMAL,
585 side_mode_short: SideMode::NORMAL,
586 stored_pos_count_long: 0,
587 stored_pos_count_short: 0,
588 stale_account_count_long: 0,
589 stale_account_count_short: 0,
590 phantom_dust_bound_long_q: 0u128,
591 phantom_dust_bound_short_q: 0u128,
592 materialized_account_count: 0,
593 last_oracle_price: init_oracle_price,
594 last_market_slot: init_slot,
595 funding_price_sample_last: init_oracle_price,
596 insurance_floor: params.insurance_floor.get(),
597 used: [0; BITMAP_WORDS],
598 num_used_accounts: 0,
599 next_account_id: 0,
600 free_head: 0,
601 next_free: [0; MAX_ACCOUNTS],
602 accounts: [empty_account(); MAX_ACCOUNTS],
603 };
604
605 for i in 0..MAX_ACCOUNTS - 1 {
606 engine.next_free[i] = (i + 1) as u16;
607 }
608 engine.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
609
610 engine
611 }
612
613 pub fn init_in_place(&mut self, params: RiskParams, init_slot: u64, init_oracle_price: u64) {
616 Self::validate_params(¶ms);
617 assert!(
618 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
619 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
620 );
621 self.vault = U128::ZERO;
622 self.insurance_fund = InsuranceFund {
623 balance: U128::ZERO,
624 };
625 self.params = params;
626 self.current_slot = init_slot;
627 self.funding_rate_bps_per_slot_last = 0;
628 self.last_crank_slot = 0;
629 self.max_crank_staleness_slots = params.max_crank_staleness_slots;
630 self.c_tot = U128::ZERO;
631 self.pnl_pos_tot = 0;
632 self.pnl_matured_pos_tot = 0;
633 self.liq_cursor = 0;
634 self.gc_cursor = 0;
635 self.last_full_sweep_start_slot = 0;
636 self.last_full_sweep_completed_slot = 0;
637 self.crank_cursor = 0;
638 self.sweep_start_idx = 0;
639 self.lifetime_liquidations = 0;
640 self.adl_mult_long = ADL_ONE;
641 self.adl_mult_short = ADL_ONE;
642 self.adl_coeff_long = 0;
643 self.adl_coeff_short = 0;
644 self.adl_epoch_long = 0;
645 self.adl_epoch_short = 0;
646 self.adl_epoch_start_k_long = 0;
647 self.adl_epoch_start_k_short = 0;
648 self.oi_eff_long_q = 0;
649 self.oi_eff_short_q = 0;
650 self.side_mode_long = SideMode::NORMAL;
651 self.side_mode_short = SideMode::NORMAL;
652 self.stored_pos_count_long = 0;
653 self.stored_pos_count_short = 0;
654 self.stale_account_count_long = 0;
655 self.stale_account_count_short = 0;
656 self.phantom_dust_bound_long_q = 0;
657 self.phantom_dust_bound_short_q = 0;
658 self.materialized_account_count = 0;
659 self.last_oracle_price = init_oracle_price;
660 self.last_market_slot = init_slot;
661 self.funding_price_sample_last = init_oracle_price;
662 self.insurance_floor = params.insurance_floor.get();
663 self.used = [0; BITMAP_WORDS];
664 self.num_used_accounts = 0;
665 self.next_account_id = 0;
666 self.free_head = 0;
667 self.accounts = [empty_account(); MAX_ACCOUNTS];
668 for i in 0..MAX_ACCOUNTS - 1 {
669 self.next_free[i] = (i + 1) as u16;
670 }
671 self.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
672 }
673
674 pub fn is_used(&self, idx: usize) -> bool {
679 if idx >= MAX_ACCOUNTS {
680 return false;
681 }
682 let w = idx >> 6;
683 let b = idx & 63;
684 ((self.used[w] >> b) & 1) == 1
685 }
686
687 fn set_used(&mut self, idx: usize) {
688 let w = idx >> 6;
689 let b = idx & 63;
690 self.used[w] |= 1u64 << b;
691 }
692
693 fn clear_used(&mut self, idx: usize) {
694 let w = idx >> 6;
695 let b = idx & 63;
696 self.used[w] &= !(1u64 << b);
697 }
698
699 #[allow(dead_code)]
700 fn for_each_used_mut<F: FnMut(usize, &mut Account)>(&mut self, mut f: F) {
701 for (block, word) in self.used.iter().copied().enumerate() {
702 let mut w = word;
703 while w != 0 {
704 let bit = w.trailing_zeros() as usize;
705 let idx = block * 64 + bit;
706 w &= w - 1;
707 if idx >= MAX_ACCOUNTS {
708 continue;
709 }
710 f(idx, &mut self.accounts[idx]);
711 }
712 }
713 }
714
715 fn for_each_used<F: FnMut(usize, &Account)>(&self, mut f: F) {
716 for (block, word) in self.used.iter().copied().enumerate() {
717 let mut w = word;
718 while w != 0 {
719 let bit = w.trailing_zeros() as usize;
720 let idx = block * 64 + bit;
721 w &= w - 1;
722 if idx >= MAX_ACCOUNTS {
723 continue;
724 }
725 f(idx, &self.accounts[idx]);
726 }
727 }
728 }
729
730 fn alloc_slot(&mut self) -> Result<u16> {
735 if self.free_head == u16::MAX {
736 return Err(RiskError::Overflow);
737 }
738 let idx = self.free_head;
739 self.free_head = self.next_free[idx as usize];
740 self.set_used(idx as usize);
741 self.num_used_accounts = self.num_used_accounts.saturating_add(1);
742 Ok(idx)
743 }
744
745 test_visible! {
746 fn free_slot(&mut self, idx: u16) {
747 self.accounts[idx as usize] = empty_account();
748 self.clear_used(idx as usize);
749 self.next_free[idx as usize] = self.free_head;
750 self.free_head = idx;
751 self.num_used_accounts = self.num_used_accounts.saturating_sub(1);
752 self.materialized_account_count = self.materialized_account_count.saturating_sub(1);
754 }
755 }
756
757 fn materialize_at(&mut self, idx: u16, slot_anchor: u64) -> Result<()> {
761 if idx as usize >= MAX_ACCOUNTS {
762 return Err(RiskError::AccountNotFound);
763 }
764
765 let used_count = self.num_used_accounts as u64;
766 if used_count >= self.params.max_accounts {
767 return Err(RiskError::Overflow);
768 }
769
770 self.materialized_account_count = self
772 .materialized_account_count
773 .checked_add(1)
774 .ok_or(RiskError::Overflow)?;
775 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
776 self.materialized_account_count -= 1;
777 return Err(RiskError::Overflow);
778 }
779
780 let mut found = false;
783 if self.free_head == idx {
784 self.free_head = self.next_free[idx as usize];
785 found = true;
786 } else {
787 let mut prev = self.free_head;
788 let mut steps = 0usize;
789 while prev != u16::MAX && steps < MAX_ACCOUNTS {
790 if self.next_free[prev as usize] == idx {
791 self.next_free[prev as usize] = self.next_free[idx as usize];
792 found = true;
793 break;
794 }
795 prev = self.next_free[prev as usize];
796 steps += 1;
797 }
798 }
799 if !found {
800 self.materialized_account_count -= 1;
802 return Err(RiskError::CorruptState);
803 }
804
805 self.set_used(idx as usize);
806 self.num_used_accounts = self.num_used_accounts.saturating_add(1);
807
808 let account_id = self.next_account_id;
809 self.next_account_id = self.next_account_id.saturating_add(1);
810
811 self.accounts[idx as usize] = Account {
813 kind: Account::KIND_USER,
814 account_id,
815 capital: U128::ZERO,
816 pnl: 0i128,
817 reserved_pnl: 0u128,
818 warmup_started_at_slot: slot_anchor,
819 warmup_slope_per_step: 0u128,
820 position_basis_q: 0i128,
821 adl_a_basis: ADL_ONE,
822 adl_k_snap: 0i128,
823 adl_epoch_snap: 0,
824 matcher_program: [0; 32],
825 matcher_context: [0; 32],
826 owner: [0; 32],
827 fee_credits: I128::ZERO,
828 last_fee_slot: slot_anchor,
829 fees_earned_total: U128::ZERO,
830 };
831
832 Ok(())
833 }
834
835 test_visible! {
842 fn set_pnl(&mut self, idx: usize, new_pnl: i128) {
843 assert!(new_pnl != i128::MIN, "set_pnl: i128::MIN forbidden");
845
846 let old = self.accounts[idx].pnl;
847 let old_pos = i128_clamp_pos(old);
848 let old_r = self.accounts[idx].reserved_pnl;
849 let old_rel = old_pos - old_r;
850 let new_pos = i128_clamp_pos(new_pnl);
851
852 assert!(new_pos <= MAX_ACCOUNT_POSITIVE_PNL, "set_pnl: exceeds MAX_ACCOUNT_POSITIVE_PNL");
854
855 let new_r = if new_pos > old_pos {
857 let reserve_add = new_pos - old_pos;
859 let nr = old_r.checked_add(reserve_add)
860 .expect("set_pnl: new_R overflow");
861 assert!(nr <= new_pos, "set_pnl: new_R > new_pos");
862 nr
863 } else {
864 let pos_loss = old_pos - new_pos;
866 let nr = old_r.saturating_sub(pos_loss);
867 assert!(nr <= new_pos, "set_pnl: new_R > new_pos");
868 nr
869 };
870
871 let new_rel = new_pos - new_r;
872
873 if new_pos > old_pos {
875 let delta = new_pos - old_pos;
876 self.pnl_pos_tot = self.pnl_pos_tot.checked_add(delta)
877 .expect("set_pnl: pnl_pos_tot overflow");
878 } else if old_pos > new_pos {
879 let delta = old_pos - new_pos;
880 self.pnl_pos_tot = self.pnl_pos_tot.checked_sub(delta)
881 .expect("set_pnl: pnl_pos_tot underflow");
882 }
883 assert!(self.pnl_pos_tot <= MAX_PNL_POS_TOT, "set_pnl: exceeds MAX_PNL_POS_TOT");
884
885 if new_rel > old_rel {
887 let delta = new_rel - old_rel;
888 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_add(delta)
889 .expect("set_pnl: pnl_matured_pos_tot overflow");
890 } else if old_rel > new_rel {
891 let delta = old_rel - new_rel;
892 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(delta)
893 .expect("set_pnl: pnl_matured_pos_tot underflow");
894 }
895 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
896 "set_pnl: pnl_matured_pos_tot > pnl_pos_tot");
897
898 self.accounts[idx].pnl = new_pnl;
900 self.accounts[idx].reserved_pnl = new_r;
901 }
902 }
903
904 test_visible! {
906 fn set_reserved_pnl(&mut self, idx: usize, new_r: u128) {
907 let pos = i128_clamp_pos(self.accounts[idx].pnl);
908 assert!(new_r <= pos, "set_reserved_pnl: new_R > max(PNL_i, 0)");
909
910 let old_r = self.accounts[idx].reserved_pnl;
911 let old_rel = pos - old_r;
912 let new_rel = pos - new_r;
913
914 if new_rel > old_rel {
916 let delta = new_rel - old_rel;
917 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_add(delta)
918 .expect("set_reserved_pnl: pnl_matured_pos_tot overflow");
919 } else if old_rel > new_rel {
920 let delta = old_rel - new_rel;
921 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(delta)
922 .expect("set_reserved_pnl: pnl_matured_pos_tot underflow");
923 }
924 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
925 "set_reserved_pnl: pnl_matured_pos_tot > pnl_pos_tot");
926
927 self.accounts[idx].reserved_pnl = new_r;
928 }
929 }
930
931 test_visible! {
934 fn consume_released_pnl(&mut self, idx: usize, x: u128) {
935 assert!(x > 0, "consume_released_pnl: x must be > 0");
936
937 let old_pos = i128_clamp_pos(self.accounts[idx].pnl);
938 let old_r = self.accounts[idx].reserved_pnl;
939 let old_rel = old_pos - old_r;
940 assert!(x <= old_rel, "consume_released_pnl: x > ReleasedPos_i");
941
942 let new_pos = old_pos - x;
943 let new_rel = old_rel - x;
944 assert!(new_pos >= old_r, "consume_released_pnl: new_pos < old_R");
945
946 self.pnl_pos_tot = self.pnl_pos_tot.checked_sub(x)
948 .expect("consume_released_pnl: pnl_pos_tot underflow");
949
950 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(x)
952 .expect("consume_released_pnl: pnl_matured_pos_tot underflow");
953 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
954 "consume_released_pnl: pnl_matured_pos_tot > pnl_pos_tot");
955
956 let x_i128: i128 = x.try_into().expect("consume_released_pnl: x > i128::MAX");
958 let new_pnl = self.accounts[idx].pnl.checked_sub(x_i128)
959 .expect("consume_released_pnl: PNL underflow");
960 assert!(new_pnl != i128::MIN, "consume_released_pnl: PNL == i128::MIN");
961 self.accounts[idx].pnl = new_pnl;
962 }
964 }
965
966 test_visible! {
968 fn set_capital(&mut self, idx: usize, new_capital: u128) {
969 let old = self.accounts[idx].capital.get();
970 if new_capital >= old {
971 let delta = new_capital - old;
972 self.c_tot = U128::new(self.c_tot.get().checked_add(delta)
973 .expect("set_capital: c_tot overflow"));
974 } else {
975 let delta = old - new_capital;
976 self.c_tot = U128::new(self.c_tot.get().checked_sub(delta)
977 .expect("set_capital: c_tot underflow"));
978 }
979 self.accounts[idx].capital = U128::new(new_capital);
980 }
981 }
982
983 test_visible! {
985 fn set_position_basis_q(&mut self, idx: usize, new_basis: i128) {
986 let old = self.accounts[idx].position_basis_q;
987 let old_side = side_of_i128(old);
988 let new_side = side_of_i128(new_basis);
989
990 if let Some(s) = old_side {
992 match s {
993 Side::Long => {
994 self.stored_pos_count_long = self.stored_pos_count_long
995 .checked_sub(1).expect("set_position_basis_q: long count underflow");
996 }
997 Side::Short => {
998 self.stored_pos_count_short = self.stored_pos_count_short
999 .checked_sub(1).expect("set_position_basis_q: short count underflow");
1000 }
1001 }
1002 }
1003
1004 if let Some(s) = new_side {
1006 match s {
1007 Side::Long => {
1008 self.stored_pos_count_long = self.stored_pos_count_long
1009 .checked_add(1).expect("set_position_basis_q: long count overflow");
1010 }
1011 Side::Short => {
1012 self.stored_pos_count_short = self.stored_pos_count_short
1013 .checked_add(1).expect("set_position_basis_q: short count overflow");
1014 }
1015 }
1016 }
1017
1018 self.accounts[idx].position_basis_q = new_basis;
1019 }
1020 }
1021
1022 test_visible! {
1024 fn attach_effective_position(&mut self, idx: usize, new_eff_pos_q: i128) {
1025 let old_basis = self.accounts[idx].position_basis_q;
1028 if old_basis != 0 {
1029 if let Some(old_side) = side_of_i128(old_basis) {
1030 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1031 let epoch_side = self.get_epoch_side(old_side);
1032 if epoch_snap == epoch_side {
1033 let a_basis = self.accounts[idx].adl_a_basis;
1034 if a_basis != 0 {
1035 let a_side = self.get_a_side(old_side);
1036 let abs_basis = old_basis.unsigned_abs();
1037 let product = U256::from_u128(abs_basis)
1039 .checked_mul(U256::from_u128(a_side));
1040 if let Some(p) = product {
1041 let rem = p.checked_rem(U256::from_u128(a_basis));
1042 if let Some(r) = rem {
1043 if !r.is_zero() {
1044 self.inc_phantom_dust_bound(old_side);
1045 }
1046 }
1047 }
1048 }
1049 }
1050 }
1051 }
1052
1053 if new_eff_pos_q == 0 {
1054 self.set_position_basis_q(idx, 0i128);
1055 self.accounts[idx].adl_a_basis = ADL_ONE;
1057 self.accounts[idx].adl_k_snap = 0i128;
1058 self.accounts[idx].adl_epoch_snap = 0;
1059 } else {
1060 assert!(
1062 new_eff_pos_q.unsigned_abs() <= MAX_POSITION_ABS_Q,
1063 "attach: abs(new_eff_pos_q) exceeds MAX_POSITION_ABS_Q"
1064 );
1065 let side = side_of_i128(new_eff_pos_q).expect("attach: nonzero must have side");
1066 self.set_position_basis_q(idx, new_eff_pos_q);
1067
1068 match side {
1069 Side::Long => {
1070 self.accounts[idx].adl_a_basis = self.adl_mult_long;
1071 self.accounts[idx].adl_k_snap = self.adl_coeff_long;
1072 self.accounts[idx].adl_epoch_snap = self.adl_epoch_long;
1073 }
1074 Side::Short => {
1075 self.accounts[idx].adl_a_basis = self.adl_mult_short;
1076 self.accounts[idx].adl_k_snap = self.adl_coeff_short;
1077 self.accounts[idx].adl_epoch_snap = self.adl_epoch_short;
1078 }
1079 }
1080 }
1081 }
1082 }
1083
1084 fn get_a_side(&self, s: Side) -> u128 {
1089 match s {
1090 Side::Long => self.adl_mult_long,
1091 Side::Short => self.adl_mult_short,
1092 }
1093 }
1094
1095 fn get_k_side(&self, s: Side) -> i128 {
1096 match s {
1097 Side::Long => self.adl_coeff_long,
1098 Side::Short => self.adl_coeff_short,
1099 }
1100 }
1101
1102 fn get_epoch_side(&self, s: Side) -> u64 {
1103 match s {
1104 Side::Long => self.adl_epoch_long,
1105 Side::Short => self.adl_epoch_short,
1106 }
1107 }
1108
1109 fn get_k_epoch_start(&self, s: Side) -> i128 {
1110 match s {
1111 Side::Long => self.adl_epoch_start_k_long,
1112 Side::Short => self.adl_epoch_start_k_short,
1113 }
1114 }
1115
1116 fn get_side_mode(&self, s: Side) -> SideMode {
1117 match s {
1118 Side::Long => SideMode::from_u8(self.side_mode_long),
1119 Side::Short => SideMode::from_u8(self.side_mode_short),
1120 }
1121 }
1122
1123 fn get_oi_eff(&self, s: Side) -> u128 {
1124 match s {
1125 Side::Long => self.oi_eff_long_q,
1126 Side::Short => self.oi_eff_short_q,
1127 }
1128 }
1129
1130 fn set_oi_eff(&mut self, s: Side, v: u128) {
1131 match s {
1132 Side::Long => self.oi_eff_long_q = v,
1133 Side::Short => self.oi_eff_short_q = v,
1134 }
1135 }
1136
1137 fn set_side_mode(&mut self, s: Side, m: SideMode) {
1138 match s {
1139 Side::Long => self.side_mode_long = m.to_u8(),
1140 Side::Short => self.side_mode_short = m.to_u8(),
1141 }
1142 }
1143
1144 fn set_a_side(&mut self, s: Side, v: u128) {
1145 match s {
1146 Side::Long => self.adl_mult_long = v,
1147 Side::Short => self.adl_mult_short = v,
1148 }
1149 }
1150
1151 fn set_k_side(&mut self, s: Side, v: i128) {
1152 match s {
1153 Side::Long => self.adl_coeff_long = v,
1154 Side::Short => self.adl_coeff_short = v,
1155 }
1156 }
1157
1158 fn get_stale_count(&self, s: Side) -> u64 {
1159 match s {
1160 Side::Long => self.stale_account_count_long,
1161 Side::Short => self.stale_account_count_short,
1162 }
1163 }
1164
1165 fn set_stale_count(&mut self, s: Side, v: u64) {
1166 match s {
1167 Side::Long => self.stale_account_count_long = v,
1168 Side::Short => self.stale_account_count_short = v,
1169 }
1170 }
1171
1172 fn get_stored_pos_count(&self, s: Side) -> u64 {
1173 match s {
1174 Side::Long => self.stored_pos_count_long,
1175 Side::Short => self.stored_pos_count_short,
1176 }
1177 }
1178
1179 fn inc_phantom_dust_bound(&mut self, s: Side) {
1181 match s {
1182 Side::Long => {
1183 self.phantom_dust_bound_long_q = self
1184 .phantom_dust_bound_long_q
1185 .checked_add(1u128)
1186 .expect("phantom_dust_bound_long_q overflow");
1187 }
1188 Side::Short => {
1189 self.phantom_dust_bound_short_q = self
1190 .phantom_dust_bound_short_q
1191 .checked_add(1u128)
1192 .expect("phantom_dust_bound_short_q overflow");
1193 }
1194 }
1195 }
1196
1197 fn inc_phantom_dust_bound_by(&mut self, s: Side, amount_q: u128) {
1199 match s {
1200 Side::Long => {
1201 self.phantom_dust_bound_long_q = self
1202 .phantom_dust_bound_long_q
1203 .checked_add(amount_q)
1204 .expect("phantom_dust_bound_long_q overflow");
1205 }
1206 Side::Short => {
1207 self.phantom_dust_bound_short_q = self
1208 .phantom_dust_bound_short_q
1209 .checked_add(amount_q)
1210 .expect("phantom_dust_bound_short_q overflow");
1211 }
1212 }
1213 }
1214
1215 pub fn effective_pos_q(&self, idx: usize) -> i128 {
1221 let basis = self.accounts[idx].position_basis_q;
1222 if basis == 0 {
1223 return 0i128;
1224 }
1225
1226 let side = side_of_i128(basis).unwrap();
1227 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1228 let epoch_side = self.get_epoch_side(side);
1229
1230 if epoch_snap != epoch_side {
1231 return 0i128;
1233 }
1234
1235 let a_side = self.get_a_side(side);
1236 let a_basis = self.accounts[idx].adl_a_basis;
1237
1238 if a_basis == 0 {
1239 return 0i128;
1240 }
1241
1242 let abs_basis = basis.unsigned_abs();
1243 let effective_abs = mul_div_floor_u128(abs_basis, a_side, a_basis);
1245
1246 if basis < 0 {
1247 if effective_abs == 0 {
1248 0i128
1249 } else {
1250 assert!(
1251 effective_abs <= i128::MAX as u128,
1252 "effective_pos_q: overflow"
1253 );
1254 -(effective_abs as i128)
1255 }
1256 } else {
1257 assert!(
1258 effective_abs <= i128::MAX as u128,
1259 "effective_pos_q: overflow"
1260 );
1261 effective_abs as i128
1262 }
1263 }
1264
1265 test_visible! {
1270 fn settle_side_effects(&mut self, idx: usize) -> Result<()> {
1271 let basis = self.accounts[idx].position_basis_q;
1272 if basis == 0 {
1273 return Ok(());
1274 }
1275
1276 let side = side_of_i128(basis).unwrap();
1277 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1278 let epoch_side = self.get_epoch_side(side);
1279 let a_basis = self.accounts[idx].adl_a_basis;
1280
1281 if a_basis == 0 {
1282 return Err(RiskError::CorruptState);
1283 }
1284
1285 let abs_basis = basis.unsigned_abs();
1286
1287 if epoch_snap == epoch_side {
1288 let a_side = self.get_a_side(side);
1290 let k_side = self.get_k_side(side);
1291 let k_snap = self.accounts[idx].adl_k_snap;
1292
1293 let q_eff_new = mul_div_floor_u128(abs_basis, a_side, a_basis);
1295
1296 let old_r = self.accounts[idx].reserved_pnl;
1298
1299 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1301 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_side, den);
1302
1303 let old_pnl = self.accounts[idx].pnl;
1304 let new_pnl = old_pnl.checked_add(pnl_delta).ok_or(RiskError::Overflow)?;
1305 if new_pnl == i128::MIN {
1306 return Err(RiskError::Overflow);
1307 }
1308 self.set_pnl(idx, new_pnl);
1309
1310 if self.accounts[idx].reserved_pnl > old_r {
1312 self.restart_warmup_after_reserve_increase(idx);
1313 }
1314
1315 if q_eff_new == 0 {
1316 self.inc_phantom_dust_bound(side);
1319 self.set_position_basis_q(idx, 0i128);
1320 self.accounts[idx].adl_a_basis = ADL_ONE;
1321 self.accounts[idx].adl_k_snap = 0i128;
1322 self.accounts[idx].adl_epoch_snap = 0;
1323 } else {
1324 self.accounts[idx].adl_k_snap = k_side;
1326 self.accounts[idx].adl_epoch_snap = epoch_side;
1327 }
1328 } else {
1329 let side_mode = self.get_side_mode(side);
1331 if side_mode != SideMode::ResetPending {
1332 return Err(RiskError::CorruptState);
1333 }
1334 if epoch_snap.checked_add(1) != Some(epoch_side) {
1335 return Err(RiskError::CorruptState);
1336 }
1337
1338 let k_epoch_start = self.get_k_epoch_start(side);
1339 let k_snap = self.accounts[idx].adl_k_snap;
1340
1341 let old_r = self.accounts[idx].reserved_pnl;
1343
1344 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1346 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_epoch_start, den);
1347
1348 let old_pnl = self.accounts[idx].pnl;
1349 let new_pnl = old_pnl.checked_add(pnl_delta).ok_or(RiskError::Overflow)?;
1350 if new_pnl == i128::MIN {
1351 return Err(RiskError::Overflow);
1352 }
1353 self.set_pnl(idx, new_pnl);
1354
1355 if self.accounts[idx].reserved_pnl > old_r {
1357 self.restart_warmup_after_reserve_increase(idx);
1358 }
1359
1360 self.set_position_basis_q(idx, 0i128);
1361
1362 let old_stale = self.get_stale_count(side);
1364 let new_stale = old_stale.checked_sub(1).ok_or(RiskError::CorruptState)?;
1365 self.set_stale_count(side, new_stale);
1366
1367 self.accounts[idx].adl_a_basis = ADL_ONE;
1369 self.accounts[idx].adl_k_snap = 0i128;
1370 self.accounts[idx].adl_epoch_snap = 0;
1371 }
1372
1373 Ok(())
1374 }
1375 }
1376
1377 pub fn accrue_market_to(&mut self, now_slot: u64, oracle_price: u64) -> Result<()> {
1382 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
1383 return Err(RiskError::Overflow);
1384 }
1385
1386 if now_slot < self.current_slot {
1388 return Err(RiskError::Overflow);
1389 }
1390 if now_slot < self.last_market_slot {
1391 return Err(RiskError::Overflow);
1392 }
1393
1394 let long_live = self.oi_eff_long_q != 0;
1396 let short_live = self.oi_eff_short_q != 0;
1397
1398 let total_dt = now_slot.saturating_sub(self.last_market_slot);
1399 if total_dt == 0 && self.last_oracle_price == oracle_price {
1400 self.current_slot = now_slot;
1402 return Ok(());
1403 }
1404
1405 let current_price = self.last_oracle_price;
1407 let delta_p = (oracle_price as i128)
1408 .checked_sub(current_price as i128)
1409 .ok_or(RiskError::Overflow)?;
1410 if delta_p != 0 {
1411 if long_live {
1412 let delta_k = checked_u128_mul_i128(self.adl_mult_long, delta_p)?;
1413 self.adl_coeff_long = self
1414 .adl_coeff_long
1415 .checked_add(delta_k)
1416 .ok_or(RiskError::Overflow)?;
1417 }
1418 if short_live {
1419 let delta_k = checked_u128_mul_i128(self.adl_mult_short, delta_p)?;
1420 self.adl_coeff_short = self
1421 .adl_coeff_short
1422 .checked_sub(delta_k)
1423 .ok_or(RiskError::Overflow)?;
1424 }
1425 }
1426
1427 let r_last = self.funding_rate_bps_per_slot_last;
1429 if r_last != 0 && total_dt > 0 && long_live && short_live {
1430 let fund_px_0 = self.funding_price_sample_last;
1433
1434 if fund_px_0 > 0 {
1435 let mut dt_remaining = total_dt;
1436
1437 while dt_remaining > 0 {
1438 let dt_sub = core::cmp::min(dt_remaining, MAX_FUNDING_DT);
1439 dt_remaining -= dt_sub;
1440
1441 let fund_num: i128 = (fund_px_0 as i128)
1443 .checked_mul(r_last as i128)
1444 .ok_or(RiskError::Overflow)?
1445 .checked_mul(dt_sub as i128)
1446 .ok_or(RiskError::Overflow)?;
1447
1448 let fund_term = floor_div_signed_conservative_i128(fund_num, 10_000u128);
1450
1451 if fund_term != 0 {
1452 let delta_k_long = checked_u128_mul_i128(self.adl_mult_long, fund_term)?;
1454 self.adl_coeff_long = self
1455 .adl_coeff_long
1456 .checked_sub(delta_k_long)
1457 .ok_or(RiskError::Overflow)?;
1458
1459 let delta_k_short = checked_u128_mul_i128(self.adl_mult_short, fund_term)?;
1461 self.adl_coeff_short = self
1462 .adl_coeff_short
1463 .checked_add(delta_k_short)
1464 .ok_or(RiskError::Overflow)?;
1465 }
1466 }
1467 }
1468 }
1469
1470 self.current_slot = now_slot;
1472 self.last_market_slot = now_slot;
1473 self.last_oracle_price = oracle_price;
1474 self.funding_price_sample_last = oracle_price;
1475
1476 Ok(())
1477 }
1478
1479 test_visible! {
1483 fn recompute_r_last_from_final_state(&mut self, externally_computed_rate: i64) -> Result<()> {
1484 if externally_computed_rate.unsigned_abs() > MAX_ABS_FUNDING_BPS_PER_SLOT as u64 {
1485 return Err(RiskError::Overflow);
1486 }
1487 self.funding_rate_bps_per_slot_last = externally_computed_rate;
1488 Ok(())
1489 }
1490 }
1491
1492 pub fn run_end_of_instruction_lifecycle(
1500 &mut self,
1501 ctx: &mut InstructionContext,
1502 funding_rate: i64,
1503 ) -> Result<()> {
1504 self.schedule_end_of_instruction_resets(ctx)?;
1505 self.finalize_end_of_instruction_resets(ctx);
1506 self.recompute_r_last_from_final_state(funding_rate)?;
1507 Ok(())
1508 }
1509
1510 fn use_insurance_buffer(&mut self, loss: u128) -> u128 {
1517 if loss == 0 {
1518 return 0;
1519 }
1520 let ins_bal = self.insurance_fund.balance.get();
1521 let available = ins_bal.saturating_sub(self.insurance_floor);
1522 let pay = core::cmp::min(loss, available);
1523 if pay > 0 {
1524 self.insurance_fund.balance = U128::new(ins_bal - pay);
1525 }
1526 loss - pay
1527 }
1528
1529 test_visible! {
1532 fn absorb_protocol_loss(&mut self, loss: u128) {
1533 if loss == 0 {
1534 return;
1535 }
1536 let _rem = self.use_insurance_buffer(loss);
1537 }
1539 }
1540
1541 test_visible! {
1546 fn enqueue_adl(&mut self, ctx: &mut InstructionContext, liq_side: Side, q_close_q: u128, d: u128) -> Result<()> {
1547 let opp = opposite_side(liq_side);
1548
1549 if q_close_q != 0 {
1551 let old_oi = self.get_oi_eff(liq_side);
1552 let new_oi = old_oi.checked_sub(q_close_q).ok_or(RiskError::CorruptState)?;
1553 self.set_oi_eff(liq_side, new_oi);
1554 }
1555
1556 let d_rem = if d > 0 { self.use_insurance_buffer(d) } else { 0u128 };
1558
1559 let oi = self.get_oi_eff(opp);
1561
1562 if oi == 0 {
1564 if self.get_oi_eff(liq_side) == 0 {
1566 set_pending_reset(ctx, liq_side);
1567 set_pending_reset(ctx, opp);
1568 }
1569 return Ok(());
1570 }
1571
1572 if self.get_stored_pos_count(opp) == 0 {
1575 if q_close_q > oi {
1576 return Err(RiskError::CorruptState);
1577 }
1578 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1579 self.set_oi_eff(opp, oi_post);
1581 if oi_post == 0 {
1582 set_pending_reset(ctx, opp);
1584 if self.get_oi_eff(liq_side) == 0 {
1586 set_pending_reset(ctx, liq_side);
1587 }
1588 }
1589 return Ok(());
1590 }
1591
1592 if q_close_q > oi {
1594 return Err(RiskError::CorruptState);
1595 }
1596
1597 let a_old = self.get_a_side(opp);
1598 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1599
1600 if d_rem != 0 {
1605 let a_ps = a_old.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1606 match wide_mul_div_ceil_u128_or_over_i128max(d_rem, a_ps, oi) {
1607 Ok(delta_k_abs) => {
1608 let delta_k = -(delta_k_abs as i128);
1609 let k_opp = self.get_k_side(opp);
1610 match k_opp.checked_add(delta_k) {
1611 Some(new_k) => {
1612 self.set_k_side(opp, new_k);
1613 }
1614 None => {
1615 }
1617 }
1618 }
1619 Err(OverI128Magnitude) => {
1620 }
1622 }
1623 }
1624
1625 if oi_post == 0 {
1627 self.set_oi_eff(opp, 0u128);
1628 set_pending_reset(ctx, opp);
1629 if self.get_oi_eff(liq_side) == 0 {
1630 set_pending_reset(ctx, liq_side);
1631 }
1632 return Ok(());
1633 }
1634
1635 let a_old_u256 = U256::from_u128(a_old);
1637 let oi_post_u256 = U256::from_u128(oi_post);
1638 let oi_u256 = U256::from_u128(oi);
1639 let (a_candidate_u256, a_trunc_rem) = mul_div_floor_u256_with_rem(
1640 a_old_u256,
1641 oi_post_u256,
1642 oi_u256,
1643 );
1644
1645 if !a_candidate_u256.is_zero() {
1647 let a_new = a_candidate_u256.try_into_u128().expect("A_candidate exceeds u128");
1648 self.set_a_side(opp, a_new);
1649 self.set_oi_eff(opp, oi_post);
1650 if !a_trunc_rem.is_zero() {
1652 let n_opp = self.get_stored_pos_count(opp) as u128;
1653 let n_opp_u256 = U256::from_u128(n_opp);
1654 let oi_plus_n = oi_u256.checked_add(n_opp_u256).unwrap_or(U256::MAX);
1656 let ceil_term = ceil_div_positive_checked(oi_plus_n, a_old_u256);
1657 let global_a_dust_bound = n_opp_u256.checked_add(ceil_term)
1658 .unwrap_or(U256::MAX);
1659 let bound_u128 = global_a_dust_bound.try_into_u128().unwrap_or(u128::MAX);
1660 self.inc_phantom_dust_bound_by(opp, bound_u128);
1661 }
1662 if a_new < MIN_A_SIDE {
1663 self.set_side_mode(opp, SideMode::DrainOnly);
1664 }
1665 return Ok(());
1666 }
1667
1668 self.set_oi_eff(opp, 0u128);
1670 self.set_oi_eff(liq_side, 0u128);
1671 set_pending_reset(ctx, opp);
1672 set_pending_reset(ctx, liq_side);
1673
1674 Ok(())
1675 }
1676 }
1677
1678 test_visible! {
1683 fn begin_full_drain_reset(&mut self, side: Side) {
1684 assert!(self.get_oi_eff(side) == 0, "begin_full_drain_reset: OI not zero");
1686
1687 let k = self.get_k_side(side);
1689 match side {
1690 Side::Long => self.adl_epoch_start_k_long = k,
1691 Side::Short => self.adl_epoch_start_k_short = k,
1692 }
1693
1694 match side {
1696 Side::Long => self.adl_epoch_long = self.adl_epoch_long.checked_add(1)
1697 .expect("epoch overflow"),
1698 Side::Short => self.adl_epoch_short = self.adl_epoch_short.checked_add(1)
1699 .expect("epoch overflow"),
1700 }
1701
1702 self.set_a_side(side, ADL_ONE);
1704
1705 let spc = self.get_stored_pos_count(side);
1707 self.set_stale_count(side, spc);
1708
1709 match side {
1711 Side::Long => self.phantom_dust_bound_long_q = 0u128,
1712 Side::Short => self.phantom_dust_bound_short_q = 0u128,
1713 }
1714
1715 self.set_side_mode(side, SideMode::ResetPending);
1717 }
1718 }
1719
1720 test_visible! {
1721 fn finalize_side_reset(&mut self, side: Side) -> Result<()> {
1722 if self.get_side_mode(side) != SideMode::ResetPending {
1723 return Err(RiskError::CorruptState);
1724 }
1725 if self.get_oi_eff(side) != 0 {
1726 return Err(RiskError::CorruptState);
1727 }
1728 if self.get_stale_count(side) != 0 {
1729 return Err(RiskError::CorruptState);
1730 }
1731 if self.get_stored_pos_count(side) != 0 {
1732 return Err(RiskError::CorruptState);
1733 }
1734 self.set_side_mode(side, SideMode::Normal);
1735 Ok(())
1736 }
1737 }
1738
1739 test_visible! {
1744 fn schedule_end_of_instruction_resets(&mut self, ctx: &mut InstructionContext) -> Result<()> {
1745 if self.stored_pos_count_long == 0 && self.stored_pos_count_short == 0 {
1747 let clear_bound_q = self.phantom_dust_bound_long_q
1748 .checked_add(self.phantom_dust_bound_short_q)
1749 .ok_or(RiskError::CorruptState)?;
1750 let has_residual = self.oi_eff_long_q != 0
1751 || self.oi_eff_short_q != 0
1752 || self.phantom_dust_bound_long_q != 0
1753 || self.phantom_dust_bound_short_q != 0;
1754 if has_residual {
1755 if self.oi_eff_long_q != self.oi_eff_short_q {
1756 return Err(RiskError::CorruptState);
1757 }
1758 if self.oi_eff_long_q <= clear_bound_q && self.oi_eff_short_q <= clear_bound_q {
1759 self.oi_eff_long_q = 0u128;
1760 self.oi_eff_short_q = 0u128;
1761 ctx.pending_reset_long = true;
1762 ctx.pending_reset_short = true;
1763 } else {
1764 return Err(RiskError::CorruptState);
1765 }
1766 }
1767 }
1768 else if self.stored_pos_count_long == 0 && self.stored_pos_count_short > 0 {
1770 let has_residual = self.oi_eff_long_q != 0
1771 || self.oi_eff_short_q != 0
1772 || self.phantom_dust_bound_long_q != 0;
1773 if has_residual {
1774 if self.oi_eff_long_q != self.oi_eff_short_q {
1775 return Err(RiskError::CorruptState);
1776 }
1777 if self.oi_eff_long_q <= self.phantom_dust_bound_long_q {
1778 self.oi_eff_long_q = 0u128;
1779 self.oi_eff_short_q = 0u128;
1780 ctx.pending_reset_long = true;
1781 ctx.pending_reset_short = true;
1782 } else {
1783 return Err(RiskError::CorruptState);
1784 }
1785 }
1786 }
1787 else if self.stored_pos_count_short == 0 && self.stored_pos_count_long > 0 {
1789 let has_residual = self.oi_eff_long_q != 0
1790 || self.oi_eff_short_q != 0
1791 || self.phantom_dust_bound_short_q != 0;
1792 if has_residual {
1793 if self.oi_eff_long_q != self.oi_eff_short_q {
1794 return Err(RiskError::CorruptState);
1795 }
1796 if self.oi_eff_short_q <= self.phantom_dust_bound_short_q {
1797 self.oi_eff_long_q = 0u128;
1798 self.oi_eff_short_q = 0u128;
1799 ctx.pending_reset_long = true;
1800 ctx.pending_reset_short = true;
1801 } else {
1802 return Err(RiskError::CorruptState);
1803 }
1804 }
1805 }
1806
1807 if self.side_mode_long == SideMode::DRAIN_ONLY && self.oi_eff_long_q == 0 {
1809 ctx.pending_reset_long = true;
1810 }
1811 if self.side_mode_short == SideMode::DRAIN_ONLY && self.oi_eff_short_q == 0 {
1812 ctx.pending_reset_short = true;
1813 }
1814
1815 Ok(())
1816 }
1817 }
1818
1819 test_visible! {
1820 fn finalize_end_of_instruction_resets(&mut self, ctx: &InstructionContext) {
1821 if ctx.pending_reset_long && self.side_mode_long != SideMode::RESET_PENDING {
1822 self.begin_full_drain_reset(Side::Long);
1823 }
1824 if ctx.pending_reset_short && self.side_mode_short != SideMode::RESET_PENDING {
1825 self.begin_full_drain_reset(Side::Short);
1826 }
1827 self.maybe_finalize_ready_reset_sides();
1829 }
1830 }
1831
1832 fn maybe_finalize_ready_reset_sides(&mut self) {
1836 if self.side_mode_long == SideMode::RESET_PENDING
1837 && self.get_oi_eff(Side::Long) == 0
1838 && self.get_stale_count(Side::Long) == 0
1839 && self.get_stored_pos_count(Side::Long) == 0
1840 {
1841 self.set_side_mode(Side::Long, SideMode::Normal);
1842 }
1843 if self.side_mode_short == SideMode::RESET_PENDING
1844 && self.get_oi_eff(Side::Short) == 0
1845 && self.get_stale_count(Side::Short) == 0
1846 && self.get_stored_pos_count(Side::Short) == 0
1847 {
1848 self.set_side_mode(Side::Short, SideMode::Normal);
1849 }
1850 }
1851
1852 pub fn haircut_ratio(&self) -> (u128, u128) {
1859 if self.pnl_matured_pos_tot == 0 {
1860 return (1u128, 1u128);
1861 }
1862 let senior_sum = self
1863 .c_tot
1864 .get()
1865 .checked_add(self.insurance_fund.balance.get());
1866 let residual: u128 = match senior_sum {
1867 Some(ss) => {
1868 if self.vault.get() >= ss {
1869 self.vault.get() - ss
1870 } else {
1871 0u128
1872 }
1873 }
1874 None => 0u128, };
1876 let h_num = if residual < self.pnl_matured_pos_tot {
1877 residual
1878 } else {
1879 self.pnl_matured_pos_tot
1880 };
1881 (h_num, self.pnl_matured_pos_tot)
1882 }
1883
1884 pub fn effective_matured_pnl(&self, idx: usize) -> u128 {
1886 let released = self.released_pos(idx);
1887 if released == 0 {
1888 return 0u128;
1889 }
1890 let (h_num, h_den) = self.haircut_ratio();
1891 if h_den == 0 {
1892 return released;
1893 }
1894 wide_mul_div_floor_u128(released, h_num, h_den)
1895 }
1896
1897 pub fn account_equity_maint_raw(&self, account: &Account) -> i128 {
1903 let wide = self.account_equity_maint_raw_wide(account);
1904 match wide.try_into_i128() {
1905 Some(v) => v,
1906 None => {
1907 if wide.is_negative() {
1912 i128::MIN + 1
1913 } else {
1914 i128::MAX
1915 }
1916 }
1917 }
1918 }
1919
1920 pub fn account_equity_maint_raw_wide(&self, account: &Account) -> I256 {
1924 let cap = I256::from_u128(account.capital.get());
1925 let pnl = I256::from_i128(account.pnl);
1926 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1927
1928 let sum = cap.checked_add(pnl).expect("I256 add overflow");
1930 sum.checked_sub(fee_debt).expect("I256 sub overflow")
1931 }
1932
1933 pub fn account_equity_net(&self, account: &Account, _oracle_price: u64) -> i128 {
1935 let raw = self.account_equity_maint_raw(account);
1936 if raw < 0 {
1937 0i128
1938 } else {
1939 raw
1940 }
1941 }
1942
1943 pub fn account_equity_init_raw(&self, account: &Account, idx: usize) -> i128 {
1947 let cap = I256::from_u128(account.capital.get());
1948 let neg_pnl = I256::from_i128(if account.pnl < 0 { account.pnl } else { 0i128 });
1949 let eff_matured = I256::from_u128(self.effective_matured_pnl(idx));
1950 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1951
1952 let sum = cap
1953 .checked_add(neg_pnl)
1954 .expect("I256 add overflow")
1955 .checked_add(eff_matured)
1956 .expect("I256 add overflow")
1957 .checked_sub(fee_debt)
1958 .expect("I256 sub overflow");
1959
1960 match sum.try_into_i128() {
1961 Some(v) => v,
1962 None => {
1963 if sum.is_negative() {
1967 i128::MIN + 1
1968 } else {
1969 i128::MAX
1970 }
1971 }
1972 }
1973 }
1974
1975 pub fn account_equity_init_net(&self, account: &Account, idx: usize) -> i128 {
1977 let raw = self.account_equity_init_raw(account, idx);
1978 if raw < 0 {
1979 0i128
1980 } else {
1981 raw
1982 }
1983 }
1984
1985 pub fn notional(&self, idx: usize, oracle_price: u64) -> u128 {
1987 let eff = self.effective_pos_q(idx);
1988 if eff == 0 {
1989 return 0;
1990 }
1991 let abs_eff = eff.unsigned_abs();
1992 mul_div_floor_u128(abs_eff, oracle_price as u128, POS_SCALE)
1993 }
1994
1995 pub fn is_above_maintenance_margin(
1998 &self,
1999 account: &Account,
2000 idx: usize,
2001 oracle_price: u64,
2002 ) -> bool {
2003 let eq_net = self.account_equity_net(account, oracle_price);
2004 let eff = self.effective_pos_q(idx);
2005 if eff == 0 {
2006 return eq_net > 0;
2007 }
2008 let not = self.notional(idx, oracle_price);
2009 let proportional =
2010 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000);
2011 let mm_req = core::cmp::max(proportional, self.params.min_nonzero_mm_req);
2012 let mm_req_i128 = if mm_req > i128::MAX as u128 {
2013 i128::MAX
2014 } else {
2015 mm_req as i128
2016 };
2017 eq_net > mm_req_i128
2018 }
2019
2020 pub fn is_above_initial_margin(
2025 &self,
2026 account: &Account,
2027 idx: usize,
2028 oracle_price: u64,
2029 ) -> bool {
2030 let eq_init_raw = self.account_equity_init_raw(account, idx);
2031 let eff = self.effective_pos_q(idx);
2032 if eff == 0 {
2033 return eq_init_raw >= 0;
2034 }
2035 let not = self.notional(idx, oracle_price);
2036 let proportional = mul_div_floor_u128(not, self.params.initial_margin_bps as u128, 10_000);
2037 let im_req = core::cmp::max(proportional, self.params.min_nonzero_im_req);
2038 let im_req_i128 = if im_req > i128::MAX as u128 {
2039 i128::MAX
2040 } else {
2041 im_req as i128
2042 };
2043 eq_init_raw >= im_req_i128
2044 }
2045
2046 pub fn check_conservation(&self) -> bool {
2051 let senior = self
2052 .c_tot
2053 .get()
2054 .checked_add(self.insurance_fund.balance.get());
2055 match senior {
2056 Some(s) => self.vault.get() >= s,
2057 None => false,
2058 }
2059 }
2060
2061 pub fn released_pos(&self, idx: usize) -> u128 {
2067 let pnl = self.accounts[idx].pnl;
2068 let pos_pnl = i128_clamp_pos(pnl);
2069 pos_pnl.saturating_sub(self.accounts[idx].reserved_pnl)
2070 }
2071
2072 test_visible! {
2075 fn restart_warmup_after_reserve_increase(&mut self, idx: usize) {
2076 let t = self.params.warmup_period_slots;
2077 if t == 0 {
2078 self.set_reserved_pnl(idx, 0);
2080 self.accounts[idx].warmup_slope_per_step = 0;
2081 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2082 return;
2083 }
2084 let r = self.accounts[idx].reserved_pnl;
2085 if r == 0 {
2086 self.accounts[idx].warmup_slope_per_step = 0;
2087 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2088 return;
2089 }
2090 let base = r / (t as u128);
2092 let slope = if base == 0 { 1u128 } else { base };
2093 self.accounts[idx].warmup_slope_per_step = slope;
2094 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2095 }
2096 }
2097
2098 test_visible! {
2100 fn advance_profit_warmup(&mut self, idx: usize) {
2101 let r = self.accounts[idx].reserved_pnl;
2102 if r == 0 {
2103 self.accounts[idx].warmup_slope_per_step = 0;
2104 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2105 return;
2106 }
2107 let t = self.params.warmup_period_slots;
2108 if t == 0 {
2109 self.set_reserved_pnl(idx, 0);
2110 self.accounts[idx].warmup_slope_per_step = 0;
2111 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2112 return;
2113 }
2114 let elapsed = self.current_slot.saturating_sub(self.accounts[idx].warmup_started_at_slot);
2115 let cap = saturating_mul_u128_u64(self.accounts[idx].warmup_slope_per_step, elapsed);
2116 let release = core::cmp::min(r, cap);
2117 if release > 0 {
2118 self.set_reserved_pnl(idx, r - release);
2119 }
2120 if self.accounts[idx].reserved_pnl == 0 {
2121 self.accounts[idx].warmup_slope_per_step = 0;
2122 }
2123 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2124 }
2125 }
2126
2127 fn settle_losses(&mut self, idx: usize) {
2133 let pnl = self.accounts[idx].pnl;
2134 if pnl >= 0 {
2135 return;
2136 }
2137 assert!(pnl != i128::MIN, "settle_losses: i128::MIN");
2138 let need = pnl.unsigned_abs();
2139 let cap = self.accounts[idx].capital.get();
2140 let pay = core::cmp::min(need, cap);
2141 if pay > 0 {
2142 self.set_capital(idx, cap - pay);
2143 let pay_i128 = pay as i128; let new_pnl = pnl.checked_add(pay_i128).unwrap_or(0i128);
2145 if new_pnl == i128::MIN {
2146 self.set_pnl(idx, 0i128);
2147 } else {
2148 self.set_pnl(idx, new_pnl);
2149 }
2150 }
2151 }
2152
2153 fn resolve_flat_negative(&mut self, idx: usize) {
2155 let eff = self.effective_pos_q(idx);
2156 if eff != 0 {
2157 return; }
2159 let pnl = self.accounts[idx].pnl;
2160 if pnl < 0 {
2161 assert!(pnl != i128::MIN, "resolve_flat_negative: i128::MIN");
2162 let loss = pnl.unsigned_abs();
2163 self.absorb_protocol_loss(loss);
2164 self.set_pnl(idx, 0i128);
2165 }
2166 }
2167
2168 fn do_profit_conversion(&mut self, idx: usize) {
2171 let x = self.released_pos(idx);
2172 if x == 0 {
2173 return;
2174 }
2175
2176 let (h_num, h_den) = self.haircut_ratio();
2180 assert!(
2181 h_den > 0,
2182 "do_profit_conversion: h_den must be > 0 when x > 0"
2183 );
2184 let y: u128 = wide_mul_div_floor_u128(x, h_num, h_den);
2185
2186 self.consume_released_pnl(idx, x);
2188
2189 let new_cap = add_u128(self.accounts[idx].capital.get(), y);
2191 self.set_capital(idx, new_cap);
2192
2193 if self.accounts[idx].reserved_pnl == 0 {
2195 self.accounts[idx].warmup_slope_per_step = 0;
2196 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2197 }
2198 }
2200
2201 test_visible! {
2203 fn fee_debt_sweep(&mut self, idx: usize) {
2204 let fc = self.accounts[idx].fee_credits.get();
2205 let debt = fee_debt_u128_checked(fc);
2206 if debt == 0 {
2207 return;
2208 }
2209 let cap = self.accounts[idx].capital.get();
2210 let pay = core::cmp::min(debt, cap);
2211 if pay > 0 {
2212 self.set_capital(idx, cap - pay);
2213 let pay_i128 = core::cmp::min(pay, i128::MAX as u128) as i128;
2215 self.accounts[idx].fee_credits = I128::new(self.accounts[idx].fee_credits.get()
2216 .checked_add(pay_i128).expect("fee_debt_sweep: pay <= debt guarantees no overflow"));
2217 self.insurance_fund.balance = self.insurance_fund.balance + pay;
2218 }
2219 }
2223 }
2224
2225 pub fn touch_account_full(
2230 &mut self,
2231 idx: usize,
2232 oracle_price: u64,
2233 now_slot: u64,
2234 ) -> Result<()> {
2235 if idx >= MAX_ACCOUNTS || !self.is_used(idx) {
2237 return Err(RiskError::AccountNotFound);
2238 }
2239 if now_slot < self.current_slot {
2241 return Err(RiskError::Overflow);
2242 }
2243 if now_slot < self.last_market_slot {
2244 return Err(RiskError::Overflow);
2245 }
2246 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2247 return Err(RiskError::Overflow);
2248 }
2249
2250 self.current_slot = now_slot;
2252
2253 self.accrue_market_to(now_slot, oracle_price)?;
2255
2256 self.advance_profit_warmup(idx);
2258
2259 self.settle_side_effects(idx)?;
2261
2262 self.settle_losses(idx);
2264
2265 if self.effective_pos_q(idx) == 0 && self.accounts[idx].pnl < 0 {
2267 self.resolve_flat_negative(idx);
2268 }
2269
2270 self.settle_maintenance_fee_internal(idx, now_slot)?;
2272
2273 if self.accounts[idx].position_basis_q == 0 {
2275 self.do_profit_conversion(idx);
2276 }
2277
2278 self.fee_debt_sweep(idx);
2280
2281 Ok(())
2282 }
2283
2284 fn settle_maintenance_fee_internal(&mut self, idx: usize, now_slot: u64) -> Result<()> {
2286 let fee_per_slot = self.params.maintenance_fee_per_slot.get();
2287 if fee_per_slot == 0 {
2288 self.accounts[idx].last_fee_slot = now_slot;
2289 return Ok(());
2290 }
2291
2292 let last = self.accounts[idx].last_fee_slot;
2293 let dt_fee = now_slot.saturating_sub(last);
2294 if dt_fee == 0 {
2295 self.accounts[idx].last_fee_slot = now_slot;
2296 return Ok(());
2297 }
2298
2299 let fee_due = (dt_fee as u128)
2301 .checked_mul(fee_per_slot)
2302 .ok_or(RiskError::Overflow)?;
2303
2304 if fee_due > MAX_PROTOCOL_FEE_ABS {
2306 return Err(RiskError::Overflow);
2307 }
2308
2309 self.accounts[idx].last_fee_slot = now_slot;
2311
2312 if fee_due > 0 {
2314 self.charge_fee_to_insurance(idx, fee_due)?;
2315 }
2316
2317 Ok(())
2318 }
2319
2320 test_visible! {
2325 fn add_user(&mut self, fee_payment: u128) -> Result<u16> {
2326 let used_count = self.num_used_accounts as u64;
2327 if used_count >= self.params.max_accounts {
2328 return Err(RiskError::Overflow);
2329 }
2330
2331 let required_fee = self.params.new_account_fee.get();
2332 if fee_payment < required_fee {
2333 return Err(RiskError::InsufficientBalance);
2334 }
2335
2336 let v_candidate = self.vault.get().checked_add(fee_payment)
2338 .ok_or(RiskError::Overflow)?;
2339 if v_candidate > MAX_VAULT_TVL {
2340 return Err(RiskError::Overflow);
2341 }
2342
2343 self.materialized_account_count = self.materialized_account_count
2346 .checked_add(1).ok_or(RiskError::Overflow)?;
2347 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2348 self.materialized_account_count -= 1;
2349 return Err(RiskError::Overflow);
2350 }
2351
2352 let idx = match self.alloc_slot() {
2353 Ok(i) => i,
2354 Err(e) => {
2355 self.materialized_account_count -= 1;
2356 return Err(e);
2357 }
2358 };
2359
2360 let excess = fee_payment.saturating_sub(required_fee);
2362 self.vault = U128::new(v_candidate);
2363 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2364
2365 let account_id = self.next_account_id;
2366 self.next_account_id = self.next_account_id.saturating_add(1);
2367
2368 self.accounts[idx as usize] = Account {
2369 kind: Account::KIND_USER,
2370 account_id,
2371 capital: U128::new(excess),
2372 pnl: 0i128,
2373 reserved_pnl: 0u128,
2374 warmup_started_at_slot: self.current_slot,
2375 warmup_slope_per_step: 0u128,
2376 position_basis_q: 0i128,
2377 adl_a_basis: ADL_ONE,
2378 adl_k_snap: 0i128,
2379 adl_epoch_snap: 0,
2380 matcher_program: [0; 32],
2381 matcher_context: [0; 32],
2382 owner: [0; 32],
2383 fee_credits: I128::ZERO,
2384 last_fee_slot: self.current_slot,
2385 fees_earned_total: U128::ZERO,
2386 };
2387
2388 if excess > 0 {
2389 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2390 .ok_or(RiskError::Overflow)?);
2391 }
2392
2393 Ok(idx)
2394 }
2395 }
2396
2397 test_visible! {
2398 fn add_lp(
2399 &mut self,
2400 matching_engine_program: [u8; 32],
2401 matching_engine_context: [u8; 32],
2402 fee_payment: u128,
2403 ) -> Result<u16> {
2404 let used_count = self.num_used_accounts as u64;
2405 if used_count >= self.params.max_accounts {
2406 return Err(RiskError::Overflow);
2407 }
2408
2409 let required_fee = self.params.new_account_fee.get();
2410 if fee_payment < required_fee {
2411 return Err(RiskError::InsufficientBalance);
2412 }
2413
2414 let v_candidate = self.vault.get().checked_add(fee_payment)
2416 .ok_or(RiskError::Overflow)?;
2417 if v_candidate > MAX_VAULT_TVL {
2418 return Err(RiskError::Overflow);
2419 }
2420
2421 self.materialized_account_count = self.materialized_account_count
2423 .checked_add(1).ok_or(RiskError::Overflow)?;
2424 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2425 self.materialized_account_count -= 1;
2426 return Err(RiskError::Overflow);
2427 }
2428
2429 let idx = match self.alloc_slot() {
2430 Ok(i) => i,
2431 Err(e) => {
2432 self.materialized_account_count -= 1;
2433 return Err(e);
2434 }
2435 };
2436
2437 let excess = fee_payment.saturating_sub(required_fee);
2439 self.vault = U128::new(v_candidate);
2440 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2441
2442 let account_id = self.next_account_id;
2443 self.next_account_id = self.next_account_id.saturating_add(1);
2444
2445 self.accounts[idx as usize] = Account {
2446 kind: Account::KIND_LP,
2447 account_id,
2448 capital: U128::new(excess),
2449 pnl: 0i128,
2450 reserved_pnl: 0u128,
2451 warmup_started_at_slot: self.current_slot,
2452 warmup_slope_per_step: 0u128,
2453 position_basis_q: 0i128,
2454 adl_a_basis: ADL_ONE,
2455 adl_k_snap: 0i128,
2456 adl_epoch_snap: 0,
2457 matcher_program: matching_engine_program,
2458 matcher_context: matching_engine_context,
2459 owner: [0; 32],
2460 fee_credits: I128::ZERO,
2461 last_fee_slot: self.current_slot,
2462 fees_earned_total: U128::ZERO,
2463 };
2464
2465 if excess > 0 {
2466 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2467 .ok_or(RiskError::Overflow)?);
2468 }
2469
2470 Ok(idx)
2471 }
2472 }
2473
2474 pub fn set_owner(&mut self, idx: u16, owner: [u8; 32]) -> Result<()> {
2475 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
2476 return Err(RiskError::Unauthorized);
2477 }
2478 if self.accounts[idx as usize].owner != [0u8; 32] {
2482 return Err(RiskError::Unauthorized);
2483 }
2484 self.accounts[idx as usize].owner = owner;
2485 Ok(())
2486 }
2487
2488 pub fn deposit(
2493 &mut self,
2494 idx: u16,
2495 amount: u128,
2496 _oracle_price: u64,
2497 now_slot: u64,
2498 ) -> Result<()> {
2499 if now_slot < self.current_slot {
2501 return Err(RiskError::Overflow);
2502 }
2503 if now_slot < self.last_market_slot {
2504 return Err(RiskError::Overflow);
2505 }
2506
2507 if !self.is_used(idx as usize) {
2510 let min_dep = self.params.min_initial_deposit.get();
2511 if amount < min_dep {
2512 return Err(RiskError::InsufficientBalance);
2513 }
2514 self.materialize_at(idx, now_slot)?;
2515 }
2516
2517 self.current_slot = now_slot;
2519
2520 let v_candidate = self
2522 .vault
2523 .get()
2524 .checked_add(amount)
2525 .ok_or(RiskError::Overflow)?;
2526 if v_candidate > MAX_VAULT_TVL {
2527 return Err(RiskError::Overflow);
2528 }
2529 self.vault = U128::new(v_candidate);
2530
2531 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), amount);
2533 self.set_capital(idx as usize, new_cap);
2534
2535 self.settle_losses(idx as usize);
2537
2538 if self.accounts[idx as usize].position_basis_q == 0 && self.accounts[idx as usize].pnl >= 0
2547 {
2548 self.fee_debt_sweep(idx as usize);
2549 }
2550
2551 Ok(())
2552 }
2553
2554 pub fn withdraw(
2559 &mut self,
2560 idx: u16,
2561 amount: u128,
2562 oracle_price: u64,
2563 now_slot: u64,
2564 funding_rate: i64,
2565 ) -> Result<()> {
2566 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2567 return Err(RiskError::Overflow);
2568 }
2569
2570 if !self.is_used(idx as usize) {
2575 return Err(RiskError::AccountNotFound);
2576 }
2577
2578 let mut ctx = InstructionContext::new();
2579
2580 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
2582
2583 if self.accounts[idx as usize].capital.get() < amount {
2585 return Err(RiskError::InsufficientBalance);
2586 }
2587
2588 let post_cap = self.accounts[idx as usize].capital.get() - amount;
2590 if post_cap != 0 && post_cap < self.params.min_initial_deposit.get() {
2591 return Err(RiskError::InsufficientBalance);
2592 }
2593
2594 let eff = self.effective_pos_q(idx as usize);
2596 if eff != 0 {
2597 let old_cap = self.accounts[idx as usize].capital.get();
2599 let old_vault = self.vault;
2600 self.set_capital(idx as usize, post_cap);
2601 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2602 let passes_im = self.is_above_initial_margin(
2603 &self.accounts[idx as usize],
2604 idx as usize,
2605 oracle_price,
2606 );
2607 self.set_capital(idx as usize, old_cap);
2609 self.vault = old_vault;
2610 if !passes_im {
2611 return Err(RiskError::Undercollateralized);
2612 }
2613 }
2614
2615 self.set_capital(
2617 idx as usize,
2618 self.accounts[idx as usize].capital.get() - amount,
2619 );
2620 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2621
2622 self.schedule_end_of_instruction_resets(&mut ctx)?;
2624 self.finalize_end_of_instruction_resets(&ctx);
2625 self.recompute_r_last_from_final_state(funding_rate)?;
2626
2627 Ok(())
2628 }
2629
2630 pub fn settle_account(
2637 &mut self,
2638 idx: u16,
2639 oracle_price: u64,
2640 now_slot: u64,
2641 funding_rate: i64,
2642 ) -> Result<()> {
2643 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2644 return Err(RiskError::Overflow);
2645 }
2646 if !self.is_used(idx as usize) {
2647 return Err(RiskError::AccountNotFound);
2648 }
2649
2650 let mut ctx = InstructionContext::new();
2651
2652 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
2654
2655 self.schedule_end_of_instruction_resets(&mut ctx)?;
2657 self.finalize_end_of_instruction_resets(&ctx);
2658 self.recompute_r_last_from_final_state(funding_rate)?;
2659
2660 assert!(
2662 self.oi_eff_long_q == self.oi_eff_short_q,
2663 "OI_eff_long != OI_eff_short after settle"
2664 );
2665
2666 Ok(())
2667 }
2668
2669 pub fn execute_trade(
2674 &mut self,
2675 a: u16,
2676 b: u16,
2677 oracle_price: u64,
2678 now_slot: u64,
2679 size_q: i128,
2680 exec_price: u64,
2681 funding_rate: i64,
2682 ) -> Result<()> {
2683 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2684 return Err(RiskError::Overflow);
2685 }
2686 if exec_price == 0 || exec_price > MAX_ORACLE_PRICE {
2687 return Err(RiskError::Overflow);
2688 }
2689 if size_q <= 0 {
2691 return Err(RiskError::Overflow);
2692 }
2693 if size_q as u128 > MAX_TRADE_SIZE_Q {
2694 return Err(RiskError::Overflow);
2695 }
2696
2697 let trade_notional_check =
2699 mul_div_floor_u128(size_q as u128, exec_price as u128, POS_SCALE);
2700 if trade_notional_check > MAX_ACCOUNT_NOTIONAL {
2701 return Err(RiskError::Overflow);
2702 }
2703
2704 if !self.is_used(a as usize) || !self.is_used(b as usize) {
2709 return Err(RiskError::AccountNotFound);
2710 }
2711 if a == b {
2712 return Err(RiskError::Overflow);
2713 }
2714
2715 let mut ctx = InstructionContext::new();
2716
2717 self.touch_account_full(a as usize, oracle_price, now_slot)?;
2719 self.touch_account_full(b as usize, oracle_price, now_slot)?;
2720
2721 let old_eff_a = self.effective_pos_q(a as usize);
2723 let old_eff_b = self.effective_pos_q(b as usize);
2724
2725 let mm_req_pre_a = if old_eff_a == 0 {
2728 0u128
2729 } else {
2730 let not = self.notional(a as usize, oracle_price);
2731 core::cmp::max(
2732 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
2733 self.params.min_nonzero_mm_req,
2734 )
2735 };
2736 let mm_req_pre_b = if old_eff_b == 0 {
2737 0u128
2738 } else {
2739 let not = self.notional(b as usize, oracle_price);
2740 core::cmp::max(
2741 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
2742 self.params.min_nonzero_mm_req,
2743 )
2744 };
2745 let maint_raw_wide_pre_a = self.account_equity_maint_raw_wide(&self.accounts[a as usize]);
2746 let maint_raw_wide_pre_b = self.account_equity_maint_raw_wide(&self.accounts[b as usize]);
2747 let buffer_pre_a = maint_raw_wide_pre_a
2748 .checked_sub(I256::from_u128(mm_req_pre_a))
2749 .expect("I256 sub");
2750 let buffer_pre_b = maint_raw_wide_pre_b
2751 .checked_sub(I256::from_u128(mm_req_pre_b))
2752 .expect("I256 sub");
2753
2754 let new_eff_a = old_eff_a.checked_add(size_q).ok_or(RiskError::Overflow)?;
2756 let neg_size_q = size_q.checked_neg().ok_or(RiskError::Overflow)?;
2757 let new_eff_b = old_eff_b
2758 .checked_add(neg_size_q)
2759 .ok_or(RiskError::Overflow)?;
2760
2761 if new_eff_a != 0 && new_eff_a.unsigned_abs() > MAX_POSITION_ABS_Q {
2763 return Err(RiskError::Overflow);
2764 }
2765 if new_eff_b != 0 && new_eff_b.unsigned_abs() > MAX_POSITION_ABS_Q {
2766 return Err(RiskError::Overflow);
2767 }
2768
2769 {
2771 let notional_a =
2772 mul_div_floor_u128(new_eff_a.unsigned_abs(), oracle_price as u128, POS_SCALE);
2773 if notional_a > MAX_ACCOUNT_NOTIONAL {
2774 return Err(RiskError::Overflow);
2775 }
2776 let notional_b =
2777 mul_div_floor_u128(new_eff_b.unsigned_abs(), oracle_price as u128, POS_SCALE);
2778 if notional_b > MAX_ACCOUNT_NOTIONAL {
2779 return Err(RiskError::Overflow);
2780 }
2781 }
2782
2783 self.maybe_finalize_ready_reset_sides();
2786
2787 self.check_side_mode_for_trade(&old_eff_a, &new_eff_a, &old_eff_b, &new_eff_b)?;
2789
2790 let price_diff = (oracle_price as i128) - (exec_price as i128);
2792 let trade_pnl_a = compute_trade_pnl(size_q, price_diff)?;
2793 let trade_pnl_b = trade_pnl_a.checked_neg().ok_or(RiskError::Overflow)?;
2794
2795 let old_r_a = self.accounts[a as usize].reserved_pnl;
2796 let old_r_b = self.accounts[b as usize].reserved_pnl;
2797
2798 let pnl_a = self.accounts[a as usize]
2799 .pnl
2800 .checked_add(trade_pnl_a)
2801 .ok_or(RiskError::Overflow)?;
2802 if pnl_a == i128::MIN {
2803 return Err(RiskError::Overflow);
2804 }
2805 self.set_pnl(a as usize, pnl_a);
2806
2807 let pnl_b = self.accounts[b as usize]
2808 .pnl
2809 .checked_add(trade_pnl_b)
2810 .ok_or(RiskError::Overflow)?;
2811 if pnl_b == i128::MIN {
2812 return Err(RiskError::Overflow);
2813 }
2814 self.set_pnl(b as usize, pnl_b);
2815
2816 if self.accounts[a as usize].reserved_pnl > old_r_a {
2818 self.restart_warmup_after_reserve_increase(a as usize);
2819 }
2820 if self.accounts[b as usize].reserved_pnl > old_r_b {
2821 self.restart_warmup_after_reserve_increase(b as usize);
2822 }
2823
2824 self.attach_effective_position(a as usize, new_eff_a);
2826 self.attach_effective_position(b as usize, new_eff_b);
2827
2828 self.update_oi_from_positions(&old_eff_a, &new_eff_a, &old_eff_b, &new_eff_b)?;
2830
2831 self.settle_losses(a as usize);
2834 self.settle_losses(b as usize);
2835
2836 let trade_notional =
2838 mul_div_floor_u128(size_q.unsigned_abs(), exec_price as u128, POS_SCALE);
2839 let fee = if trade_notional > 0 && self.params.trading_fee_bps > 0 {
2840 mul_div_ceil_u128(trade_notional, self.params.trading_fee_bps as u128, 10_000)
2841 } else {
2842 0
2843 };
2844
2845 if fee > 0 {
2847 if fee > MAX_PROTOCOL_FEE_ABS {
2848 return Err(RiskError::Overflow);
2849 }
2850 self.charge_fee_to_insurance(a as usize, fee)?;
2851 self.charge_fee_to_insurance(b as usize, fee)?;
2852 }
2853
2854 if self.accounts[a as usize].is_lp() {
2856 self.accounts[a as usize].fees_earned_total = U128::new(add_u128(
2857 self.accounts[a as usize].fees_earned_total.get(),
2858 fee,
2859 ));
2860 }
2861 if self.accounts[b as usize].is_lp() {
2862 self.accounts[b as usize].fees_earned_total = U128::new(add_u128(
2863 self.accounts[b as usize].fees_earned_total.get(),
2864 fee,
2865 ));
2866 }
2867
2868 self.enforce_post_trade_margin(
2870 a as usize,
2871 b as usize,
2872 oracle_price,
2873 &old_eff_a,
2874 &new_eff_a,
2875 &old_eff_b,
2876 &new_eff_b,
2877 buffer_pre_a,
2878 buffer_pre_b,
2879 fee,
2880 )?;
2881
2882 self.schedule_end_of_instruction_resets(&mut ctx)?;
2884 self.finalize_end_of_instruction_resets(&ctx);
2885
2886 self.recompute_r_last_from_final_state(funding_rate)?;
2888
2889 assert!(
2891 self.oi_eff_long_q == self.oi_eff_short_q,
2892 "OI_eff_long != OI_eff_short after trade"
2893 );
2894
2895 Ok(())
2896 }
2897
2898 fn charge_fee_to_insurance(&mut self, idx: usize, fee: u128) -> Result<()> {
2901 if fee > MAX_PROTOCOL_FEE_ABS {
2902 return Err(RiskError::Overflow);
2903 }
2904 let cap = self.accounts[idx].capital.get();
2905 let fee_paid = core::cmp::min(fee, cap);
2906 if fee_paid > 0 {
2907 self.set_capital(idx, cap - fee_paid);
2908 self.insurance_fund.balance = self.insurance_fund.balance + fee_paid;
2909 }
2910 let fee_shortfall = fee - fee_paid;
2911 if fee_shortfall > 0 {
2912 let current_fc = self.accounts[idx].fee_credits.get();
2916 let headroom = match current_fc.checked_add(i128::MAX) {
2918 Some(h) if h > 0 => h as u128,
2919 _ => 0u128, };
2921 let collectible = core::cmp::min(fee_shortfall, headroom);
2922 if collectible > 0 {
2923 let new_fc = current_fc - (collectible as i128);
2926 self.accounts[idx].fee_credits = I128::new(new_fc);
2927 }
2928 }
2930 Ok(())
2931 }
2932
2933 fn oi_long_component(pos: i128) -> u128 {
2935 if pos > 0 {
2936 pos as u128
2937 } else {
2938 0u128
2939 }
2940 }
2941
2942 fn oi_short_component(pos: i128) -> u128 {
2943 if pos < 0 {
2944 pos.unsigned_abs()
2945 } else {
2946 0u128
2947 }
2948 }
2949
2950 fn bilateral_oi_after(
2953 &self,
2954 old_a: &i128,
2955 new_a: &i128,
2956 old_b: &i128,
2957 new_b: &i128,
2958 ) -> Result<(u128, u128)> {
2959 let oi_long_after = self
2960 .oi_eff_long_q
2961 .checked_sub(Self::oi_long_component(*old_a))
2962 .ok_or(RiskError::CorruptState)?
2963 .checked_sub(Self::oi_long_component(*old_b))
2964 .ok_or(RiskError::CorruptState)?
2965 .checked_add(Self::oi_long_component(*new_a))
2966 .ok_or(RiskError::Overflow)?
2967 .checked_add(Self::oi_long_component(*new_b))
2968 .ok_or(RiskError::Overflow)?;
2969
2970 let oi_short_after = self
2971 .oi_eff_short_q
2972 .checked_sub(Self::oi_short_component(*old_a))
2973 .ok_or(RiskError::CorruptState)?
2974 .checked_sub(Self::oi_short_component(*old_b))
2975 .ok_or(RiskError::CorruptState)?
2976 .checked_add(Self::oi_short_component(*new_a))
2977 .ok_or(RiskError::Overflow)?
2978 .checked_add(Self::oi_short_component(*new_b))
2979 .ok_or(RiskError::Overflow)?;
2980
2981 Ok((oi_long_after, oi_short_after))
2982 }
2983
2984 fn check_side_mode_for_trade(
2987 &self,
2988 old_a: &i128,
2989 new_a: &i128,
2990 old_b: &i128,
2991 new_b: &i128,
2992 ) -> Result<()> {
2993 let (oi_long_after, oi_short_after) =
2994 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
2995
2996 for &side in &[Side::Long, Side::Short] {
2997 let mode = self.get_side_mode(side);
2998 if mode != SideMode::DrainOnly && mode != SideMode::ResetPending {
2999 continue;
3000 }
3001 let (oi_after, oi_before) = match side {
3002 Side::Long => (oi_long_after, self.oi_eff_long_q),
3003 Side::Short => (oi_short_after, self.oi_eff_short_q),
3004 };
3005 if oi_after > oi_before {
3006 return Err(RiskError::SideBlocked);
3007 }
3008 }
3009 Ok(())
3010 }
3011
3012 fn enforce_post_trade_margin(
3015 &self,
3016 a: usize,
3017 b: usize,
3018 oracle_price: u64,
3019 old_eff_a: &i128,
3020 new_eff_a: &i128,
3021 old_eff_b: &i128,
3022 new_eff_b: &i128,
3023 buffer_pre_a: I256,
3024 buffer_pre_b: I256,
3025 fee: u128,
3026 ) -> Result<()> {
3027 self.enforce_one_side_margin(a, oracle_price, old_eff_a, new_eff_a, buffer_pre_a, fee)?;
3028 self.enforce_one_side_margin(b, oracle_price, old_eff_b, new_eff_b, buffer_pre_b, fee)?;
3029 Ok(())
3030 }
3031
3032 fn enforce_one_side_margin(
3033 &self,
3034 idx: usize,
3035 oracle_price: u64,
3036 old_eff: &i128,
3037 new_eff: &i128,
3038 buffer_pre: I256,
3039 fee: u128,
3040 ) -> Result<()> {
3041 if *new_eff == 0 {
3042 let maint_raw = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3045 if maint_raw.is_negative() {
3046 return Err(RiskError::Undercollateralized);
3047 }
3048 return Ok(());
3049 }
3050
3051 let abs_old: u128 = if *old_eff == 0 {
3052 0u128
3053 } else {
3054 old_eff.unsigned_abs()
3055 };
3056 let abs_new = new_eff.unsigned_abs();
3057
3058 let risk_increasing = abs_new > abs_old
3060 || (*old_eff > 0 && *new_eff < 0)
3061 || (*old_eff < 0 && *new_eff > 0)
3062 || *old_eff == 0;
3063
3064 let strictly_reducing = *old_eff != 0
3066 && *new_eff != 0
3067 && ((*old_eff > 0 && *new_eff > 0) || (*old_eff < 0 && *new_eff < 0))
3068 && abs_new < abs_old;
3069
3070 if risk_increasing {
3071 if !self.is_above_initial_margin(&self.accounts[idx], idx, oracle_price) {
3073 return Err(RiskError::Undercollateralized);
3074 }
3075 } else if self.is_above_maintenance_margin(&self.accounts[idx], idx, oracle_price) {
3076 } else if strictly_reducing {
3078 let maint_raw_wide_post = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3083 let fee_wide = I256::from_u128(fee);
3084
3085 let maint_raw_fee_neutral =
3087 maint_raw_wide_post.checked_add(fee_wide).expect("I256 add");
3088 let mm_req_post = {
3089 let not = self.notional(idx, oracle_price);
3090 core::cmp::max(
3091 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
3092 self.params.min_nonzero_mm_req,
3093 )
3094 };
3095 let buffer_post_fee_neutral = maint_raw_fee_neutral
3096 .checked_sub(I256::from_u128(mm_req_post))
3097 .expect("I256 sub");
3098
3099 let mm_req_pre = {
3101 let not_pre = if *old_eff == 0 {
3102 0u128
3103 } else {
3104 mul_div_floor_u128(old_eff.unsigned_abs(), oracle_price as u128, POS_SCALE)
3105 };
3106 core::cmp::max(
3107 mul_div_floor_u128(not_pre, self.params.maintenance_margin_bps as u128, 10_000),
3108 self.params.min_nonzero_mm_req,
3109 )
3110 };
3111 let maint_raw_pre = buffer_pre
3112 .checked_add(I256::from_u128(mm_req_pre))
3113 .expect("I256 add");
3114
3115 let cond1 = buffer_post_fee_neutral > buffer_pre;
3117
3118 let zero = I256::from_i128(0);
3121 let shortfall_post = if maint_raw_fee_neutral < zero {
3122 maint_raw_fee_neutral
3123 } else {
3124 zero
3125 };
3126 let shortfall_pre = if maint_raw_pre < zero {
3127 maint_raw_pre
3128 } else {
3129 zero
3130 };
3131 let cond2 = shortfall_post >= shortfall_pre;
3132
3133 if cond1 && cond2 {
3134 } else {
3136 return Err(RiskError::Undercollateralized);
3137 }
3138 } else {
3139 return Err(RiskError::Undercollateralized);
3140 }
3141 Ok(())
3142 }
3143
3144 fn update_oi_from_positions(
3147 &mut self,
3148 old_a: &i128,
3149 new_a: &i128,
3150 old_b: &i128,
3151 new_b: &i128,
3152 ) -> Result<()> {
3153 let (oi_long_after, oi_short_after) =
3154 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
3155
3156 if oi_long_after > MAX_OI_SIDE_Q {
3158 return Err(RiskError::Overflow);
3159 }
3160 if oi_short_after > MAX_OI_SIDE_Q {
3161 return Err(RiskError::Overflow);
3162 }
3163
3164 self.oi_eff_long_q = oi_long_after;
3165 self.oi_eff_short_q = oi_short_after;
3166
3167 Ok(())
3168 }
3169
3170 pub fn liquidate_at_oracle(
3177 &mut self,
3178 idx: u16,
3179 now_slot: u64,
3180 oracle_price: u64,
3181 policy: LiquidationPolicy,
3182 funding_rate: i64,
3183 ) -> Result<bool> {
3184 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3187 return Ok(false);
3188 }
3189
3190 let mut ctx = InstructionContext::new();
3191
3192 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3194
3195 let result =
3196 self.liquidate_at_oracle_internal(idx, now_slot, oracle_price, policy, &mut ctx)?;
3197
3198 self.schedule_end_of_instruction_resets(&mut ctx)?;
3201 self.finalize_end_of_instruction_resets(&ctx);
3202 self.recompute_r_last_from_final_state(funding_rate)?;
3203
3204 assert!(
3206 self.oi_eff_long_q == self.oi_eff_short_q,
3207 "OI_eff_long != OI_eff_short after liquidation"
3208 );
3209 Ok(result)
3210 }
3211
3212 fn liquidate_at_oracle_internal(
3216 &mut self,
3217 idx: u16,
3218 _now_slot: u64,
3219 oracle_price: u64,
3220 policy: LiquidationPolicy,
3221 ctx: &mut InstructionContext,
3222 ) -> Result<bool> {
3223 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3224 return Ok(false);
3225 }
3226
3227 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3228 return Err(RiskError::Overflow);
3229 }
3230
3231 let old_eff = self.effective_pos_q(idx as usize);
3233 if old_eff == 0 {
3234 return Ok(false);
3235 }
3236
3237 if self.is_above_maintenance_margin(
3239 &self.accounts[idx as usize],
3240 idx as usize,
3241 oracle_price,
3242 ) {
3243 return Ok(false);
3244 }
3245
3246 let liq_side = side_of_i128(old_eff).unwrap();
3247 let abs_old_eff = old_eff.unsigned_abs();
3248
3249 match policy {
3250 LiquidationPolicy::ExactPartial(q_close_q) => {
3251 if q_close_q == 0 || q_close_q >= abs_old_eff {
3254 return Err(RiskError::Overflow);
3255 }
3256 let new_eff_abs_q = abs_old_eff
3258 .checked_sub(q_close_q)
3259 .ok_or(RiskError::Overflow)?;
3260 if new_eff_abs_q == 0 {
3262 return Err(RiskError::Overflow);
3263 }
3264 let sign = if old_eff > 0 { 1i128 } else { -1i128 };
3266 let new_eff = sign
3267 .checked_mul(new_eff_abs_q as i128)
3268 .ok_or(RiskError::Overflow)?;
3269
3270 self.attach_effective_position(idx as usize, new_eff);
3272
3273 self.settle_losses(idx as usize);
3275
3276 let liq_fee = {
3278 let notional_val =
3279 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3280 let liq_fee_raw = mul_div_ceil_u128(
3281 notional_val,
3282 self.params.liquidation_fee_bps as u128,
3283 10_000,
3284 );
3285 core::cmp::min(
3286 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3287 self.params.liquidation_fee_cap.get(),
3288 )
3289 };
3290 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3291
3292 self.enqueue_adl(ctx, liq_side, q_close_q, 0)?;
3294
3295 if !self.is_above_maintenance_margin(
3301 &self.accounts[idx as usize],
3302 idx as usize,
3303 oracle_price,
3304 ) {
3305 return Err(RiskError::Undercollateralized);
3306 }
3307
3308 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3309 Ok(true)
3310 }
3311 LiquidationPolicy::FullClose => {
3312 let q_close_q = abs_old_eff;
3314
3315 self.attach_effective_position(idx as usize, 0i128);
3317
3318 self.settle_losses(idx as usize);
3320
3321 let liq_fee = if q_close_q == 0 {
3323 0u128
3324 } else {
3325 let notional_val =
3326 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3327 let liq_fee_raw = mul_div_ceil_u128(
3328 notional_val,
3329 self.params.liquidation_fee_bps as u128,
3330 10_000,
3331 );
3332 core::cmp::min(
3333 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3334 self.params.liquidation_fee_cap.get(),
3335 )
3336 };
3337 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3338
3339 let eff_post = self.effective_pos_q(idx as usize);
3341 let d: u128 = if eff_post == 0 && self.accounts[idx as usize].pnl < 0 {
3342 assert!(
3343 self.accounts[idx as usize].pnl != i128::MIN,
3344 "liquidate: i128::MIN pnl"
3345 );
3346 self.accounts[idx as usize].pnl.unsigned_abs()
3347 } else {
3348 0u128
3349 };
3350
3351 if q_close_q != 0 || d != 0 {
3353 self.enqueue_adl(ctx, liq_side, q_close_q, d)?;
3354 }
3355
3356 if d != 0 {
3358 self.set_pnl(idx as usize, 0i128);
3359 }
3360
3361 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3362 Ok(true)
3363 }
3364 }
3365 }
3366
3367 pub fn keeper_crank(
3375 &mut self,
3376 now_slot: u64,
3377 oracle_price: u64,
3378 ordered_candidates: &[(u16, Option<LiquidationPolicy>)],
3379 max_revalidations: u16,
3380 funding_rate: i64,
3381 ) -> Result<CrankOutcome> {
3382 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3383 return Err(RiskError::Overflow);
3384 }
3385
3386 let mut ctx = InstructionContext::new();
3388
3389 if now_slot < self.current_slot {
3391 return Err(RiskError::Overflow);
3392 }
3393 if now_slot < self.last_market_slot {
3394 return Err(RiskError::Overflow);
3395 }
3396
3397 self.accrue_market_to(now_slot, oracle_price)?;
3399
3400 self.current_slot = now_slot;
3402
3403 let advanced = now_slot > self.last_crank_slot;
3404 if advanced {
3405 self.last_crank_slot = now_slot;
3406 }
3407
3408 let mut attempts: u16 = 0;
3410 let mut num_liquidations: u32 = 0;
3411
3412 for &(candidate_idx, ref hint) in ordered_candidates {
3413 if attempts >= max_revalidations {
3415 break;
3416 }
3417 if ctx.pending_reset_long || ctx.pending_reset_short {
3419 break;
3420 }
3421 if (candidate_idx as usize) >= MAX_ACCOUNTS || !self.is_used(candidate_idx as usize) {
3423 continue;
3424 }
3425
3426 attempts += 1;
3428 let cidx = candidate_idx as usize;
3429
3430 self.advance_profit_warmup(cidx);
3435
3436 self.settle_side_effects(cidx)?;
3438
3439 self.settle_losses(cidx);
3441
3442 if self.effective_pos_q(cidx) == 0 && self.accounts[cidx].pnl < 0 {
3444 self.resolve_flat_negative(cidx);
3445 }
3446
3447 self.settle_maintenance_fee_internal(cidx, now_slot)?;
3449
3450 if self.accounts[cidx].position_basis_q == 0 {
3452 self.do_profit_conversion(cidx);
3453 }
3454
3455 self.fee_debt_sweep(cidx);
3457
3458 if !ctx.pending_reset_long && !ctx.pending_reset_short {
3461 let eff = self.effective_pos_q(cidx);
3462 if eff != 0 {
3463 if !self.is_above_maintenance_margin(&self.accounts[cidx], cidx, oracle_price) {
3464 if let Some(policy) =
3468 self.validate_keeper_hint(candidate_idx, eff, hint, oracle_price)
3469 {
3470 match self.liquidate_at_oracle_internal(
3471 candidate_idx,
3472 now_slot,
3473 oracle_price,
3474 policy,
3475 &mut ctx,
3476 ) {
3477 Ok(true) => {
3478 num_liquidations += 1;
3479 }
3480 Ok(false) => {}
3481 Err(e) => return Err(e),
3482 }
3483 }
3484 }
3485 }
3486 }
3487 }
3488
3489 self.schedule_end_of_instruction_resets(&mut ctx)?;
3491 self.finalize_end_of_instruction_resets(&ctx);
3492
3493 self.recompute_r_last_from_final_state(funding_rate)?;
3495
3496 assert!(
3498 self.oi_eff_long_q == self.oi_eff_short_q,
3499 "OI_eff_long != OI_eff_short after keeper_crank"
3500 );
3501
3502 Ok(CrankOutcome {
3503 advanced,
3504 slots_forgiven: 0,
3505 caller_settle_ok: true,
3506 force_realize_needed: false,
3507 panic_needed: false,
3508 num_liquidations,
3509 num_liq_errors: 0,
3510 num_gc_closed: 0,
3511 last_cursor: 0,
3512 sweep_complete: false,
3513 })
3514 }
3515
3516 test_visible! {
3526 fn validate_keeper_hint(
3527 &self,
3528 idx: u16,
3529 eff: i128,
3530 hint: &Option<LiquidationPolicy>,
3531 oracle_price: u64,
3532 ) -> Option<LiquidationPolicy> {
3533 match hint {
3534 None => None,
3536 Some(LiquidationPolicy::FullClose) => Some(LiquidationPolicy::FullClose),
3537 Some(LiquidationPolicy::ExactPartial(q_close_q)) => {
3538 let abs_eff = eff.unsigned_abs();
3539 if *q_close_q == 0 || *q_close_q >= abs_eff {
3542 return None;
3543 }
3544
3545 let account = &self.accounts[idx as usize];
3547
3548 let notional_closed = mul_div_floor_u128(*q_close_q, oracle_price as u128, POS_SCALE);
3550 let liq_fee_raw = mul_div_ceil_u128(notional_closed, self.params.liquidation_fee_bps as u128, 10_000);
3551 let liq_fee = core::cmp::min(
3552 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3553 self.params.liquidation_fee_cap.get(),
3554 );
3555
3556 let cap = account.capital.get();
3560 let fee_from_capital = core::cmp::min(liq_fee, cap);
3561 let fee_shortfall = liq_fee - fee_from_capital;
3562 let current_fc = account.fee_credits.get();
3563 let fc_headroom = match current_fc.checked_add(i128::MAX) {
3564 Some(h) if h > 0 => h as u128,
3565 _ => 0u128,
3566 };
3567 let fee_from_debt = core::cmp::min(fee_shortfall, fc_headroom);
3568 let fee_applied = fee_from_capital + fee_from_debt;
3569
3570 let eq_raw_wide = self.account_equity_maint_raw_wide(account);
3571 let predicted_eq = match eq_raw_wide.checked_sub(I256::from_u128(fee_applied)) {
3572 Some(v) => v,
3573 None => return None,
3574 };
3575
3576 let rem_eff = abs_eff - *q_close_q;
3578 let rem_notional = mul_div_floor_u128(rem_eff, oracle_price as u128, POS_SCALE);
3579 let proportional_mm = mul_div_floor_u128(rem_notional, self.params.maintenance_margin_bps as u128, 10_000);
3580 let predicted_mm_req = if rem_eff == 0 {
3581 0u128
3582 } else {
3583 core::cmp::max(proportional_mm, self.params.min_nonzero_mm_req)
3584 };
3585
3586 if predicted_eq <= I256::from_u128(predicted_mm_req) {
3589 return None;
3590 }
3591
3592 Some(LiquidationPolicy::ExactPartial(*q_close_q))
3593 }
3594 }
3595 }
3596 }
3597
3598 pub fn convert_released_pnl(
3604 &mut self,
3605 idx: u16,
3606 x_req: u128,
3607 oracle_price: u64,
3608 now_slot: u64,
3609 funding_rate: i64,
3610 ) -> Result<()> {
3611 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3612 return Err(RiskError::Overflow);
3613 }
3614 if !self.is_used(idx as usize) {
3615 return Err(RiskError::AccountNotFound);
3616 }
3617
3618 let mut ctx = InstructionContext::new();
3619
3620 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3622
3623 if self.accounts[idx as usize].position_basis_q == 0 {
3625 self.schedule_end_of_instruction_resets(&mut ctx)?;
3626 self.finalize_end_of_instruction_resets(&ctx);
3627 self.recompute_r_last_from_final_state(funding_rate)?;
3628 return Ok(());
3629 }
3630
3631 let released = self.released_pos(idx as usize);
3633 if x_req == 0 || x_req > released {
3634 return Err(RiskError::Overflow);
3635 }
3636
3637 let (h_num, h_den) = self.haircut_ratio();
3640 assert!(
3641 h_den > 0,
3642 "convert_released_pnl: h_den must be > 0 when x_req > 0"
3643 );
3644 let y: u128 = wide_mul_div_floor_u128(x_req, h_num, h_den);
3645
3646 self.consume_released_pnl(idx as usize, x_req);
3648
3649 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), y);
3651 self.set_capital(idx as usize, new_cap);
3652
3653 self.fee_debt_sweep(idx as usize);
3655
3656 let eff = self.effective_pos_q(idx as usize);
3658 if eff != 0 {
3659 if !self.is_above_maintenance_margin(
3660 &self.accounts[idx as usize],
3661 idx as usize,
3662 oracle_price,
3663 ) {
3664 return Err(RiskError::Undercollateralized);
3665 }
3666 }
3667
3668 self.schedule_end_of_instruction_resets(&mut ctx)?;
3670 self.finalize_end_of_instruction_resets(&ctx);
3671 self.recompute_r_last_from_final_state(funding_rate)?;
3672
3673 Ok(())
3674 }
3675
3676 pub fn close_account(
3681 &mut self,
3682 idx: u16,
3683 now_slot: u64,
3684 oracle_price: u64,
3685 funding_rate: i64,
3686 ) -> Result<u128> {
3687 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3688 return Err(RiskError::AccountNotFound);
3689 }
3690
3691 let mut ctx = InstructionContext::new();
3692
3693 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3694
3695 let eff = self.effective_pos_q(idx as usize);
3697 if eff != 0 {
3698 return Err(RiskError::Undercollateralized);
3699 }
3700
3701 if self.accounts[idx as usize].pnl > 0 {
3704 return Err(RiskError::PnlNotWarmedUp);
3705 }
3706 if self.accounts[idx as usize].pnl < 0 {
3707 return Err(RiskError::Undercollateralized);
3708 }
3709
3710 if self.accounts[idx as usize].fee_credits.get() < 0 {
3712 self.accounts[idx as usize].fee_credits = I128::ZERO;
3713 }
3714
3715 let capital = self.accounts[idx as usize].capital;
3716
3717 if capital > self.vault {
3718 return Err(RiskError::InsufficientBalance);
3719 }
3720 self.vault = self.vault - capital;
3721 self.set_capital(idx as usize, 0);
3722
3723 self.schedule_end_of_instruction_resets(&mut ctx)?;
3725 self.finalize_end_of_instruction_resets(&ctx);
3726 self.recompute_r_last_from_final_state(funding_rate)?;
3727
3728 self.free_slot(idx);
3729
3730 Ok(capital.get())
3731 }
3732
3733 pub fn force_close_resolved(&mut self, idx: u16) -> Result<u128> {
3748 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3749 return Err(RiskError::AccountNotFound);
3750 }
3751
3752 let i = idx as usize;
3753
3754 if self.accounts[i].position_basis_q != 0 {
3756 let settle_ok = self.settle_side_effects(i).is_ok();
3758
3759 if !settle_ok {
3760 let basis = self.accounts[i].position_basis_q;
3764 let abs_basis = basis.unsigned_abs();
3765 let a_basis = self.accounts[i].adl_a_basis;
3766 let k_snap = self.accounts[i].adl_k_snap;
3767
3768 if a_basis > 0 {
3769 let side = side_of_i128(basis).unwrap();
3770 let epoch_snap = self.accounts[i].adl_epoch_snap;
3771 let epoch_side = self.get_epoch_side(side);
3772
3773 let k_end = if epoch_snap == epoch_side {
3775 self.get_k_side(side)
3776 } else {
3777 self.get_k_epoch_start(side)
3778 };
3779
3780 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
3781 let pnl_delta =
3782 wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_end, den);
3783
3784 if pnl_delta != 0 {
3785 let old_r = self.accounts[i].reserved_pnl;
3786 let new_pnl = self.accounts[i]
3787 .pnl
3788 .checked_add(pnl_delta)
3789 .ok_or(RiskError::Overflow)?;
3790 if new_pnl == i128::MIN {
3791 return Err(RiskError::Overflow);
3792 }
3793 self.set_pnl(i, new_pnl);
3794 if self.accounts[i].reserved_pnl > old_r {
3795 self.restart_warmup_after_reserve_increase(i);
3796 }
3797 }
3798
3799 if epoch_snap != epoch_side {
3801 let old_stale = self.get_stale_count(side);
3802 if old_stale > 0 {
3803 self.set_stale_count(side, old_stale - 1);
3804 }
3805 }
3806 }
3807
3808 self.set_position_basis_q(i, 0);
3810 self.accounts[i].adl_a_basis = ADL_ONE;
3811 self.accounts[i].adl_k_snap = 0;
3812 self.accounts[i].adl_epoch_snap = 0;
3813 }
3814
3815 if self.accounts[i].position_basis_q != 0 {
3818 self.set_position_basis_q(i, 0);
3819 self.accounts[i].adl_a_basis = ADL_ONE;
3820 self.accounts[i].adl_k_snap = 0;
3821 self.accounts[i].adl_epoch_snap = 0;
3822 }
3823 }
3824
3825 self.settle_losses(i);
3827
3828 self.resolve_flat_negative(i);
3830
3831 if self.accounts[i].pnl > 0 {
3833 self.set_reserved_pnl(i, 0);
3835 let pos_pnl = self.accounts[i].pnl as u128;
3837 let released = self.released_pos(i);
3838 if released > 0 {
3839 let (h_num, h_den) = self.haircut_ratio();
3840 let y = if h_den == 0 {
3841 released
3842 } else {
3843 wide_mul_div_floor_u128(released, h_num, h_den)
3844 };
3845 self.consume_released_pnl(i, released);
3846 let new_cap = add_u128(self.accounts[i].capital.get(), y);
3847 self.set_capital(i, new_cap);
3848 }
3849 }
3853
3854 self.fee_debt_sweep(i);
3856
3857 if self.accounts[i].fee_credits.get() < 0 {
3859 self.accounts[i].fee_credits = I128::ZERO;
3860 }
3861
3862 let capital = self.accounts[i].capital;
3864 if capital > self.vault {
3865 return Err(RiskError::InsufficientBalance);
3866 }
3867 self.vault = self.vault - capital;
3868 self.set_capital(i, 0);
3869
3870 self.free_slot(idx);
3871
3872 Ok(capital.get())
3873 }
3874
3875 pub fn reclaim_empty_account(&mut self, idx: u16, now_slot: u64) -> Result<()> {
3884 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3885 return Err(RiskError::AccountNotFound);
3886 }
3887 if now_slot < self.current_slot {
3888 return Err(RiskError::Overflow);
3889 }
3890
3891 let account = &self.accounts[idx as usize];
3893 if account.position_basis_q != 0 {
3894 return Err(RiskError::Undercollateralized);
3895 }
3896 if account.pnl != 0 {
3897 return Err(RiskError::Undercollateralized);
3898 }
3899 if account.reserved_pnl != 0 {
3900 return Err(RiskError::Undercollateralized);
3901 }
3902 if account.fee_credits.get() > 0 {
3903 return Err(RiskError::Undercollateralized);
3904 }
3905
3906 self.current_slot = now_slot;
3908
3909 self.settle_maintenance_fee_internal(idx as usize, now_slot)?;
3911
3912 if self.accounts[idx as usize].capital.get() >= self.params.min_initial_deposit.get()
3915 && !self.accounts[idx as usize].capital.is_zero()
3916 {
3917 return Err(RiskError::Undercollateralized);
3918 }
3919
3920 let dust_cap = self.accounts[idx as usize].capital.get();
3922 if dust_cap > 0 {
3923 self.set_capital(idx as usize, 0);
3924 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
3925 }
3926
3927 if self.accounts[idx as usize].fee_credits.get() < 0 {
3929 self.accounts[idx as usize].fee_credits = I128::new(0);
3930 }
3931
3932 self.free_slot(idx);
3934
3935 Ok(())
3936 }
3937
3938 test_visible! {
3943 fn garbage_collect_dust(&mut self) -> u32 {
3944 let mut to_free: [u16; GC_CLOSE_BUDGET as usize] = [0; GC_CLOSE_BUDGET as usize];
3945 let mut num_to_free = 0usize;
3946
3947 let max_scan = (ACCOUNTS_PER_CRANK as usize).min(MAX_ACCOUNTS);
3948 let start = self.gc_cursor as usize;
3949
3950 let mut scanned: usize = 0;
3951 for offset in 0..max_scan {
3952 if num_to_free >= GC_CLOSE_BUDGET as usize {
3953 break;
3954 }
3955 scanned = offset + 1;
3956
3957 let idx = (start + offset) & ACCOUNT_IDX_MASK;
3958 let block = idx >> 6;
3959 let bit = idx & 63;
3960 if (self.used[block] & (1u64 << bit)) == 0 {
3961 continue;
3962 }
3963
3964 let account = &self.accounts[idx];
3971 if account.position_basis_q != 0 {
3972 continue;
3973 }
3974 if account.capital.get() >= self.params.min_initial_deposit.get()
3976 && !account.capital.is_zero() {
3977 continue;
3978 }
3979 if account.reserved_pnl != 0 {
3980 continue;
3981 }
3982 if account.pnl != 0 {
3985 continue;
3986 }
3987 if account.fee_credits.get() > 0 {
3988 continue;
3989 }
3990
3991 let dust_cap = self.accounts[idx].capital.get();
3993 if dust_cap > 0 {
3994 self.set_capital(idx, 0);
3995 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
3996 }
3997
3998 if self.accounts[idx].fee_credits.get() < 0 {
4000 self.accounts[idx].fee_credits = I128::new(0);
4001 }
4002
4003 to_free[num_to_free] = idx as u16;
4004 num_to_free += 1;
4005 }
4006
4007 self.gc_cursor = ((start + scanned) & ACCOUNT_IDX_MASK) as u16;
4010
4011 for i in 0..num_to_free {
4012 self.free_slot(to_free[i]);
4013 }
4014
4015 num_to_free as u32
4016 }
4017 }
4018
4019 fn require_fresh_crank(&self, now_slot: u64) -> Result<()> {
4024 if now_slot.saturating_sub(self.last_crank_slot) > self.max_crank_staleness_slots {
4025 return Err(RiskError::Unauthorized);
4026 }
4027 Ok(())
4028 }
4029
4030 fn require_recent_full_sweep(&self, now_slot: u64) -> Result<()> {
4031 if now_slot.saturating_sub(self.last_full_sweep_start_slot) > self.max_crank_staleness_slots
4032 {
4033 return Err(RiskError::Unauthorized);
4034 }
4035 Ok(())
4036 }
4037
4038 pub fn top_up_insurance_fund(&mut self, amount: u128, now_slot: u64) -> Result<bool> {
4043 if now_slot < self.current_slot {
4045 return Err(RiskError::Overflow);
4046 }
4047 self.current_slot = now_slot;
4048 let new_vault = self
4049 .vault
4050 .get()
4051 .checked_add(amount)
4052 .ok_or(RiskError::Overflow)?;
4053 if new_vault > MAX_VAULT_TVL {
4054 return Err(RiskError::Overflow);
4055 }
4056 let new_ins = self
4057 .insurance_fund
4058 .balance
4059 .get()
4060 .checked_add(amount)
4061 .ok_or(RiskError::Overflow)?;
4062 self.vault = U128::new(new_vault);
4063 self.insurance_fund.balance = U128::new(new_ins);
4064 Ok(self.insurance_fund.balance.get() > self.insurance_floor)
4065 }
4066
4067 pub fn deposit_fee_credits(&mut self, idx: u16, amount: u128, now_slot: u64) -> Result<()> {
4075 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4076 return Err(RiskError::Unauthorized);
4077 }
4078 if now_slot < self.current_slot {
4079 return Err(RiskError::Unauthorized);
4080 }
4081 let debt = fee_debt_u128_checked(self.accounts[idx as usize].fee_credits.get());
4083 let capped = amount.min(debt);
4084 if capped == 0 {
4085 self.current_slot = now_slot;
4086 return Ok(()); }
4088 if capped > i128::MAX as u128 {
4089 return Err(RiskError::Overflow);
4090 }
4091 let new_vault = self
4092 .vault
4093 .get()
4094 .checked_add(capped)
4095 .ok_or(RiskError::Overflow)?;
4096 if new_vault > MAX_VAULT_TVL {
4097 return Err(RiskError::Overflow);
4098 }
4099 let new_ins = self
4100 .insurance_fund
4101 .balance
4102 .get()
4103 .checked_add(capped)
4104 .ok_or(RiskError::Overflow)?;
4105 let new_credits = self.accounts[idx as usize]
4106 .fee_credits
4107 .checked_add(capped as i128)
4108 .ok_or(RiskError::Overflow)?;
4109 self.current_slot = now_slot;
4111 self.vault = U128::new(new_vault);
4112 self.insurance_fund.balance = U128::new(new_ins);
4113 self.accounts[idx as usize].fee_credits = new_credits;
4114 Ok(())
4115 }
4116
4117 #[cfg(any(test, feature = "test", kani))]
4118 test_visible! {
4119 fn add_fee_credits(&mut self, idx: u16, amount: u128) -> Result<()> {
4120 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4121 return Err(RiskError::Unauthorized);
4122 }
4123 self.accounts[idx as usize].fee_credits = self.accounts[idx as usize]
4124 .fee_credits.saturating_add(amount as i128);
4125 Ok(())
4126 }
4127 }
4128
4129 test_visible! {
4134 fn recompute_aggregates(&mut self) {
4135 let mut c_tot = 0u128;
4136 let mut pnl_pos_tot = 0u128;
4137 self.for_each_used(|_idx, account| {
4138 c_tot = c_tot.saturating_add(account.capital.get());
4139 if account.pnl > 0 {
4140 pnl_pos_tot = pnl_pos_tot.saturating_add(account.pnl as u128);
4141 }
4142 });
4143 self.c_tot = U128::new(c_tot);
4144 self.pnl_pos_tot = pnl_pos_tot;
4145 }
4146 }
4147
4148 test_visible! {
4153 fn advance_slot(&mut self, slots: u64) {
4154 self.current_slot = self.current_slot.saturating_add(slots);
4155 }
4156 }
4157
4158 test_visible! {
4160 fn count_used(&self) -> u64 {
4161 let mut count = 0u64;
4162 self.for_each_used(|_, _| {
4163 count += 1;
4164 });
4165 count
4166 }
4167 }
4168}
4169
4170fn set_pending_reset(ctx: &mut InstructionContext, side: Side) {
4176 match side {
4177 Side::Long => ctx.pending_reset_long = true,
4178 Side::Short => ctx.pending_reset_short = true,
4179 }
4180}
4181
4182pub fn checked_u128_mul_i128(a: u128, b: i128) -> Result<i128> {
4185 if a == 0 || b == 0 {
4186 return Ok(0i128);
4187 }
4188 let negative = b < 0;
4189 let abs_b = if b == i128::MIN {
4190 return Err(RiskError::Overflow);
4191 } else {
4192 b.unsigned_abs()
4193 };
4194 let product = U256::from_u128(a)
4196 .checked_mul(U256::from_u128(abs_b))
4197 .ok_or(RiskError::Overflow)?;
4198 match product.try_into_u128() {
4201 Some(v) if v <= i128::MAX as u128 => {
4202 if negative {
4203 Ok(-(v as i128))
4204 } else {
4205 Ok(v as i128)
4206 }
4207 }
4208 _ => Err(RiskError::Overflow),
4209 }
4210}
4211
4212pub fn compute_trade_pnl(size_q: i128, price_diff: i128) -> Result<i128> {
4215 if size_q == 0 || price_diff == 0 {
4216 return Ok(0i128);
4217 }
4218
4219 let neg_size = size_q < 0;
4221 let neg_price = price_diff < 0;
4222 let result_negative = neg_size != neg_price;
4223
4224 let abs_size = size_q.unsigned_abs();
4225 let abs_price = price_diff.unsigned_abs();
4226
4227 let abs_size_u256 = U256::from_u128(abs_size);
4230 let abs_price_u256 = U256::from_u128(abs_price);
4231 let ps_u256 = U256::from_u128(POS_SCALE);
4232
4233 let (q, r) = mul_div_floor_u256_with_rem(abs_size_u256, abs_price_u256, ps_u256);
4235
4236 if result_negative {
4237 let mag = if !r.is_zero() {
4239 q.checked_add(U256::ONE).ok_or(RiskError::Overflow)?
4240 } else {
4241 q
4242 };
4243 match mag.try_into_u128() {
4246 Some(v) if v <= i128::MAX as u128 => Ok(-(v as i128)),
4247 _ => Err(RiskError::Overflow),
4248 }
4249 } else {
4250 match q.try_into_u128() {
4251 Some(v) if v <= i128::MAX as u128 => Ok(v as i128),
4252 _ => Err(RiskError::Overflow),
4253 }
4254 }
4255}
4256
4257#[cfg(all(feature = "solana", not(kani)))]
4268unsafe impl bytemuck::Zeroable for InsuranceFund {}
4269#[cfg(all(feature = "solana", not(kani)))]
4270unsafe impl bytemuck::Pod for InsuranceFund {}
4271
4272#[cfg(all(feature = "solana", not(kani)))]
4273unsafe impl bytemuck::Zeroable for Account {}
4274#[cfg(all(feature = "solana", not(kani)))]
4275unsafe impl bytemuck::Pod for Account {}
4276
4277#[cfg(all(feature = "solana", not(kani)))]
4278unsafe impl bytemuck::Zeroable for RiskParams {}
4279#[cfg(all(feature = "solana", not(kani)))]
4280unsafe impl bytemuck::Pod for RiskParams {}
4281
4282#[cfg(all(feature = "solana", not(kani)))]
4285unsafe impl bytemuck::Zeroable for RiskEngine {}
4286#[cfg(kani)]
4294mod proofs {
4295 use super::*;
4296
4297 fn simple_params() -> RiskParams {
4302 RiskParams {
4303 warmup_period_slots: 0,
4304 maintenance_margin_bps: 500,
4305 initial_margin_bps: 1000,
4306 trading_fee_bps: 0,
4307 max_accounts: MAX_ACCOUNTS as u64,
4308 new_account_fee: U128::ZERO,
4309 maintenance_fee_per_slot: U128::ZERO,
4310 max_crank_staleness_slots: u64::MAX,
4311 liquidation_fee_bps: 0,
4312 liquidation_fee_cap: U128::ZERO,
4313 liquidation_buffer_bps: 50,
4314 min_liquidation_abs: U128::ZERO,
4315 min_initial_deposit: U128::new(3),
4316 min_nonzero_mm_req: 1,
4317 min_nonzero_im_req: 2,
4318 insurance_floor: U128::ZERO,
4319 }
4320 }
4321
4322 #[kani::proof]
4328 fn prove_set_capital_aggregate_consistency() {
4329 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4330
4331 let cap0: u128 = kani::any();
4333 let cap1: u128 = kani::any();
4334 kani::assume(cap0 >= 3 && cap0 <= 1_000_000_000);
4335 kani::assume(cap1 >= 3 && cap1 <= 1_000_000_000);
4336 eng.deposit(0, cap0, 1000, 100).unwrap();
4337 eng.deposit(1, cap1, 1000, 100).unwrap();
4338
4339 let sum = eng.accounts[0].capital.get() + eng.accounts[1].capital.get();
4341 assert!(eng.c_tot.get() == sum);
4342
4343 let new_cap: u128 = kani::any();
4345 kani::assume(new_cap <= 2_000_000_000);
4346 eng.set_capital(0, new_cap);
4347
4348 let sum_after = eng.accounts[0].capital.get() + eng.accounts[1].capital.get();
4349 assert!(eng.c_tot.get() == sum_after);
4350 }
4351
4352 #[kani::proof]
4354 #[kani::unwind(5)]
4355 fn prove_deposit_vault_monotonic_engine() {
4356 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4357
4358 let cap: u128 = kani::any();
4359 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4360 eng.deposit(0, cap, 1000, 100).unwrap();
4361
4362 let vault_before = eng.vault.get();
4363 let amount: u128 = kani::any();
4364 kani::assume(amount >= 3 && amount <= 1_000_000_000);
4365
4366 if eng.deposit(0, amount, 1000, 100).is_ok() {
4367 assert!(eng.vault.get() == vault_before + amount);
4368 }
4369 }
4370
4371 #[kani::proof]
4373 fn prove_settle_losses_correctness() {
4374 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4375
4376 let cap: u128 = kani::any();
4377 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4378 eng.deposit(0, cap, 1000, 100).unwrap();
4379
4380 let loss: u128 = kani::any();
4382 kani::assume(loss >= 1 && loss <= 1_000_000_000);
4383 let neg_pnl = -(loss as i128);
4384 eng.set_pnl(0, neg_pnl);
4385
4386 let cap_before = eng.accounts[0].capital.get();
4387 let pnl_before = eng.accounts[0].pnl;
4388
4389 eng.settle_losses(0);
4390
4391 assert!(eng.accounts[0].capital.get() <= cap_before);
4393 assert!(eng.accounts[0].pnl >= pnl_before);
4395 }
4396
4397 #[kani::proof]
4404 #[kani::unwind(5)]
4405 fn prove_conservation_after_deposit() {
4406 let oracle: u64 = kani::any();
4407 kani::assume(oracle > 0 && oracle <= 1_000_000);
4408 let mut eng = RiskEngine::new_with_market(simple_params(), 100, oracle);
4409
4410 let cap0: u128 = kani::any();
4412 let cap1: u128 = kani::any();
4413 kani::assume(cap0 >= 3 && cap0 <= 1_000_000_000);
4414 kani::assume(cap1 >= 3 && cap1 <= 1_000_000_000);
4415 eng.deposit(0, cap0, oracle, 100).unwrap();
4416 eng.deposit(1, cap1, oracle, 100).unwrap();
4417
4418 assert!(eng.check_conservation());
4420
4421 let amount: u128 = kani::any();
4423 kani::assume(amount >= 3 && amount <= 1_000_000_000);
4424
4425 if eng.deposit(0, amount, oracle, 100).is_ok() {
4426 assert!(eng.check_conservation());
4427 }
4428 }
4429
4430 #[kani::proof]
4432 #[kani::unwind(6)]
4433 fn prove_withdraw_vault_exact_decrease() {
4434 let oracle: u64 = kani::any();
4435 kani::assume(oracle > 0 && oracle <= 1_000_000);
4436 let slot: u64 = 100;
4437 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4438
4439 let cap: u128 = kani::any();
4440 kani::assume(cap >= 6 && cap <= 1_000_000_000);
4441 eng.deposit(0, cap, oracle, slot).unwrap();
4442
4443 let vault_before = eng.vault.get();
4444 let amount: u128 = kani::any();
4445 kani::assume(amount >= 3 && amount <= cap);
4446
4447 if eng.withdraw(0, amount, oracle, slot, 0).is_ok() {
4449 assert!(eng.vault.get() == vault_before - amount);
4450 }
4451 }
4452
4453 #[kani::proof]
4460 #[kani::unwind(6)]
4461 fn prove_conservation_after_withdraw() {
4462 let oracle: u64 = kani::any();
4463 kani::assume(oracle > 0 && oracle <= 1_000_000);
4464 let slot: u64 = 100;
4465 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4466
4467 let cap: u128 = kani::any();
4468 kani::assume(cap >= 6 && cap <= 1_000_000_000);
4469 eng.deposit(0, cap, oracle, slot).unwrap();
4470
4471 assert!(eng.check_conservation());
4472
4473 let withdraw_amt: u128 = kani::any();
4474 kani::assume(withdraw_amt >= 3 && withdraw_amt <= cap);
4475
4476 if eng.withdraw(0, withdraw_amt, oracle, slot, 0).is_ok() {
4477 assert!(eng.check_conservation());
4478 }
4479 }
4480
4481 #[kani::proof]
4494 fn prove_use_insurance_never_exceeds_balance() {
4495 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4496
4497 let cap: u128 = kani::any();
4499 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4500 eng.deposit(0, cap, 1000, 100).unwrap();
4501
4502 let ins_bal: u128 = kani::any();
4503 kani::assume(ins_bal <= 1_000_000_000);
4504 eng.insurance_fund.balance = U128::new(ins_bal);
4505
4506 let loss: u128 = kani::any();
4507 kani::assume(loss <= 2_000_000_000);
4508
4509 let ins_before = eng.insurance_fund.balance.get();
4510 let _remaining = eng.use_insurance_buffer(loss);
4511
4512 assert!(eng.insurance_fund.balance.get() <= ins_before);
4515 }
4516
4517 #[kani::proof]
4519 fn prove_absorb_loss_preserves_vault_and_capital() {
4520 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4521
4522 let cap: u128 = kani::any();
4523 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4524 eng.deposit(0, cap, 1000, 100).unwrap();
4525
4526 let ins_bal: u128 = kani::any();
4527 kani::assume(ins_bal <= 1_000_000_000);
4528 eng.insurance_fund.balance = U128::new(ins_bal);
4529
4530 let vault_before = eng.vault.get();
4531 let ctot_before = eng.c_tot.get();
4532
4533 let loss: u128 = kani::any();
4534 kani::assume(loss <= 2_000_000_000);
4535 eng.absorb_protocol_loss(loss);
4536
4537 assert!(eng.vault.get() == vault_before);
4539 assert!(eng.c_tot.get() == ctot_before);
4540 }
4541
4542 #[kani::proof]
4544 fn prove_haircut_ratio_bounded_engine() {
4545 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4546
4547 let cap: u128 = kani::any();
4549 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4550 eng.deposit(0, cap, 1000, 100).unwrap();
4551
4552 let matured: u128 = kani::any();
4554 kani::assume(matured <= 1_000_000_000);
4555 eng.pnl_matured_pos_tot = matured;
4556
4557 let (h_num, h_den) = eng.haircut_ratio();
4558
4559 if h_den > 0 {
4560 assert!(h_num <= h_den);
4561 } else {
4562 assert!(h_num == 1 && h_den == 1);
4564 }
4565 }
4566
4567 #[kani::proof]
4570 #[kani::unwind(5)]
4571 fn prove_conservation_after_two_deposits() {
4572 let oracle: u64 = kani::any();
4573 kani::assume(oracle > 0 && oracle <= 1_000_000);
4574 let mut eng = RiskEngine::new_with_market(simple_params(), 100, oracle);
4575
4576 let cap0: u128 = kani::any();
4577 let cap1: u128 = kani::any();
4578 let cap2: u128 = kani::any();
4579 kani::assume(cap0 >= 3 && cap0 <= 500_000_000);
4580 kani::assume(cap1 >= 3 && cap1 <= 500_000_000);
4581 kani::assume(cap2 >= 3 && cap2 <= 500_000_000);
4582
4583 if eng.deposit(0, cap0, oracle, 100).is_ok() {
4585 assert!(eng.check_conservation());
4586
4587 if eng.deposit(1, cap1, oracle, 100).is_ok() {
4589 assert!(eng.check_conservation());
4590
4591 if eng.deposit(0, cap2, oracle, 100).is_ok() {
4593 assert!(eng.check_conservation());
4594 }
4595 }
4596 }
4597 }
4598
4599 #[kani::proof]
4602 fn prove_settle_losses_preserves_conservation() {
4603 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4604
4605 let cap: u128 = kani::any();
4606 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4607 eng.deposit(0, cap, 1000, 100).unwrap();
4608
4609 assert!(eng.check_conservation());
4610
4611 let loss: u128 = kani::any();
4613 kani::assume(loss >= 1 && loss <= 1_000_000_000);
4614 let neg_pnl = -(loss as i128);
4615 eng.set_pnl(0, neg_pnl);
4616
4617 eng.settle_losses(0);
4621 assert!(eng.check_conservation());
4622 }
4623
4624 #[kani::proof]
4630 fn prove_accrue_market_to_noop() {
4631 let oracle: u64 = kani::any();
4632 kani::assume(oracle > 0 && oracle <= 1_000_000);
4633 let slot: u64 = 100;
4634 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4635
4636 let slot_before = eng.last_market_slot;
4637 let oracle_before = eng.last_oracle_price;
4638
4639 eng.accrue_market_to(slot, oracle).unwrap();
4640
4641 assert!(eng.last_market_slot == slot_before);
4642 assert!(eng.last_oracle_price == oracle_before);
4643 assert!(eng.current_slot == slot);
4644 }
4645
4646 #[kani::proof]
4648 fn prove_accrue_market_to_time_monotonicity() {
4649 let oracle: u64 = kani::any();
4650 kani::assume(oracle > 0 && oracle <= 1_000_000);
4651 let slot1: u64 = kani::any();
4652 let slot2: u64 = kani::any();
4653 kani::assume(slot1 >= 100 && slot1 <= 200);
4654 kani::assume(slot2 > slot1 && slot2 <= 300);
4655
4656 let mut eng = RiskEngine::new_with_market(simple_params(), slot1, oracle);
4657
4658 eng.accrue_market_to(slot2, oracle).unwrap();
4660 assert!(eng.last_market_slot == slot2);
4661 assert!(eng.current_slot == slot2);
4662
4663 assert!(eng.accrue_market_to(slot1, oracle).is_err());
4665 }
4666
4667 #[kani::proof]
4674 fn prove_charge_fee_to_insurance_bounded() {
4675 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4676
4677 let cap: u128 = kani::any();
4678 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4679 eng.deposit(0, cap, 1000, 100).unwrap();
4680
4681 let fee: u128 = kani::any();
4682 kani::assume(fee <= 1_000_000_000);
4683
4684 let cap_before = eng.accounts[0].capital.get();
4685 let ins_before = eng.insurance_fund.balance.get();
4686
4687 if eng.charge_fee_to_insurance(0, fee).is_ok() {
4688 let fee_paid = cap_before - eng.accounts[0].capital.get();
4689 assert!(fee_paid <= fee);
4691 assert!(fee_paid <= cap_before);
4692 assert!(eng.insurance_fund.balance.get() == ins_before + fee_paid);
4694 }
4695 }
4696
4697 #[kani::proof]
4700 #[kani::unwind(6)]
4701 fn prove_liquidate_flat_account_noop() {
4702 let oracle: u64 = kani::any();
4703 kani::assume(oracle > 0 && oracle <= 1_000_000);
4704 let slot: u64 = 100;
4705 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4706
4707 let cap: u128 = kani::any();
4708 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4709 eng.deposit(0, cap, oracle, slot).unwrap();
4710
4711 let cap_before = eng.accounts[0].capital.get();
4713 let result = eng.liquidate_at_oracle(0, slot, oracle, LiquidationPolicy::FullClose, 0);
4714
4715 assert!(result == Ok(false));
4716 assert!(eng.accounts[0].capital.get() == cap_before);
4717 }
4718
4719 #[kani::proof]
4725 fn prove_checked_u128_mul_i128_sign() {
4726 let a: u128 = kani::any();
4727 let b: i128 = kani::any();
4728 kani::assume(a <= 1_000_000);
4729 kani::assume(b != i128::MIN);
4730 kani::assume(b.unsigned_abs() <= 1_000_000);
4731
4732 if let Ok(result) = checked_u128_mul_i128(a, b) {
4733 if a > 0 && b > 0 {
4734 assert!(result > 0);
4735 }
4736 if a > 0 && b < 0 {
4737 assert!(result < 0);
4738 }
4739 if a == 0 || b == 0 {
4740 assert!(result == 0);
4741 }
4742 }
4743 }
4744}