1#![allow(clippy::all, unused, unused_doc_comments)]
2#![no_std]
32#![forbid(unsafe_code)]
33
34#[cfg(kani)]
35extern crate kani;
36
37macro_rules! test_visible {
50 (
51 $(#[$meta:meta])*
52 fn $name:ident($($args:tt)*) $(-> $ret:ty)? $body:block
53 ) => {
54 $(#[$meta])*
55 #[cfg(any(feature = "test", kani))]
56 pub fn $name($($args)*) $(-> $ret)? $body
57
58 $(#[$meta])*
59 #[cfg(not(any(feature = "test", kani)))]
60 fn $name($($args)*) $(-> $ret)? $body
61 };
62}
63
64#[cfg(kani)]
69pub const MAX_ACCOUNTS: usize = 4;
70
71#[cfg(all(feature = "test", not(kani)))]
72pub const MAX_ACCOUNTS: usize = 64;
73
74#[cfg(all(not(kani), not(feature = "test")))]
75pub const MAX_ACCOUNTS: usize = 4096;
76
77pub const BITMAP_WORDS: usize = (MAX_ACCOUNTS + 63) / 64;
78pub const MAX_ROUNDING_SLACK: u128 = MAX_ACCOUNTS as u128;
79const ACCOUNT_IDX_MASK: usize = MAX_ACCOUNTS - 1;
80
81pub const GC_CLOSE_BUDGET: u32 = 32;
82pub const ACCOUNTS_PER_CRANK: u16 = 128;
83pub const LIQ_BUDGET_PER_CRANK: u16 = 64;
84
85pub const POS_SCALE: u128 = 1_000_000;
87
88pub const ADL_ONE: u128 = 1_000_000;
90
91pub const MIN_A_SIDE: u128 = 1_000;
93
94pub const MAX_ORACLE_PRICE: u64 = 1_000_000_000_000;
96
97pub const MAX_FUNDING_DT: u64 = u16::MAX as u64;
99
100pub const MAX_ABS_FUNDING_BPS_PER_SLOT: i64 = 10_000;
102
103pub const MAX_VAULT_TVL: u128 = 10_000_000_000_000_000;
105pub const MAX_POSITION_ABS_Q: u128 = 100_000_000_000_000;
106pub const MAX_ACCOUNT_NOTIONAL: u128 = 100_000_000_000_000_000_000;
107pub const MAX_TRADE_SIZE_Q: u128 = MAX_POSITION_ABS_Q; pub const MAX_OI_SIDE_Q: u128 = 100_000_000_000_000;
109pub const MAX_MATERIALIZED_ACCOUNTS: u64 = 1_000_000;
110pub const MAX_ACCOUNT_POSITIVE_PNL: u128 = 100_000_000_000_000_000_000_000_000_000_000;
111pub const MAX_PNL_POS_TOT: u128 = 100_000_000_000_000_000_000_000_000_000_000_000_000;
112pub const MAX_TRADING_FEE_BPS: u64 = 10_000;
113pub const MAX_MARGIN_BPS: u64 = 10_000;
114pub const MAX_LIQUIDATION_FEE_BPS: u64 = 10_000;
115pub 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; pub mod i128;
122pub use i128::{I128, U128};
123
124pub mod wide_math;
128use wide_math::{
129 ceil_div_positive_checked, fee_debt_u128_checked, floor_div_signed_conservative_i128,
130 mul_div_ceil_u128, mul_div_floor_u128, mul_div_floor_u256_with_rem, saturating_mul_u128_u64,
131 wide_mul_div_ceil_u128_or_over_i128max, wide_mul_div_floor_u128,
132 wide_signed_mul_div_floor_from_k_pair, OverI128Magnitude, I256, U256,
133};
134
135#[repr(u8)]
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub enum SideMode {
148 Normal = 0,
149 DrainOnly = 1,
150 ResetPending = 2,
151}
152
153pub struct InstructionContext {
155 pub pending_reset_long: bool,
156 pub pending_reset_short: bool,
157}
158
159impl InstructionContext {
160 pub fn new() -> Self {
161 Self {
162 pending_reset_long: false,
163 pending_reset_short: false,
164 }
165 }
166}
167
168#[repr(C)]
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct Account {
172 pub account_id: u64,
173 pub capital: U128,
174 pub kind: u8, pub pnl: i128,
178
179 pub reserved_pnl: u128,
181
182 pub warmup_started_at_slot: u64,
184
185 pub warmup_slope_per_step: u128,
187
188 pub position_basis_q: i128,
190
191 pub adl_a_basis: u128,
193
194 pub adl_k_snap: i128,
196
197 pub adl_epoch_snap: u64,
199
200 pub matcher_program: [u8; 32],
202 pub matcher_context: [u8; 32],
203
204 pub owner: [u8; 32],
206
207 pub fee_credits: I128,
209 pub last_fee_slot: u64,
210
211 pub fees_earned_total: U128,
213}
214
215impl Account {
216 pub const KIND_USER: u8 = 0;
217 pub const KIND_LP: u8 = 1;
218
219 pub fn is_lp(&self) -> bool {
220 self.kind == Self::KIND_LP
221 }
222
223 pub fn is_user(&self) -> bool {
224 self.kind == Self::KIND_USER
225 }
226}
227
228fn empty_account() -> Account {
229 Account {
230 account_id: 0,
231 capital: U128::ZERO,
232 kind: Account::KIND_USER,
233 pnl: 0i128,
234 reserved_pnl: 0u128,
235 warmup_started_at_slot: 0,
236 warmup_slope_per_step: 0u128,
237 position_basis_q: 0i128,
238 adl_a_basis: ADL_ONE,
239 adl_k_snap: 0i128,
240 adl_epoch_snap: 0,
241 matcher_program: [0; 32],
242 matcher_context: [0; 32],
243 owner: [0; 32],
244 fee_credits: I128::ZERO,
245 last_fee_slot: 0,
246 fees_earned_total: U128::ZERO,
247 }
248}
249
250#[repr(C)]
252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253pub struct InsuranceFund {
254 pub balance: U128,
255}
256
257#[repr(C)]
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260pub struct RiskParams {
261 pub warmup_period_slots: u64,
262 pub maintenance_margin_bps: u64,
263 pub initial_margin_bps: u64,
264 pub trading_fee_bps: u64,
265 pub max_accounts: u64,
266 pub new_account_fee: U128,
267 pub maintenance_fee_per_slot: U128,
268 pub max_crank_staleness_slots: u64,
269 pub liquidation_fee_bps: u64,
270 pub liquidation_fee_cap: U128,
271 pub min_liquidation_abs: U128,
272 pub min_initial_deposit: U128,
273 pub min_nonzero_mm_req: u128,
275 pub min_nonzero_im_req: u128,
276 pub insurance_floor: U128,
278}
279
280#[repr(C)]
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct RiskEngine {
284 pub vault: U128,
285 pub insurance_fund: InsuranceFund,
286 pub params: RiskParams,
287 pub current_slot: u64,
288
289 pub funding_rate_bps_per_slot_last: i64,
291
292 pub last_crank_slot: u64,
294 pub max_crank_staleness_slots: u64,
295
296 pub c_tot: U128,
298 pub pnl_pos_tot: u128,
299 pub pnl_matured_pos_tot: u128,
300
301 pub liq_cursor: u16,
303 pub gc_cursor: u16,
304 pub last_full_sweep_start_slot: u64,
305 pub last_full_sweep_completed_slot: u64,
306 pub crank_cursor: u16,
307 pub sweep_start_idx: u16,
308
309 pub lifetime_liquidations: u64,
311
312 pub adl_mult_long: u128,
314 pub adl_mult_short: u128,
315 pub adl_coeff_long: i128,
316 pub adl_coeff_short: i128,
317 pub adl_epoch_long: u64,
318 pub adl_epoch_short: u64,
319 pub adl_epoch_start_k_long: i128,
320 pub adl_epoch_start_k_short: i128,
321 pub oi_eff_long_q: u128,
322 pub oi_eff_short_q: u128,
323 pub side_mode_long: SideMode,
324 pub side_mode_short: SideMode,
325 pub stored_pos_count_long: u64,
326 pub stored_pos_count_short: u64,
327 pub stale_account_count_long: u64,
328 pub stale_account_count_short: u64,
329
330 pub phantom_dust_bound_long_q: u128,
332 pub phantom_dust_bound_short_q: u128,
333
334 pub materialized_account_count: u64,
336
337 pub last_oracle_price: u64,
339 pub last_market_slot: u64,
341 pub funding_price_sample_last: u64,
343
344 pub used: [u64; BITMAP_WORDS],
348 pub num_used_accounts: u16,
349 pub next_account_id: u64,
350 pub free_head: u16,
351 pub next_free: [u16; MAX_ACCOUNTS],
352 pub accounts: [Account; MAX_ACCOUNTS],
353}
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub enum RiskError {
361 InsufficientBalance,
362 Undercollateralized,
363 Unauthorized,
364 InvalidMatchingEngine,
365 PnlNotWarmedUp,
366 Overflow,
367 AccountNotFound,
368 NotAnLPAccount,
369 PositionSizeMismatch,
370 AccountKindMismatch,
371 SideBlocked,
372 CorruptState,
373}
374
375pub type Result<T> = core::result::Result<T, RiskError>;
376
377#[derive(Clone, Copy, Debug, PartialEq, Eq)]
379pub enum LiquidationPolicy {
380 FullClose,
381 ExactPartial(u128), }
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub struct CrankOutcome {
387 pub advanced: bool,
388 pub slots_forgiven: u64,
389 pub caller_settle_ok: bool,
390 pub force_realize_needed: bool,
391 pub panic_needed: bool,
392 pub num_liquidations: u32,
393 pub num_liq_errors: u16,
394 pub num_gc_closed: u32,
395 pub last_cursor: u16,
396 pub sweep_complete: bool,
397}
398
399#[inline]
404fn add_u128(a: u128, b: u128) -> u128 {
405 a.checked_add(b).expect("add_u128 overflow")
406}
407
408#[inline]
409fn sub_u128(a: u128, b: u128) -> u128 {
410 a.checked_sub(b).expect("sub_u128 underflow")
411}
412
413#[inline]
414fn mul_u128(a: u128, b: u128) -> u128 {
415 a.checked_mul(b).expect("mul_u128 overflow")
416}
417
418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420pub enum Side {
421 Long,
422 Short,
423}
424
425fn side_of_i128(v: i128) -> Option<Side> {
426 if v == 0 {
427 None
428 } else if v > 0 {
429 Some(Side::Long)
430 } else {
431 Some(Side::Short)
432 }
433}
434
435fn opposite_side(s: Side) -> Side {
436 match s {
437 Side::Long => Side::Short,
438 Side::Short => Side::Long,
439 }
440}
441
442fn i128_clamp_pos(v: i128) -> u128 {
444 if v > 0 {
445 v as u128
446 } else {
447 0u128
448 }
449}
450
451impl RiskEngine {
456 fn validate_params(params: &RiskParams) {
459 assert!(
461 (params.max_accounts as usize) <= MAX_ACCOUNTS && params.max_accounts > 0,
462 "max_accounts must be in 1..=MAX_ACCOUNTS"
463 );
464
465 assert!(
467 params.maintenance_margin_bps <= params.initial_margin_bps,
468 "maintenance_margin_bps must be <= initial_margin_bps (spec §1.4)"
469 );
470 assert!(
471 params.initial_margin_bps <= 10_000,
472 "initial_margin_bps must be <= 10_000"
473 );
474
475 assert!(
477 params.trading_fee_bps <= 10_000,
478 "trading_fee_bps must be <= 10_000"
479 );
480 assert!(
481 params.liquidation_fee_bps <= 10_000,
482 "liquidation_fee_bps must be <= 10_000"
483 );
484
485 assert!(
487 params.min_nonzero_mm_req > 0,
488 "min_nonzero_mm_req must be > 0"
489 );
490 assert!(
491 params.min_nonzero_mm_req < params.min_nonzero_im_req,
492 "min_nonzero_mm_req must be strictly less than min_nonzero_im_req"
493 );
494 assert!(
495 params.min_nonzero_im_req <= params.min_initial_deposit.get(),
496 "min_nonzero_im_req must be <= min_initial_deposit (spec §1.4)"
497 );
498
499 assert!(
501 params.min_initial_deposit.get() > 0,
502 "min_initial_deposit must be > 0 (spec §1.4)"
503 );
504 assert!(
505 params.min_initial_deposit.get() <= MAX_VAULT_TVL,
506 "min_initial_deposit must be <= MAX_VAULT_TVL"
507 );
508
509 assert!(
511 params.min_liquidation_abs.get() <= params.liquidation_fee_cap.get(),
512 "min_liquidation_abs must be <= liquidation_fee_cap (spec §1.4)"
513 );
514 assert!(
515 params.liquidation_fee_cap.get() <= MAX_PROTOCOL_FEE_ABS,
516 "liquidation_fee_cap must be <= MAX_PROTOCOL_FEE_ABS (spec §1.4)"
517 );
518
519 assert!(
521 params.maintenance_fee_per_slot.get() <= MAX_MAINTENANCE_FEE_PER_SLOT,
522 "maintenance_fee_per_slot must be <= MAX_MAINTENANCE_FEE_PER_SLOT (spec §8.2.1)"
523 );
524
525 assert!(
527 params.insurance_floor.get() <= MAX_VAULT_TVL,
528 "insurance_floor must be <= MAX_VAULT_TVL (spec §1.4)"
529 );
530 }
531
532 #[cfg(any(feature = "test", kani))]
535 pub fn new(params: RiskParams) -> Self {
536 Self::new_with_market(params, 0, 1)
537 }
538
539 pub fn new_with_market(params: RiskParams, init_slot: u64, init_oracle_price: u64) -> Self {
542 Self::validate_params(¶ms);
543 assert!(
544 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
545 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
546 );
547 let mut engine = Self {
548 vault: U128::ZERO,
549 insurance_fund: InsuranceFund {
550 balance: U128::ZERO,
551 },
552 params,
553 current_slot: init_slot,
554 funding_rate_bps_per_slot_last: 0,
555 last_crank_slot: 0,
556 max_crank_staleness_slots: params.max_crank_staleness_slots,
557 c_tot: U128::ZERO,
558 pnl_pos_tot: 0u128,
559 pnl_matured_pos_tot: 0u128,
560 liq_cursor: 0,
561 gc_cursor: 0,
562 last_full_sweep_start_slot: 0,
563 last_full_sweep_completed_slot: 0,
564 crank_cursor: 0,
565 sweep_start_idx: 0,
566 lifetime_liquidations: 0,
567 adl_mult_long: ADL_ONE,
568 adl_mult_short: ADL_ONE,
569 adl_coeff_long: 0i128,
570 adl_coeff_short: 0i128,
571 adl_epoch_long: 0,
572 adl_epoch_short: 0,
573 adl_epoch_start_k_long: 0i128,
574 adl_epoch_start_k_short: 0i128,
575 oi_eff_long_q: 0u128,
576 oi_eff_short_q: 0u128,
577 side_mode_long: SideMode::Normal,
578 side_mode_short: SideMode::Normal,
579 stored_pos_count_long: 0,
580 stored_pos_count_short: 0,
581 stale_account_count_long: 0,
582 stale_account_count_short: 0,
583 phantom_dust_bound_long_q: 0u128,
584 phantom_dust_bound_short_q: 0u128,
585 materialized_account_count: 0,
586 last_oracle_price: init_oracle_price,
587 last_market_slot: init_slot,
588 funding_price_sample_last: init_oracle_price,
589 used: [0; BITMAP_WORDS],
590 num_used_accounts: 0,
591 next_account_id: 0,
592 free_head: 0,
593 next_free: [0; MAX_ACCOUNTS],
594 accounts: [empty_account(); MAX_ACCOUNTS],
595 };
596
597 for i in 0..MAX_ACCOUNTS - 1 {
598 engine.next_free[i] = (i + 1) as u16;
599 }
600 engine.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
601
602 engine
603 }
604
605 pub fn init_in_place(&mut self, params: RiskParams, init_slot: u64, init_oracle_price: u64) {
608 Self::validate_params(¶ms);
609 assert!(
610 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
611 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
612 );
613 self.vault = U128::ZERO;
614 self.insurance_fund = InsuranceFund {
615 balance: U128::ZERO,
616 };
617 self.params = params;
618 self.current_slot = init_slot;
619 self.funding_rate_bps_per_slot_last = 0;
620 self.last_crank_slot = 0;
621 self.max_crank_staleness_slots = params.max_crank_staleness_slots;
622 self.c_tot = U128::ZERO;
623 self.pnl_pos_tot = 0;
624 self.pnl_matured_pos_tot = 0;
625 self.liq_cursor = 0;
626 self.gc_cursor = 0;
627 self.last_full_sweep_start_slot = 0;
628 self.last_full_sweep_completed_slot = 0;
629 self.crank_cursor = 0;
630 self.sweep_start_idx = 0;
631 self.lifetime_liquidations = 0;
632 self.adl_mult_long = ADL_ONE;
633 self.adl_mult_short = ADL_ONE;
634 self.adl_coeff_long = 0;
635 self.adl_coeff_short = 0;
636 self.adl_epoch_long = 0;
637 self.adl_epoch_short = 0;
638 self.adl_epoch_start_k_long = 0;
639 self.adl_epoch_start_k_short = 0;
640 self.oi_eff_long_q = 0;
641 self.oi_eff_short_q = 0;
642 self.side_mode_long = SideMode::Normal;
643 self.side_mode_short = SideMode::Normal;
644 self.stored_pos_count_long = 0;
645 self.stored_pos_count_short = 0;
646 self.stale_account_count_long = 0;
647 self.stale_account_count_short = 0;
648 self.phantom_dust_bound_long_q = 0;
649 self.phantom_dust_bound_short_q = 0;
650 self.materialized_account_count = 0;
651 self.last_oracle_price = init_oracle_price;
652 self.last_market_slot = init_slot;
653 self.funding_price_sample_last = init_oracle_price;
654 self.used = [0; BITMAP_WORDS];
656 self.num_used_accounts = 0;
657 self.next_account_id = 0;
658 self.free_head = 0;
659 self.accounts = [empty_account(); MAX_ACCOUNTS];
660 for i in 0..MAX_ACCOUNTS - 1 {
661 self.next_free[i] = (i + 1) as u16;
662 }
663 self.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
664 }
665
666 pub fn is_used(&self, idx: usize) -> bool {
671 if idx >= MAX_ACCOUNTS {
672 return false;
673 }
674 let w = idx >> 6;
675 let b = idx & 63;
676 ((self.used[w] >> b) & 1) == 1
677 }
678
679 fn set_used(&mut self, idx: usize) {
680 let w = idx >> 6;
681 let b = idx & 63;
682 self.used[w] |= 1u64 << b;
683 }
684
685 fn clear_used(&mut self, idx: usize) {
686 let w = idx >> 6;
687 let b = idx & 63;
688 self.used[w] &= !(1u64 << b);
689 }
690
691 #[allow(dead_code)]
692 fn for_each_used_mut<F: FnMut(usize, &mut Account)>(&mut self, mut f: F) {
693 for (block, word) in self.used.iter().copied().enumerate() {
694 let mut w = word;
695 while w != 0 {
696 let bit = w.trailing_zeros() as usize;
697 let idx = block * 64 + bit;
698 w &= w - 1;
699 if idx >= MAX_ACCOUNTS {
700 continue;
701 }
702 f(idx, &mut self.accounts[idx]);
703 }
704 }
705 }
706
707 fn for_each_used<F: FnMut(usize, &Account)>(&self, mut f: F) {
708 for (block, word) in self.used.iter().copied().enumerate() {
709 let mut w = word;
710 while w != 0 {
711 let bit = w.trailing_zeros() as usize;
712 let idx = block * 64 + bit;
713 w &= w - 1;
714 if idx >= MAX_ACCOUNTS {
715 continue;
716 }
717 f(idx, &self.accounts[idx]);
718 }
719 }
720 }
721
722 fn alloc_slot(&mut self) -> Result<u16> {
727 if self.free_head == u16::MAX {
728 return Err(RiskError::Overflow);
729 }
730 let idx = self.free_head;
731 self.free_head = self.next_free[idx as usize];
732 self.set_used(idx as usize);
733 self.num_used_accounts = self
734 .num_used_accounts
735 .checked_add(1)
736 .expect("num_used_accounts overflow — slot leak corruption");
737 Ok(idx)
738 }
739
740 test_visible! {
741 fn free_slot(&mut self, idx: u16) {
742 self.accounts[idx as usize] = empty_account();
743 self.clear_used(idx as usize);
744 self.next_free[idx as usize] = self.free_head;
745 self.free_head = idx;
746 self.num_used_accounts = self.num_used_accounts.checked_sub(1)
747 .expect("free_slot: num_used_accounts underflow — double-free corruption");
748 self.materialized_account_count = self.materialized_account_count.checked_sub(1)
750 .expect("free_slot: materialized_account_count underflow — double-free corruption");
751 }
752 }
753
754 fn materialize_at(&mut self, idx: u16, slot_anchor: u64) -> Result<()> {
758 if idx as usize >= MAX_ACCOUNTS {
759 return Err(RiskError::AccountNotFound);
760 }
761
762 let used_count = self.num_used_accounts as u64;
763 if used_count >= self.params.max_accounts {
764 return Err(RiskError::Overflow);
765 }
766
767 self.materialized_account_count = self
769 .materialized_account_count
770 .checked_add(1)
771 .ok_or(RiskError::Overflow)?;
772 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
773 self.materialized_account_count -= 1;
774 return Err(RiskError::Overflow);
775 }
776
777 let mut found = false;
780 if self.free_head == idx {
781 self.free_head = self.next_free[idx as usize];
782 found = true;
783 } else {
784 let mut prev = self.free_head;
785 let mut steps = 0usize;
786 while prev != u16::MAX && steps < MAX_ACCOUNTS {
787 if self.next_free[prev as usize] == idx {
788 self.next_free[prev as usize] = self.next_free[idx as usize];
789 found = true;
790 break;
791 }
792 prev = self.next_free[prev as usize];
793 steps += 1;
794 }
795 }
796 if !found {
797 self.materialized_account_count -= 1;
799 return Err(RiskError::CorruptState);
800 }
801
802 self.set_used(idx as usize);
803 self.num_used_accounts = self
804 .num_used_accounts
805 .checked_add(1)
806 .expect("num_used_accounts overflow — slot leak corruption");
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 => self.side_mode_long,
1119 Side::Short => 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,
1140 Side::Short => self.side_mode_short = m,
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);
1332 if side_mode != SideMode::ResetPending {
1333 return Err(RiskError::CorruptState);
1334 }
1335 if epoch_snap.checked_add(1) != Some(epoch_side) {
1336 return Err(RiskError::CorruptState);
1337 }
1338
1339 let k_epoch_start = self.get_k_epoch_start(side);
1340 let k_snap = self.accounts[idx].adl_k_snap;
1341
1342 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1344 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_epoch_start, den);
1345
1346 let new_pnl = self.accounts[idx].pnl.checked_add(pnl_delta).ok_or(RiskError::Overflow)?;
1347 if new_pnl == i128::MIN {
1348 return Err(RiskError::Overflow);
1349 }
1350
1351 let old_stale = self.get_stale_count(side);
1352 let new_stale = old_stale.checked_sub(1).ok_or(RiskError::CorruptState)?;
1353
1354 let old_r = self.accounts[idx].reserved_pnl;
1356 self.set_pnl(idx, new_pnl);
1357
1358 if self.accounts[idx].reserved_pnl > old_r {
1359 self.restart_warmup_after_reserve_increase(idx);
1360 }
1361
1362 self.set_position_basis_q(idx, 0i128);
1363 self.set_stale_count(side, new_stale);
1364
1365 self.accounts[idx].adl_a_basis = ADL_ONE;
1367 self.accounts[idx].adl_k_snap = 0i128;
1368 self.accounts[idx].adl_epoch_snap = 0;
1369 }
1370
1371 Ok(())
1372 }
1373 }
1374
1375 pub fn accrue_market_to(&mut self, now_slot: u64, oracle_price: u64) -> Result<()> {
1380 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
1381 return Err(RiskError::Overflow);
1382 }
1383
1384 if now_slot < self.current_slot {
1386 return Err(RiskError::Overflow);
1387 }
1388 if now_slot < self.last_market_slot {
1389 return Err(RiskError::Overflow);
1390 }
1391
1392 let long_live = self.oi_eff_long_q != 0;
1394 let short_live = self.oi_eff_short_q != 0;
1395
1396 let total_dt = now_slot.saturating_sub(self.last_market_slot);
1397 if total_dt == 0 && self.last_oracle_price == oracle_price {
1398 self.current_slot = now_slot;
1400 return Ok(());
1401 }
1402
1403 let mut k_long = self.adl_coeff_long;
1407 let mut k_short = self.adl_coeff_short;
1408
1409 let current_price = self.last_oracle_price;
1411 let delta_p = (oracle_price as i128)
1412 .checked_sub(current_price as i128)
1413 .ok_or(RiskError::Overflow)?;
1414 if delta_p != 0 {
1415 if long_live {
1416 let dk = checked_u128_mul_i128(self.adl_mult_long, delta_p)?;
1417 k_long = k_long.checked_add(dk).ok_or(RiskError::Overflow)?;
1418 }
1419 if short_live {
1420 let dk = checked_u128_mul_i128(self.adl_mult_short, delta_p)?;
1421 k_short = k_short.checked_sub(dk).ok_or(RiskError::Overflow)?;
1422 }
1423 }
1424
1425 let r_last = self.funding_rate_bps_per_slot_last;
1427 if r_last != 0 && total_dt > 0 && long_live && short_live {
1428 let fund_px_0 = self.funding_price_sample_last;
1429
1430 if fund_px_0 > 0 {
1431 let mut dt_remaining = total_dt;
1432
1433 while dt_remaining > 0 {
1434 let dt_sub = core::cmp::min(dt_remaining, MAX_FUNDING_DT);
1435 dt_remaining -= dt_sub;
1436
1437 let fund_num: i128 = (fund_px_0 as i128)
1438 .checked_mul(r_last as i128)
1439 .ok_or(RiskError::Overflow)?
1440 .checked_mul(dt_sub as i128)
1441 .ok_or(RiskError::Overflow)?;
1442
1443 let fund_term = floor_div_signed_conservative_i128(fund_num, 10_000u128);
1444
1445 if fund_term != 0 {
1446 let dk_long = checked_u128_mul_i128(self.adl_mult_long, fund_term)?;
1447 k_long = k_long.checked_sub(dk_long).ok_or(RiskError::Overflow)?;
1448 let dk_short = checked_u128_mul_i128(self.adl_mult_short, fund_term)?;
1449 k_short = k_short.checked_add(dk_short).ok_or(RiskError::Overflow)?;
1450 }
1451 }
1452 }
1453 }
1454
1455 self.adl_coeff_long = k_long;
1457 self.adl_coeff_short = k_short;
1458 self.current_slot = now_slot;
1459 self.last_market_slot = now_slot;
1460 self.last_oracle_price = oracle_price;
1461 self.funding_price_sample_last = oracle_price;
1462
1463 Ok(())
1464 }
1465
1466 fn validate_funding_rate(rate: i64) -> Result<()> {
1469 if rate.unsigned_abs() > MAX_ABS_FUNDING_BPS_PER_SLOT as u64 {
1470 return Err(RiskError::Overflow);
1471 }
1472 Ok(())
1473 }
1474
1475 test_visible! {
1478 fn recompute_r_last_from_final_state(&mut self, externally_computed_rate: i64) -> Result<()> {
1479 if externally_computed_rate.unsigned_abs() > MAX_ABS_FUNDING_BPS_PER_SLOT as u64 {
1482 return Err(RiskError::Overflow);
1483 }
1484 self.funding_rate_bps_per_slot_last = externally_computed_rate;
1485 Ok(())
1486 }
1487 }
1488
1489 pub fn run_end_of_instruction_lifecycle(
1497 &mut self,
1498 ctx: &mut InstructionContext,
1499 funding_rate: i64,
1500 ) -> Result<()> {
1501 Self::validate_funding_rate(funding_rate)?;
1502
1503 self.schedule_end_of_instruction_resets(ctx)?;
1504 self.finalize_end_of_instruction_resets(ctx);
1505 self.recompute_r_last_from_final_state(funding_rate)?;
1506 Ok(())
1507 }
1508
1509 fn use_insurance_buffer(&mut self, loss: u128) -> u128 {
1516 if loss == 0 {
1517 return 0;
1518 }
1519 let ins_bal = self.insurance_fund.balance.get();
1520 let available = ins_bal.saturating_sub(self.params.insurance_floor.get());
1521 let pay = core::cmp::min(loss, available);
1522 if pay > 0 {
1523 self.insurance_fund.balance = U128::new(ins_bal - pay);
1524 }
1525 loss - pay
1526 }
1527
1528 test_visible! {
1531 fn absorb_protocol_loss(&mut self, loss: u128) {
1532 if loss == 0 {
1533 return;
1534 }
1535 let _rem = self.use_insurance_buffer(loss);
1536 }
1538 }
1539
1540 test_visible! {
1545 fn enqueue_adl(&mut self, ctx: &mut InstructionContext, liq_side: Side, q_close_q: u128, d: u128) -> Result<()> {
1546 let opp = opposite_side(liq_side);
1547
1548 if q_close_q != 0 {
1550 let old_oi = self.get_oi_eff(liq_side);
1551 let new_oi = old_oi.checked_sub(q_close_q).ok_or(RiskError::CorruptState)?;
1552 self.set_oi_eff(liq_side, new_oi);
1553 }
1554
1555 let d_rem = if d > 0 { self.use_insurance_buffer(d) } else { 0u128 };
1557
1558 let oi = self.get_oi_eff(opp);
1560
1561 if oi == 0 {
1563 if self.get_oi_eff(liq_side) == 0 {
1565 set_pending_reset(ctx, liq_side);
1566 set_pending_reset(ctx, opp);
1567 }
1568 return Ok(());
1569 }
1570
1571 if self.get_stored_pos_count(opp) == 0 {
1574 if q_close_q > oi {
1575 return Err(RiskError::CorruptState);
1576 }
1577 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1578 self.set_oi_eff(opp, oi_post);
1580 if oi_post == 0 {
1581 set_pending_reset(ctx, opp);
1583 if self.get_oi_eff(liq_side) == 0 {
1585 set_pending_reset(ctx, liq_side);
1586 }
1587 }
1588 return Ok(());
1589 }
1590
1591 if q_close_q > oi {
1593 return Err(RiskError::CorruptState);
1594 }
1595
1596 let a_old = self.get_a_side(opp);
1597 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1598
1599 if d_rem != 0 {
1604 let a_ps = a_old.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1605 match wide_mul_div_ceil_u128_or_over_i128max(d_rem, a_ps, oi) {
1606 Ok(delta_k_abs) => {
1607 let delta_k = -(delta_k_abs as i128);
1608 let k_opp = self.get_k_side(opp);
1609 match k_opp.checked_add(delta_k) {
1610 Some(new_k) => {
1611 self.set_k_side(opp, new_k);
1612 }
1613 None => {
1614 }
1616 }
1617 }
1618 Err(OverI128Magnitude) => {
1619 }
1621 }
1622 }
1623
1624 if oi_post == 0 {
1626 self.set_oi_eff(opp, 0u128);
1627 set_pending_reset(ctx, opp);
1628 if self.get_oi_eff(liq_side) == 0 {
1629 set_pending_reset(ctx, liq_side);
1630 }
1631 return Ok(());
1632 }
1633
1634 let a_old_u256 = U256::from_u128(a_old);
1636 let oi_post_u256 = U256::from_u128(oi_post);
1637 let oi_u256 = U256::from_u128(oi);
1638 let (a_candidate_u256, a_trunc_rem) = mul_div_floor_u256_with_rem(
1639 a_old_u256,
1640 oi_post_u256,
1641 oi_u256,
1642 );
1643
1644 if !a_candidate_u256.is_zero() {
1646 let a_new = a_candidate_u256.try_into_u128().expect("A_candidate exceeds u128");
1647 self.set_a_side(opp, a_new);
1648 self.set_oi_eff(opp, oi_post);
1649 if !a_trunc_rem.is_zero() {
1651 let n_opp = self.get_stored_pos_count(opp) as u128;
1652 let n_opp_u256 = U256::from_u128(n_opp);
1653 let oi_plus_n = oi_u256.checked_add(n_opp_u256).unwrap_or(U256::MAX);
1655 let ceil_term = ceil_div_positive_checked(oi_plus_n, a_old_u256);
1656 let global_a_dust_bound = n_opp_u256.checked_add(ceil_term)
1657 .unwrap_or(U256::MAX);
1658 let bound_u128 = global_a_dust_bound.try_into_u128().unwrap_or(u128::MAX);
1659 self.inc_phantom_dust_bound_by(opp, bound_u128);
1660 }
1661 if a_new < MIN_A_SIDE {
1662 self.set_side_mode(opp, SideMode::DrainOnly);
1663 }
1664 return Ok(());
1665 }
1666
1667 self.set_oi_eff(opp, 0u128);
1669 self.set_oi_eff(liq_side, 0u128);
1670 set_pending_reset(ctx, opp);
1671 set_pending_reset(ctx, liq_side);
1672
1673 Ok(())
1674 }
1675 }
1676
1677 test_visible! {
1682 fn begin_full_drain_reset(&mut self, side: Side) {
1683 assert!(self.get_oi_eff(side) == 0, "begin_full_drain_reset: OI not zero");
1685
1686 let k = self.get_k_side(side);
1688 match side {
1689 Side::Long => self.adl_epoch_start_k_long = k,
1690 Side::Short => self.adl_epoch_start_k_short = k,
1691 }
1692
1693 match side {
1695 Side::Long => self.adl_epoch_long = self.adl_epoch_long.checked_add(1)
1696 .expect("epoch overflow"),
1697 Side::Short => self.adl_epoch_short = self.adl_epoch_short.checked_add(1)
1698 .expect("epoch overflow"),
1699 }
1700
1701 self.set_a_side(side, ADL_ONE);
1703
1704 let spc = self.get_stored_pos_count(side);
1706 self.set_stale_count(side, spc);
1707
1708 match side {
1710 Side::Long => self.phantom_dust_bound_long_q = 0u128,
1711 Side::Short => self.phantom_dust_bound_short_q = 0u128,
1712 }
1713
1714 self.set_side_mode(side, SideMode::ResetPending);
1716 }
1717 }
1718
1719 test_visible! {
1720 fn finalize_side_reset(&mut self, side: Side) -> Result<()> {
1721 if self.get_side_mode(side) != SideMode::ResetPending {
1722 return Err(RiskError::CorruptState);
1723 }
1724 if self.get_oi_eff(side) != 0 {
1725 return Err(RiskError::CorruptState);
1726 }
1727 if self.get_stale_count(side) != 0 {
1728 return Err(RiskError::CorruptState);
1729 }
1730 if self.get_stored_pos_count(side) != 0 {
1731 return Err(RiskError::CorruptState);
1732 }
1733 self.set_side_mode(side, SideMode::Normal);
1734 Ok(())
1735 }
1736 }
1737
1738 test_visible! {
1743 fn schedule_end_of_instruction_resets(&mut self, ctx: &mut InstructionContext) -> Result<()> {
1744 if self.stored_pos_count_long == 0 && self.stored_pos_count_short == 0 {
1746 let clear_bound_q = self.phantom_dust_bound_long_q
1747 .checked_add(self.phantom_dust_bound_short_q)
1748 .ok_or(RiskError::CorruptState)?;
1749 let has_residual = self.oi_eff_long_q != 0
1750 || self.oi_eff_short_q != 0
1751 || self.phantom_dust_bound_long_q != 0
1752 || self.phantom_dust_bound_short_q != 0;
1753 if has_residual {
1754 if self.oi_eff_long_q != self.oi_eff_short_q {
1755 return Err(RiskError::CorruptState);
1756 }
1757 if self.oi_eff_long_q <= clear_bound_q && self.oi_eff_short_q <= clear_bound_q {
1758 self.oi_eff_long_q = 0u128;
1759 self.oi_eff_short_q = 0u128;
1760 ctx.pending_reset_long = true;
1761 ctx.pending_reset_short = true;
1762 } else {
1763 return Err(RiskError::CorruptState);
1764 }
1765 }
1766 }
1767 else if self.stored_pos_count_long == 0 && self.stored_pos_count_short > 0 {
1769 let has_residual = self.oi_eff_long_q != 0
1770 || self.oi_eff_short_q != 0
1771 || self.phantom_dust_bound_long_q != 0;
1772 if has_residual {
1773 if self.oi_eff_long_q != self.oi_eff_short_q {
1774 return Err(RiskError::CorruptState);
1775 }
1776 if self.oi_eff_long_q <= self.phantom_dust_bound_long_q {
1777 self.oi_eff_long_q = 0u128;
1778 self.oi_eff_short_q = 0u128;
1779 ctx.pending_reset_long = true;
1780 ctx.pending_reset_short = true;
1781 } else {
1782 return Err(RiskError::CorruptState);
1783 }
1784 }
1785 }
1786 else if self.stored_pos_count_short == 0 && self.stored_pos_count_long > 0 {
1788 let has_residual = self.oi_eff_long_q != 0
1789 || self.oi_eff_short_q != 0
1790 || self.phantom_dust_bound_short_q != 0;
1791 if has_residual {
1792 if self.oi_eff_long_q != self.oi_eff_short_q {
1793 return Err(RiskError::CorruptState);
1794 }
1795 if self.oi_eff_short_q <= self.phantom_dust_bound_short_q {
1796 self.oi_eff_long_q = 0u128;
1797 self.oi_eff_short_q = 0u128;
1798 ctx.pending_reset_long = true;
1799 ctx.pending_reset_short = true;
1800 } else {
1801 return Err(RiskError::CorruptState);
1802 }
1803 }
1804 }
1805
1806 if self.side_mode_long == SideMode::DrainOnly && self.oi_eff_long_q == 0 {
1808 ctx.pending_reset_long = true;
1809 }
1810 if self.side_mode_short == SideMode::DrainOnly && self.oi_eff_short_q == 0 {
1811 ctx.pending_reset_short = true;
1812 }
1813
1814 Ok(())
1815 }
1816 }
1817
1818 test_visible! {
1819 fn finalize_end_of_instruction_resets(&mut self, ctx: &InstructionContext) {
1820 if ctx.pending_reset_long && self.side_mode_long != SideMode::ResetPending {
1821 self.begin_full_drain_reset(Side::Long);
1822 }
1823 if ctx.pending_reset_short && self.side_mode_short != SideMode::ResetPending {
1824 self.begin_full_drain_reset(Side::Short);
1825 }
1826 self.maybe_finalize_ready_reset_sides();
1828 }
1829 }
1830
1831 fn maybe_finalize_ready_reset_sides(&mut self) {
1835 if self.side_mode_long == SideMode::ResetPending
1836 && self.get_oi_eff(Side::Long) == 0
1837 && self.get_stale_count(Side::Long) == 0
1838 && self.get_stored_pos_count(Side::Long) == 0
1839 {
1840 self.set_side_mode(Side::Long, SideMode::Normal);
1841 }
1842 if self.side_mode_short == SideMode::ResetPending
1843 && self.get_oi_eff(Side::Short) == 0
1844 && self.get_stale_count(Side::Short) == 0
1845 && self.get_stored_pos_count(Side::Short) == 0
1846 {
1847 self.set_side_mode(Side::Short, SideMode::Normal);
1848 }
1849 }
1850
1851 pub fn haircut_ratio(&self) -> (u128, u128) {
1858 if self.pnl_matured_pos_tot == 0 {
1859 return (1u128, 1u128);
1860 }
1861 let senior_sum = self
1862 .c_tot
1863 .get()
1864 .checked_add(self.insurance_fund.balance.get());
1865 let residual: u128 = match senior_sum {
1866 Some(ss) => {
1867 if self.vault.get() >= ss {
1868 self.vault.get() - ss
1869 } else {
1870 0u128
1871 }
1872 }
1873 None => 0u128, };
1875 let h_num = if residual < self.pnl_matured_pos_tot {
1876 residual
1877 } else {
1878 self.pnl_matured_pos_tot
1879 };
1880 (h_num, self.pnl_matured_pos_tot)
1881 }
1882
1883 pub fn effective_matured_pnl(&self, idx: usize) -> u128 {
1885 let released = self.released_pos(idx);
1886 if released == 0 {
1887 return 0u128;
1888 }
1889 let (h_num, h_den) = self.haircut_ratio();
1890 if h_den == 0 {
1891 return released;
1892 }
1893 wide_mul_div_floor_u128(released, h_num, h_den)
1894 }
1895
1896 pub fn account_equity_maint_raw(&self, account: &Account) -> i128 {
1902 let wide = self.account_equity_maint_raw_wide(account);
1903 match wide.try_into_i128() {
1904 Some(v) => v,
1905 None => {
1906 if wide.is_negative() {
1911 i128::MIN + 1
1912 } else {
1913 i128::MAX
1914 }
1915 }
1916 }
1917 }
1918
1919 pub fn account_equity_maint_raw_wide(&self, account: &Account) -> I256 {
1923 let cap = I256::from_u128(account.capital.get());
1924 let pnl = I256::from_i128(account.pnl);
1925 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1926
1927 let sum = cap.checked_add(pnl).expect("I256 add overflow");
1929 sum.checked_sub(fee_debt).expect("I256 sub overflow")
1930 }
1931
1932 pub fn account_equity_net(&self, account: &Account, _oracle_price: u64) -> i128 {
1934 let raw = self.account_equity_maint_raw(account);
1935 if raw < 0 {
1936 0i128
1937 } else {
1938 raw
1939 }
1940 }
1941
1942 pub fn account_equity_init_raw(&self, account: &Account, idx: usize) -> i128 {
1946 let cap = I256::from_u128(account.capital.get());
1947 let neg_pnl = I256::from_i128(if account.pnl < 0 { account.pnl } else { 0i128 });
1948 let eff_matured = I256::from_u128(self.effective_matured_pnl(idx));
1949 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1950
1951 let sum = cap
1952 .checked_add(neg_pnl)
1953 .expect("I256 add overflow")
1954 .checked_add(eff_matured)
1955 .expect("I256 add overflow")
1956 .checked_sub(fee_debt)
1957 .expect("I256 sub overflow");
1958
1959 match sum.try_into_i128() {
1960 Some(v) => v,
1961 None => {
1962 if sum.is_negative() {
1966 i128::MIN + 1
1967 } else {
1968 i128::MAX
1969 }
1970 }
1971 }
1972 }
1973
1974 pub fn account_equity_init_net(&self, account: &Account, idx: usize) -> i128 {
1976 let raw = self.account_equity_init_raw(account, idx);
1977 if raw < 0 {
1978 0i128
1979 } else {
1980 raw
1981 }
1982 }
1983
1984 pub fn notional(&self, idx: usize, oracle_price: u64) -> u128 {
1986 let eff = self.effective_pos_q(idx);
1987 if eff == 0 {
1988 return 0;
1989 }
1990 let abs_eff = eff.unsigned_abs();
1991 mul_div_floor_u128(abs_eff, oracle_price as u128, POS_SCALE)
1992 }
1993
1994 pub fn is_above_maintenance_margin(
1997 &self,
1998 account: &Account,
1999 idx: usize,
2000 oracle_price: u64,
2001 ) -> bool {
2002 let eq_net = self.account_equity_net(account, oracle_price);
2003 let eff = self.effective_pos_q(idx);
2004 if eff == 0 {
2005 return eq_net > 0;
2006 }
2007 let not = self.notional(idx, oracle_price);
2008 let proportional =
2009 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000);
2010 let mm_req = core::cmp::max(proportional, self.params.min_nonzero_mm_req);
2011 let mm_req_i128 = if mm_req > i128::MAX as u128 {
2012 i128::MAX
2013 } else {
2014 mm_req as i128
2015 };
2016 eq_net > mm_req_i128
2017 }
2018
2019 pub fn is_above_initial_margin(
2024 &self,
2025 account: &Account,
2026 idx: usize,
2027 oracle_price: u64,
2028 ) -> bool {
2029 let eq_init_raw = self.account_equity_init_raw(account, idx);
2030 let eff = self.effective_pos_q(idx);
2031 if eff == 0 {
2032 return eq_init_raw >= 0;
2033 }
2034 let not = self.notional(idx, oracle_price);
2035 let proportional = mul_div_floor_u128(not, self.params.initial_margin_bps as u128, 10_000);
2036 let im_req = core::cmp::max(proportional, self.params.min_nonzero_im_req);
2037 let im_req_i128 = if im_req > i128::MAX as u128 {
2038 i128::MAX
2039 } else {
2040 im_req as i128
2041 };
2042 eq_init_raw >= im_req_i128
2043 }
2044
2045 pub fn check_conservation(&self) -> bool {
2050 let senior = self
2051 .c_tot
2052 .get()
2053 .checked_add(self.insurance_fund.balance.get());
2054 match senior {
2055 Some(s) => self.vault.get() >= s,
2056 None => false,
2057 }
2058 }
2059
2060 pub fn released_pos(&self, idx: usize) -> u128 {
2066 let pnl = self.accounts[idx].pnl;
2067 let pos_pnl = i128_clamp_pos(pnl);
2068 pos_pnl.saturating_sub(self.accounts[idx].reserved_pnl)
2069 }
2070
2071 test_visible! {
2074 fn restart_warmup_after_reserve_increase(&mut self, idx: usize) {
2075 let t = self.params.warmup_period_slots;
2076 if t == 0 {
2077 self.set_reserved_pnl(idx, 0);
2079 self.accounts[idx].warmup_slope_per_step = 0;
2080 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2081 return;
2082 }
2083 let r = self.accounts[idx].reserved_pnl;
2084 if r == 0 {
2085 self.accounts[idx].warmup_slope_per_step = 0;
2086 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2087 return;
2088 }
2089 let base = r / (t as u128);
2091 let slope = if base == 0 { 1u128 } else { base };
2092 self.accounts[idx].warmup_slope_per_step = slope;
2093 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2094 }
2095 }
2096
2097 test_visible! {
2099 fn advance_profit_warmup(&mut self, idx: usize) {
2100 let r = self.accounts[idx].reserved_pnl;
2101 if r == 0 {
2102 self.accounts[idx].warmup_slope_per_step = 0;
2103 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2104 return;
2105 }
2106 let t = self.params.warmup_period_slots;
2107 if t == 0 {
2108 self.set_reserved_pnl(idx, 0);
2109 self.accounts[idx].warmup_slope_per_step = 0;
2110 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2111 return;
2112 }
2113 let elapsed = self.current_slot.saturating_sub(self.accounts[idx].warmup_started_at_slot);
2114 let cap = saturating_mul_u128_u64(self.accounts[idx].warmup_slope_per_step, elapsed);
2115 let release = core::cmp::min(r, cap);
2116 if release > 0 {
2117 self.set_reserved_pnl(idx, r - release);
2118 }
2119 if self.accounts[idx].reserved_pnl == 0 {
2120 self.accounts[idx].warmup_slope_per_step = 0;
2121 }
2122 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2123 }
2124 }
2125
2126 fn settle_losses(&mut self, idx: usize) {
2132 let pnl = self.accounts[idx].pnl;
2133 if pnl >= 0 {
2134 return;
2135 }
2136 assert!(pnl != i128::MIN, "settle_losses: i128::MIN");
2137 let need = pnl.unsigned_abs();
2138 let cap = self.accounts[idx].capital.get();
2139 let pay = core::cmp::min(need, cap);
2140 if pay > 0 {
2141 self.set_capital(idx, cap - pay);
2142 let pay_i128 = pay as i128; let new_pnl = pnl
2144 .checked_add(pay_i128)
2145 .expect("settle_losses: unreachable overflow (pay <= |pnl|)");
2146 assert!(
2147 new_pnl != i128::MIN,
2148 "settle_losses: new_pnl == i128::MIN is unreachable"
2149 );
2150 self.set_pnl(idx, new_pnl);
2151 }
2152 }
2153
2154 fn resolve_flat_negative(&mut self, idx: usize) {
2156 let eff = self.effective_pos_q(idx);
2157 if eff != 0 {
2158 return; }
2160 let pnl = self.accounts[idx].pnl;
2161 if pnl < 0 {
2162 assert!(pnl != i128::MIN, "resolve_flat_negative: i128::MIN");
2163 let loss = pnl.unsigned_abs();
2164 self.absorb_protocol_loss(loss);
2165 self.set_pnl(idx, 0i128);
2166 }
2167 }
2168
2169 fn do_profit_conversion(&mut self, idx: usize) {
2172 let x = self.released_pos(idx);
2173 if x == 0 {
2174 return;
2175 }
2176
2177 let (h_num, h_den) = self.haircut_ratio();
2181 assert!(
2182 h_den > 0,
2183 "do_profit_conversion: h_den must be > 0 when x > 0"
2184 );
2185 let y: u128 = wide_mul_div_floor_u128(x, h_num, h_den);
2186
2187 self.consume_released_pnl(idx, x);
2189
2190 let new_cap = add_u128(self.accounts[idx].capital.get(), y);
2192 self.set_capital(idx, new_cap);
2193
2194 if self.accounts[idx].reserved_pnl == 0 {
2196 self.accounts[idx].warmup_slope_per_step = 0;
2197 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2198 }
2199 }
2201
2202 test_visible! {
2204 fn fee_debt_sweep(&mut self, idx: usize) {
2205 let fc = self.accounts[idx].fee_credits.get();
2206 let debt = fee_debt_u128_checked(fc);
2207 if debt == 0 {
2208 return;
2209 }
2210 let cap = self.accounts[idx].capital.get();
2211 let pay = core::cmp::min(debt, cap);
2212 if pay > 0 {
2213 self.set_capital(idx, cap - pay);
2214 let pay_i128 = core::cmp::min(pay, i128::MAX as u128) as i128;
2216 self.accounts[idx].fee_credits = I128::new(self.accounts[idx].fee_credits.get()
2217 .checked_add(pay_i128).expect("fee_debt_sweep: pay <= debt guarantees no overflow"));
2218 self.insurance_fund.balance = U128::new(
2219 self.insurance_fund.balance.get().checked_add(pay)
2220 .expect("fee_debt_sweep: insurance overflow (I <= V <= MAX_VAULT_TVL)"));
2221 }
2222 }
2226 }
2227
2228 pub fn touch_account_full_not_atomic(
2233 &mut self,
2234 idx: usize,
2235 oracle_price: u64,
2236 now_slot: u64,
2237 ) -> Result<()> {
2238 if idx >= MAX_ACCOUNTS || !self.is_used(idx) {
2240 return Err(RiskError::AccountNotFound);
2241 }
2242 if now_slot < self.current_slot {
2244 return Err(RiskError::Overflow);
2245 }
2246 if now_slot < self.last_market_slot {
2247 return Err(RiskError::Overflow);
2248 }
2249 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2250 return Err(RiskError::Overflow);
2251 }
2252
2253 self.current_slot = now_slot;
2255
2256 self.accrue_market_to(now_slot, oracle_price)?;
2258
2259 self.advance_profit_warmup(idx);
2261
2262 self.settle_side_effects(idx)?;
2264
2265 self.settle_losses(idx);
2267
2268 if self.effective_pos_q(idx) == 0 && self.accounts[idx].pnl < 0 {
2270 self.resolve_flat_negative(idx);
2271 }
2272
2273 self.settle_maintenance_fee_internal(idx, now_slot)?;
2275
2276 if self.accounts[idx].position_basis_q == 0 {
2278 self.do_profit_conversion(idx);
2279 }
2280
2281 self.fee_debt_sweep(idx);
2283
2284 Ok(())
2285 }
2286
2287 fn settle_maintenance_fee_internal(&mut self, idx: usize, now_slot: u64) -> Result<()> {
2289 let fee_per_slot = self.params.maintenance_fee_per_slot.get();
2290 if fee_per_slot == 0 {
2291 self.accounts[idx].last_fee_slot = now_slot;
2292 return Ok(());
2293 }
2294
2295 let last = self.accounts[idx].last_fee_slot;
2296 let dt_fee = now_slot.saturating_sub(last);
2297 if dt_fee == 0 {
2298 self.accounts[idx].last_fee_slot = now_slot;
2299 return Ok(());
2300 }
2301
2302 let fee_due = (dt_fee as u128)
2304 .checked_mul(fee_per_slot)
2305 .ok_or(RiskError::Overflow)?;
2306
2307 if fee_due > MAX_PROTOCOL_FEE_ABS {
2309 return Err(RiskError::Overflow);
2310 }
2311
2312 if fee_due > 0 {
2314 self.charge_fee_to_insurance(idx, fee_due)?;
2315 }
2316
2317 self.accounts[idx].last_fee_slot = now_slot;
2319
2320 Ok(())
2321 }
2322
2323 test_visible! {
2328 fn add_user(&mut self, fee_payment: u128) -> Result<u16> {
2329 let used_count = self.num_used_accounts as u64;
2330 if used_count >= self.params.max_accounts {
2331 return Err(RiskError::Overflow);
2332 }
2333
2334 let required_fee = self.params.new_account_fee.get();
2335 if fee_payment < required_fee {
2336 return Err(RiskError::InsufficientBalance);
2337 }
2338
2339 let v_candidate = self.vault.get().checked_add(fee_payment)
2341 .ok_or(RiskError::Overflow)?;
2342 if v_candidate > MAX_VAULT_TVL {
2343 return Err(RiskError::Overflow);
2344 }
2345
2346 self.materialized_account_count = self.materialized_account_count
2349 .checked_add(1).ok_or(RiskError::Overflow)?;
2350 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2351 self.materialized_account_count -= 1;
2352 return Err(RiskError::Overflow);
2353 }
2354
2355 let idx = match self.alloc_slot() {
2356 Ok(i) => i,
2357 Err(e) => {
2358 self.materialized_account_count -= 1;
2359 return Err(e);
2360 }
2361 };
2362
2363 let excess = fee_payment.saturating_sub(required_fee);
2365 self.vault = U128::new(v_candidate);
2366 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2367
2368 let account_id = self.next_account_id;
2369 self.next_account_id = self.next_account_id.saturating_add(1);
2370
2371 self.accounts[idx as usize] = Account {
2372 kind: Account::KIND_USER,
2373 account_id,
2374 capital: U128::new(excess),
2375 pnl: 0i128,
2376 reserved_pnl: 0u128,
2377 warmup_started_at_slot: self.current_slot,
2378 warmup_slope_per_step: 0u128,
2379 position_basis_q: 0i128,
2380 adl_a_basis: ADL_ONE,
2381 adl_k_snap: 0i128,
2382 adl_epoch_snap: 0,
2383 matcher_program: [0; 32],
2384 matcher_context: [0; 32],
2385 owner: [0; 32],
2386 fee_credits: I128::ZERO,
2387 last_fee_slot: self.current_slot,
2388 fees_earned_total: U128::ZERO,
2389 };
2390
2391 if excess > 0 {
2392 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2393 .ok_or(RiskError::Overflow)?);
2394 }
2395
2396 Ok(idx)
2397 }
2398 }
2399
2400 test_visible! {
2401 fn add_lp(
2402 &mut self,
2403 matching_engine_program: [u8; 32],
2404 matching_engine_context: [u8; 32],
2405 fee_payment: u128,
2406 ) -> Result<u16> {
2407 let used_count = self.num_used_accounts as u64;
2408 if used_count >= self.params.max_accounts {
2409 return Err(RiskError::Overflow);
2410 }
2411
2412 let required_fee = self.params.new_account_fee.get();
2413 if fee_payment < required_fee {
2414 return Err(RiskError::InsufficientBalance);
2415 }
2416
2417 let v_candidate = self.vault.get().checked_add(fee_payment)
2419 .ok_or(RiskError::Overflow)?;
2420 if v_candidate > MAX_VAULT_TVL {
2421 return Err(RiskError::Overflow);
2422 }
2423
2424 self.materialized_account_count = self.materialized_account_count
2426 .checked_add(1).ok_or(RiskError::Overflow)?;
2427 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2428 self.materialized_account_count -= 1;
2429 return Err(RiskError::Overflow);
2430 }
2431
2432 let idx = match self.alloc_slot() {
2433 Ok(i) => i,
2434 Err(e) => {
2435 self.materialized_account_count -= 1;
2436 return Err(e);
2437 }
2438 };
2439
2440 let excess = fee_payment.saturating_sub(required_fee);
2442 self.vault = U128::new(v_candidate);
2443 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2444
2445 let account_id = self.next_account_id;
2446 self.next_account_id = self.next_account_id.saturating_add(1);
2447
2448 self.accounts[idx as usize] = Account {
2449 kind: Account::KIND_LP,
2450 account_id,
2451 capital: U128::new(excess),
2452 pnl: 0i128,
2453 reserved_pnl: 0u128,
2454 warmup_started_at_slot: self.current_slot,
2455 warmup_slope_per_step: 0u128,
2456 position_basis_q: 0i128,
2457 adl_a_basis: ADL_ONE,
2458 adl_k_snap: 0i128,
2459 adl_epoch_snap: 0,
2460 matcher_program: matching_engine_program,
2461 matcher_context: matching_engine_context,
2462 owner: [0; 32],
2463 fee_credits: I128::ZERO,
2464 last_fee_slot: self.current_slot,
2465 fees_earned_total: U128::ZERO,
2466 };
2467
2468 if excess > 0 {
2469 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2470 .ok_or(RiskError::Overflow)?);
2471 }
2472
2473 Ok(idx)
2474 }
2475 }
2476
2477 pub fn set_owner(&mut self, idx: u16, owner: [u8; 32]) -> Result<()> {
2478 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
2479 return Err(RiskError::Unauthorized);
2480 }
2481 if self.accounts[idx as usize].owner != [0u8; 32] {
2485 return Err(RiskError::Unauthorized);
2486 }
2487 self.accounts[idx as usize].owner = owner;
2488 Ok(())
2489 }
2490
2491 pub fn deposit(
2496 &mut self,
2497 idx: u16,
2498 amount: u128,
2499 _oracle_price: u64,
2500 now_slot: u64,
2501 ) -> Result<()> {
2502 if now_slot < self.current_slot {
2504 return Err(RiskError::Overflow);
2505 }
2506 if now_slot < self.last_market_slot {
2507 return Err(RiskError::Overflow);
2508 }
2509
2510 let v_candidate = self
2512 .vault
2513 .get()
2514 .checked_add(amount)
2515 .ok_or(RiskError::Overflow)?;
2516 if v_candidate > MAX_VAULT_TVL {
2517 return Err(RiskError::Overflow);
2518 }
2519
2520 if !self.is_used(idx as usize) {
2523 let min_dep = self.params.min_initial_deposit.get();
2524 if amount < min_dep {
2525 return Err(RiskError::InsufficientBalance);
2526 }
2527 self.materialize_at(idx, now_slot)?;
2528 }
2529
2530 self.current_slot = now_slot;
2532 self.vault = U128::new(v_candidate);
2533
2534 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), amount);
2536 self.set_capital(idx as usize, new_cap);
2537
2538 self.settle_losses(idx as usize);
2540
2541 if self.accounts[idx as usize].position_basis_q == 0 && self.accounts[idx as usize].pnl >= 0
2550 {
2551 self.fee_debt_sweep(idx as usize);
2552 }
2553
2554 Ok(())
2555 }
2556
2557 pub fn withdraw_not_atomic(
2562 &mut self,
2563 idx: u16,
2564 amount: u128,
2565 oracle_price: u64,
2566 now_slot: u64,
2567 funding_rate: i64,
2568 ) -> Result<()> {
2569 Self::validate_funding_rate(funding_rate)?;
2570
2571 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2572 return Err(RiskError::Overflow);
2573 }
2574
2575 if !self.is_used(idx as usize) {
2580 return Err(RiskError::AccountNotFound);
2581 }
2582
2583 let mut ctx = InstructionContext::new();
2584
2585 self.touch_account_full_not_atomic(idx as usize, oracle_price, now_slot)?;
2587
2588 if self.accounts[idx as usize].capital.get() < amount {
2590 return Err(RiskError::InsufficientBalance);
2591 }
2592
2593 let post_cap = self.accounts[idx as usize].capital.get() - amount;
2595 if post_cap != 0 && post_cap < self.params.min_initial_deposit.get() {
2596 return Err(RiskError::InsufficientBalance);
2597 }
2598
2599 let eff = self.effective_pos_q(idx as usize);
2601 if eff != 0 {
2602 let old_cap = self.accounts[idx as usize].capital.get();
2604 let old_vault = self.vault;
2605 self.set_capital(idx as usize, post_cap);
2606 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2607 let passes_im = self.is_above_initial_margin(
2608 &self.accounts[idx as usize],
2609 idx as usize,
2610 oracle_price,
2611 );
2612 self.set_capital(idx as usize, old_cap);
2614 self.vault = old_vault;
2615 if !passes_im {
2616 return Err(RiskError::Undercollateralized);
2617 }
2618 }
2619
2620 self.set_capital(
2622 idx as usize,
2623 self.accounts[idx as usize].capital.get() - amount,
2624 );
2625 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2626
2627 self.schedule_end_of_instruction_resets(&mut ctx)?;
2629 self.finalize_end_of_instruction_resets(&ctx);
2630 self.recompute_r_last_from_final_state(funding_rate)?;
2631
2632 Ok(())
2633 }
2634
2635 pub fn settle_account_not_atomic(
2642 &mut self,
2643 idx: u16,
2644 oracle_price: u64,
2645 now_slot: u64,
2646 funding_rate: i64,
2647 ) -> Result<()> {
2648 Self::validate_funding_rate(funding_rate)?;
2649
2650 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2651 return Err(RiskError::Overflow);
2652 }
2653 if !self.is_used(idx as usize) {
2654 return Err(RiskError::AccountNotFound);
2655 }
2656
2657 let mut ctx = InstructionContext::new();
2658
2659 self.touch_account_full_not_atomic(idx as usize, oracle_price, now_slot)?;
2661
2662 self.schedule_end_of_instruction_resets(&mut ctx)?;
2664 self.finalize_end_of_instruction_resets(&ctx);
2665 self.recompute_r_last_from_final_state(funding_rate)?;
2666
2667 assert!(
2669 self.oi_eff_long_q == self.oi_eff_short_q,
2670 "OI_eff_long != OI_eff_short after settle"
2671 );
2672
2673 Ok(())
2674 }
2675
2676 pub fn execute_trade_not_atomic(
2681 &mut self,
2682 a: u16,
2683 b: u16,
2684 oracle_price: u64,
2685 now_slot: u64,
2686 size_q: i128,
2687 exec_price: u64,
2688 funding_rate: i64,
2689 ) -> Result<()> {
2690 Self::validate_funding_rate(funding_rate)?;
2691
2692 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2693 return Err(RiskError::Overflow);
2694 }
2695 if exec_price == 0 || exec_price > MAX_ORACLE_PRICE {
2696 return Err(RiskError::Overflow);
2697 }
2698 if size_q <= 0 {
2700 return Err(RiskError::Overflow);
2701 }
2702 if size_q as u128 > MAX_TRADE_SIZE_Q {
2703 return Err(RiskError::Overflow);
2704 }
2705
2706 let trade_notional_check =
2708 mul_div_floor_u128(size_q as u128, exec_price as u128, POS_SCALE);
2709 if trade_notional_check > MAX_ACCOUNT_NOTIONAL {
2710 return Err(RiskError::Overflow);
2711 }
2712
2713 if !self.is_used(a as usize) || !self.is_used(b as usize) {
2718 return Err(RiskError::AccountNotFound);
2719 }
2720 if a == b {
2721 return Err(RiskError::Overflow);
2722 }
2723
2724 let mut ctx = InstructionContext::new();
2725
2726 self.touch_account_full_not_atomic(a as usize, oracle_price, now_slot)?;
2728 self.touch_account_full_not_atomic(b as usize, oracle_price, now_slot)?;
2729
2730 let old_eff_a = self.effective_pos_q(a as usize);
2732 let old_eff_b = self.effective_pos_q(b as usize);
2733
2734 let mm_req_pre_a = if old_eff_a == 0 {
2737 0u128
2738 } else {
2739 let not = self.notional(a 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 mm_req_pre_b = if old_eff_b == 0 {
2746 0u128
2747 } else {
2748 let not = self.notional(b as usize, oracle_price);
2749 core::cmp::max(
2750 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
2751 self.params.min_nonzero_mm_req,
2752 )
2753 };
2754 let maint_raw_wide_pre_a = self.account_equity_maint_raw_wide(&self.accounts[a as usize]);
2755 let maint_raw_wide_pre_b = self.account_equity_maint_raw_wide(&self.accounts[b as usize]);
2756 let buffer_pre_a = maint_raw_wide_pre_a
2757 .checked_sub(I256::from_u128(mm_req_pre_a))
2758 .expect("I256 sub");
2759 let buffer_pre_b = maint_raw_wide_pre_b
2760 .checked_sub(I256::from_u128(mm_req_pre_b))
2761 .expect("I256 sub");
2762
2763 let new_eff_a = old_eff_a.checked_add(size_q).ok_or(RiskError::Overflow)?;
2765 let neg_size_q = size_q.checked_neg().ok_or(RiskError::Overflow)?;
2766 let new_eff_b = old_eff_b
2767 .checked_add(neg_size_q)
2768 .ok_or(RiskError::Overflow)?;
2769
2770 if new_eff_a != 0 && new_eff_a.unsigned_abs() > MAX_POSITION_ABS_Q {
2772 return Err(RiskError::Overflow);
2773 }
2774 if new_eff_b != 0 && new_eff_b.unsigned_abs() > MAX_POSITION_ABS_Q {
2775 return Err(RiskError::Overflow);
2776 }
2777
2778 {
2780 let notional_a =
2781 mul_div_floor_u128(new_eff_a.unsigned_abs(), oracle_price as u128, POS_SCALE);
2782 if notional_a > MAX_ACCOUNT_NOTIONAL {
2783 return Err(RiskError::Overflow);
2784 }
2785 let notional_b =
2786 mul_div_floor_u128(new_eff_b.unsigned_abs(), oracle_price as u128, POS_SCALE);
2787 if notional_b > MAX_ACCOUNT_NOTIONAL {
2788 return Err(RiskError::Overflow);
2789 }
2790 }
2791
2792 self.maybe_finalize_ready_reset_sides();
2795
2796 let (oi_long_after, oi_short_after) =
2799 self.bilateral_oi_after(&old_eff_a, &new_eff_a, &old_eff_b, &new_eff_b)?;
2800
2801 if oi_long_after > MAX_OI_SIDE_Q || oi_short_after > MAX_OI_SIDE_Q {
2803 return Err(RiskError::Overflow);
2804 }
2805
2806 if (self.side_mode_long == SideMode::DrainOnly
2808 || self.side_mode_long == SideMode::ResetPending)
2809 && oi_long_after > self.oi_eff_long_q
2810 {
2811 return Err(RiskError::SideBlocked);
2812 }
2813 if (self.side_mode_short == SideMode::DrainOnly
2814 || self.side_mode_short == SideMode::ResetPending)
2815 && oi_short_after > self.oi_eff_short_q
2816 {
2817 return Err(RiskError::SideBlocked);
2818 }
2819
2820 let price_diff = (oracle_price as i128) - (exec_price as i128);
2822 let trade_pnl_a = compute_trade_pnl(size_q, price_diff)?;
2823 let trade_pnl_b = trade_pnl_a.checked_neg().ok_or(RiskError::Overflow)?;
2824
2825 let old_r_a = self.accounts[a as usize].reserved_pnl;
2826 let old_r_b = self.accounts[b as usize].reserved_pnl;
2827
2828 let pnl_a = self.accounts[a as usize]
2829 .pnl
2830 .checked_add(trade_pnl_a)
2831 .ok_or(RiskError::Overflow)?;
2832 if pnl_a == i128::MIN {
2833 return Err(RiskError::Overflow);
2834 }
2835 self.set_pnl(a as usize, pnl_a);
2836
2837 let pnl_b = self.accounts[b as usize]
2838 .pnl
2839 .checked_add(trade_pnl_b)
2840 .ok_or(RiskError::Overflow)?;
2841 if pnl_b == i128::MIN {
2842 return Err(RiskError::Overflow);
2843 }
2844 self.set_pnl(b as usize, pnl_b);
2845
2846 if self.accounts[a as usize].reserved_pnl > old_r_a {
2848 self.restart_warmup_after_reserve_increase(a as usize);
2849 }
2850 if self.accounts[b as usize].reserved_pnl > old_r_b {
2851 self.restart_warmup_after_reserve_increase(b as usize);
2852 }
2853
2854 self.attach_effective_position(a as usize, new_eff_a);
2856 self.attach_effective_position(b as usize, new_eff_b);
2857
2858 self.oi_eff_long_q = oi_long_after;
2860 self.oi_eff_short_q = oi_short_after;
2861
2862 self.settle_losses(a as usize);
2865 self.settle_losses(b as usize);
2866
2867 let trade_notional =
2869 mul_div_floor_u128(size_q.unsigned_abs(), exec_price as u128, POS_SCALE);
2870 let fee = if trade_notional > 0 && self.params.trading_fee_bps > 0 {
2871 mul_div_ceil_u128(trade_notional, self.params.trading_fee_bps as u128, 10_000)
2872 } else {
2873 0
2874 };
2875
2876 let mut fee_cash_a = 0u128;
2879 let mut fee_cash_b = 0u128;
2880 let mut fee_impact_a = 0u128;
2881 let mut fee_impact_b = 0u128;
2882 if fee > 0 {
2883 if fee > MAX_PROTOCOL_FEE_ABS {
2884 return Err(RiskError::Overflow);
2885 }
2886 let (cash_a, impact_a) = self.charge_fee_to_insurance(a as usize, fee)?;
2887 let (cash_b, impact_b) = self.charge_fee_to_insurance(b as usize, fee)?;
2888 fee_cash_a = cash_a;
2889 fee_cash_b = cash_b;
2890 fee_impact_a = impact_a;
2891 fee_impact_b = impact_b;
2892 }
2893
2894 if self.accounts[a as usize].is_lp() {
2899 self.accounts[a as usize].fees_earned_total = U128::new(add_u128(
2900 self.accounts[a as usize].fees_earned_total.get(),
2901 fee_impact_b,
2902 ));
2903 }
2904 if self.accounts[b as usize].is_lp() {
2905 self.accounts[b as usize].fees_earned_total = U128::new(add_u128(
2906 self.accounts[b as usize].fees_earned_total.get(),
2907 fee_impact_a,
2908 ));
2909 }
2910
2911 if new_eff_a == 0 && self.accounts[a as usize].pnl < 0 {
2913 return Err(RiskError::Undercollateralized);
2914 }
2915 if new_eff_b == 0 && self.accounts[b as usize].pnl < 0 {
2916 return Err(RiskError::Undercollateralized);
2917 }
2918
2919 self.enforce_post_trade_margin(
2927 a as usize,
2928 b as usize,
2929 oracle_price,
2930 &old_eff_a,
2931 &new_eff_a,
2932 &old_eff_b,
2933 &new_eff_b,
2934 buffer_pre_a,
2935 buffer_pre_b,
2936 fee_impact_a,
2937 fee_impact_b,
2938 )?;
2939
2940 self.schedule_end_of_instruction_resets(&mut ctx)?;
2942 self.finalize_end_of_instruction_resets(&ctx);
2943
2944 self.recompute_r_last_from_final_state(funding_rate)?;
2946
2947 assert!(
2949 self.oi_eff_long_q == self.oi_eff_short_q,
2950 "OI_eff_long != OI_eff_short after trade"
2951 );
2952
2953 Ok(())
2954 }
2955
2956 fn charge_fee_to_insurance(&mut self, idx: usize, fee: u128) -> Result<(u128, u128)> {
2961 if fee > MAX_PROTOCOL_FEE_ABS {
2962 return Err(RiskError::Overflow);
2963 }
2964 let cap = self.accounts[idx].capital.get();
2965 let fee_paid = core::cmp::min(fee, cap);
2966 if fee_paid > 0 {
2967 self.set_capital(idx, cap - fee_paid);
2968 self.insurance_fund.balance = self.insurance_fund.balance + fee_paid;
2969 }
2970 let fee_shortfall = fee - fee_paid;
2971 if fee_shortfall > 0 {
2972 let current_fc = self.accounts[idx].fee_credits.get();
2976 let headroom = match current_fc.checked_add(i128::MAX) {
2978 Some(h) if h > 0 => h as u128,
2979 _ => 0u128, };
2981 let collectible = core::cmp::min(fee_shortfall, headroom);
2982 if collectible > 0 {
2983 let new_fc = current_fc - (collectible as i128);
2986 self.accounts[idx].fee_credits = I128::new(new_fc);
2987 }
2988 Ok((fee_paid, fee_paid + collectible))
2990 } else {
2991 Ok((fee_paid, fee_paid))
2992 }
2993 }
2994
2995 fn oi_long_component(pos: i128) -> u128 {
2997 if pos > 0 {
2998 pos as u128
2999 } else {
3000 0u128
3001 }
3002 }
3003
3004 fn oi_short_component(pos: i128) -> u128 {
3005 if pos < 0 {
3006 pos.unsigned_abs()
3007 } else {
3008 0u128
3009 }
3010 }
3011
3012 fn bilateral_oi_after(
3015 &self,
3016 old_a: &i128,
3017 new_a: &i128,
3018 old_b: &i128,
3019 new_b: &i128,
3020 ) -> Result<(u128, u128)> {
3021 let oi_long_after = self
3022 .oi_eff_long_q
3023 .checked_sub(Self::oi_long_component(*old_a))
3024 .ok_or(RiskError::CorruptState)?
3025 .checked_sub(Self::oi_long_component(*old_b))
3026 .ok_or(RiskError::CorruptState)?
3027 .checked_add(Self::oi_long_component(*new_a))
3028 .ok_or(RiskError::Overflow)?
3029 .checked_add(Self::oi_long_component(*new_b))
3030 .ok_or(RiskError::Overflow)?;
3031
3032 let oi_short_after = self
3033 .oi_eff_short_q
3034 .checked_sub(Self::oi_short_component(*old_a))
3035 .ok_or(RiskError::CorruptState)?
3036 .checked_sub(Self::oi_short_component(*old_b))
3037 .ok_or(RiskError::CorruptState)?
3038 .checked_add(Self::oi_short_component(*new_a))
3039 .ok_or(RiskError::Overflow)?
3040 .checked_add(Self::oi_short_component(*new_b))
3041 .ok_or(RiskError::Overflow)?;
3042
3043 Ok((oi_long_after, oi_short_after))
3044 }
3045
3046 fn check_side_mode_for_trade(
3049 &self,
3050 old_a: &i128,
3051 new_a: &i128,
3052 old_b: &i128,
3053 new_b: &i128,
3054 ) -> Result<()> {
3055 let (oi_long_after, oi_short_after) =
3056 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
3057
3058 for &side in &[Side::Long, Side::Short] {
3059 let mode = self.get_side_mode(side);
3060 if mode != SideMode::DrainOnly && mode != SideMode::ResetPending {
3061 continue;
3062 }
3063 let (oi_after, oi_before) = match side {
3064 Side::Long => (oi_long_after, self.oi_eff_long_q),
3065 Side::Short => (oi_short_after, self.oi_eff_short_q),
3066 };
3067 if oi_after > oi_before {
3068 return Err(RiskError::SideBlocked);
3069 }
3070 }
3071 Ok(())
3072 }
3073
3074 fn enforce_post_trade_margin(
3077 &self,
3078 a: usize,
3079 b: usize,
3080 oracle_price: u64,
3081 old_eff_a: &i128,
3082 new_eff_a: &i128,
3083 old_eff_b: &i128,
3084 new_eff_b: &i128,
3085 buffer_pre_a: I256,
3086 buffer_pre_b: I256,
3087 fee_a: u128,
3088 fee_b: u128,
3089 ) -> Result<()> {
3090 self.enforce_one_side_margin(a, oracle_price, old_eff_a, new_eff_a, buffer_pre_a, fee_a)?;
3091 self.enforce_one_side_margin(b, oracle_price, old_eff_b, new_eff_b, buffer_pre_b, fee_b)?;
3092 Ok(())
3093 }
3094
3095 fn enforce_one_side_margin(
3096 &self,
3097 idx: usize,
3098 oracle_price: u64,
3099 old_eff: &i128,
3100 new_eff: &i128,
3101 buffer_pre: I256,
3102 fee: u128,
3103 ) -> Result<()> {
3104 if *new_eff == 0 {
3105 let maint_raw = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3108 if maint_raw.is_negative() {
3109 return Err(RiskError::Undercollateralized);
3110 }
3111 return Ok(());
3112 }
3113
3114 let abs_old: u128 = if *old_eff == 0 {
3115 0u128
3116 } else {
3117 old_eff.unsigned_abs()
3118 };
3119 let abs_new = new_eff.unsigned_abs();
3120
3121 let risk_increasing = abs_new > abs_old
3123 || (*old_eff > 0 && *new_eff < 0)
3124 || (*old_eff < 0 && *new_eff > 0)
3125 || *old_eff == 0;
3126
3127 let strictly_reducing = *old_eff != 0
3129 && *new_eff != 0
3130 && ((*old_eff > 0 && *new_eff > 0) || (*old_eff < 0 && *new_eff < 0))
3131 && abs_new < abs_old;
3132
3133 if risk_increasing {
3134 if !self.is_above_initial_margin(&self.accounts[idx], idx, oracle_price) {
3136 return Err(RiskError::Undercollateralized);
3137 }
3138 } else if self.is_above_maintenance_margin(&self.accounts[idx], idx, oracle_price) {
3139 } else if strictly_reducing {
3141 let maint_raw_wide_post = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3146 let fee_wide = I256::from_u128(fee);
3147
3148 let maint_raw_fee_neutral =
3150 maint_raw_wide_post.checked_add(fee_wide).expect("I256 add");
3151 let mm_req_post = {
3152 let not = self.notional(idx, oracle_price);
3153 core::cmp::max(
3154 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
3155 self.params.min_nonzero_mm_req,
3156 )
3157 };
3158 let buffer_post_fee_neutral = maint_raw_fee_neutral
3159 .checked_sub(I256::from_u128(mm_req_post))
3160 .expect("I256 sub");
3161
3162 let mm_req_pre = {
3164 let not_pre = if *old_eff == 0 {
3165 0u128
3166 } else {
3167 mul_div_floor_u128(old_eff.unsigned_abs(), oracle_price as u128, POS_SCALE)
3168 };
3169 core::cmp::max(
3170 mul_div_floor_u128(not_pre, self.params.maintenance_margin_bps as u128, 10_000),
3171 self.params.min_nonzero_mm_req,
3172 )
3173 };
3174 let maint_raw_pre = buffer_pre
3175 .checked_add(I256::from_u128(mm_req_pre))
3176 .expect("I256 add");
3177
3178 let cond1 = buffer_post_fee_neutral > buffer_pre;
3180
3181 let zero = I256::from_i128(0);
3184 let shortfall_post = if maint_raw_fee_neutral < zero {
3185 maint_raw_fee_neutral
3186 } else {
3187 zero
3188 };
3189 let shortfall_pre = if maint_raw_pre < zero {
3190 maint_raw_pre
3191 } else {
3192 zero
3193 };
3194 let cond2 = shortfall_post >= shortfall_pre;
3195
3196 if cond1 && cond2 {
3197 } else {
3199 return Err(RiskError::Undercollateralized);
3200 }
3201 } else {
3202 return Err(RiskError::Undercollateralized);
3203 }
3204 Ok(())
3205 }
3206
3207 fn update_oi_from_positions(
3210 &mut self,
3211 old_a: &i128,
3212 new_a: &i128,
3213 old_b: &i128,
3214 new_b: &i128,
3215 ) -> Result<()> {
3216 let (oi_long_after, oi_short_after) =
3217 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
3218
3219 if oi_long_after > MAX_OI_SIDE_Q {
3221 return Err(RiskError::Overflow);
3222 }
3223 if oi_short_after > MAX_OI_SIDE_Q {
3224 return Err(RiskError::Overflow);
3225 }
3226
3227 self.oi_eff_long_q = oi_long_after;
3228 self.oi_eff_short_q = oi_short_after;
3229
3230 Ok(())
3231 }
3232
3233 pub fn liquidate_at_oracle_not_atomic(
3240 &mut self,
3241 idx: u16,
3242 now_slot: u64,
3243 oracle_price: u64,
3244 policy: LiquidationPolicy,
3245 funding_rate: i64,
3246 ) -> Result<bool> {
3247 Self::validate_funding_rate(funding_rate)?;
3248
3249 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3252 return Ok(false);
3253 }
3254
3255 let mut ctx = InstructionContext::new();
3256
3257 self.touch_account_full_not_atomic(idx as usize, oracle_price, now_slot)?;
3259
3260 let result =
3261 self.liquidate_at_oracle_internal(idx, now_slot, oracle_price, policy, &mut ctx)?;
3262
3263 self.schedule_end_of_instruction_resets(&mut ctx)?;
3266 self.finalize_end_of_instruction_resets(&ctx);
3267 self.recompute_r_last_from_final_state(funding_rate)?;
3268
3269 assert!(
3271 self.oi_eff_long_q == self.oi_eff_short_q,
3272 "OI_eff_long != OI_eff_short after liquidation"
3273 );
3274 Ok(result)
3275 }
3276
3277 fn liquidate_at_oracle_internal(
3281 &mut self,
3282 idx: u16,
3283 _now_slot: u64,
3284 oracle_price: u64,
3285 policy: LiquidationPolicy,
3286 ctx: &mut InstructionContext,
3287 ) -> Result<bool> {
3288 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3289 return Ok(false);
3290 }
3291
3292 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3293 return Err(RiskError::Overflow);
3294 }
3295
3296 let old_eff = self.effective_pos_q(idx as usize);
3298 if old_eff == 0 {
3299 return Ok(false);
3300 }
3301
3302 if self.is_above_maintenance_margin(
3304 &self.accounts[idx as usize],
3305 idx as usize,
3306 oracle_price,
3307 ) {
3308 return Ok(false);
3309 }
3310
3311 let liq_side = side_of_i128(old_eff).unwrap();
3312 let abs_old_eff = old_eff.unsigned_abs();
3313
3314 match policy {
3315 LiquidationPolicy::ExactPartial(q_close_q) => {
3316 if q_close_q == 0 || q_close_q >= abs_old_eff {
3319 return Err(RiskError::Overflow);
3320 }
3321 let new_eff_abs_q = abs_old_eff
3323 .checked_sub(q_close_q)
3324 .ok_or(RiskError::Overflow)?;
3325 if new_eff_abs_q == 0 {
3327 return Err(RiskError::Overflow);
3328 }
3329 let sign = if old_eff > 0 { 1i128 } else { -1i128 };
3331 let new_eff = sign
3332 .checked_mul(new_eff_abs_q as i128)
3333 .ok_or(RiskError::Overflow)?;
3334
3335 self.attach_effective_position(idx as usize, new_eff);
3337
3338 self.settle_losses(idx as usize);
3340
3341 let liq_fee = {
3343 let notional_val =
3344 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3345 let liq_fee_raw = mul_div_ceil_u128(
3346 notional_val,
3347 self.params.liquidation_fee_bps as u128,
3348 10_000,
3349 );
3350 core::cmp::min(
3351 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3352 self.params.liquidation_fee_cap.get(),
3353 )
3354 };
3355 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3356
3357 self.enqueue_adl(ctx, liq_side, q_close_q, 0)?;
3359
3360 if !self.is_above_maintenance_margin(
3366 &self.accounts[idx as usize],
3367 idx as usize,
3368 oracle_price,
3369 ) {
3370 return Err(RiskError::Undercollateralized);
3371 }
3372
3373 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3374 Ok(true)
3375 }
3376 LiquidationPolicy::FullClose => {
3377 let q_close_q = abs_old_eff;
3379
3380 self.attach_effective_position(idx as usize, 0i128);
3382
3383 self.settle_losses(idx as usize);
3385
3386 let liq_fee = if q_close_q == 0 {
3388 0u128
3389 } else {
3390 let notional_val =
3391 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3392 let liq_fee_raw = mul_div_ceil_u128(
3393 notional_val,
3394 self.params.liquidation_fee_bps as u128,
3395 10_000,
3396 );
3397 core::cmp::min(
3398 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3399 self.params.liquidation_fee_cap.get(),
3400 )
3401 };
3402 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3403
3404 let eff_post = self.effective_pos_q(idx as usize);
3406 let d: u128 = if eff_post == 0 && self.accounts[idx as usize].pnl < 0 {
3407 assert!(
3408 self.accounts[idx as usize].pnl != i128::MIN,
3409 "liquidate: i128::MIN pnl"
3410 );
3411 self.accounts[idx as usize].pnl.unsigned_abs()
3412 } else {
3413 0u128
3414 };
3415
3416 if q_close_q != 0 || d != 0 {
3418 self.enqueue_adl(ctx, liq_side, q_close_q, d)?;
3419 }
3420
3421 if d != 0 {
3423 self.set_pnl(idx as usize, 0i128);
3424 }
3425
3426 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3427 Ok(true)
3428 }
3429 }
3430 }
3431
3432 pub fn keeper_crank_not_atomic(
3440 &mut self,
3441 now_slot: u64,
3442 oracle_price: u64,
3443 ordered_candidates: &[(u16, Option<LiquidationPolicy>)],
3444 max_revalidations: u16,
3445 funding_rate: i64,
3446 ) -> Result<CrankOutcome> {
3447 Self::validate_funding_rate(funding_rate)?;
3448
3449 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3450 return Err(RiskError::Overflow);
3451 }
3452
3453 let mut ctx = InstructionContext::new();
3455
3456 if now_slot < self.current_slot {
3458 return Err(RiskError::Overflow);
3459 }
3460 if now_slot < self.last_market_slot {
3461 return Err(RiskError::Overflow);
3462 }
3463
3464 self.accrue_market_to(now_slot, oracle_price)?;
3466
3467 self.current_slot = now_slot;
3469
3470 let advanced = now_slot > self.last_crank_slot;
3471 if advanced {
3472 self.last_crank_slot = now_slot;
3473 }
3474
3475 let mut attempts: u16 = 0;
3477 let mut num_liquidations: u32 = 0;
3478
3479 for &(candidate_idx, ref hint) in ordered_candidates {
3480 if attempts >= max_revalidations {
3482 break;
3483 }
3484 if ctx.pending_reset_long || ctx.pending_reset_short {
3486 break;
3487 }
3488 if (candidate_idx as usize) >= MAX_ACCOUNTS || !self.is_used(candidate_idx as usize) {
3490 continue;
3491 }
3492
3493 attempts += 1;
3495 let cidx = candidate_idx as usize;
3496
3497 self.advance_profit_warmup(cidx);
3502
3503 self.settle_side_effects(cidx)?;
3505
3506 self.settle_losses(cidx);
3508
3509 if self.effective_pos_q(cidx) == 0 && self.accounts[cidx].pnl < 0 {
3511 self.resolve_flat_negative(cidx);
3512 }
3513
3514 self.settle_maintenance_fee_internal(cidx, now_slot)?;
3516
3517 if self.accounts[cidx].position_basis_q == 0 {
3519 self.do_profit_conversion(cidx);
3520 }
3521
3522 self.fee_debt_sweep(cidx);
3524
3525 if !ctx.pending_reset_long && !ctx.pending_reset_short {
3528 let eff = self.effective_pos_q(cidx);
3529 if eff != 0 {
3530 if !self.is_above_maintenance_margin(&self.accounts[cidx], cidx, oracle_price) {
3531 if let Some(policy) =
3535 self.validate_keeper_hint(candidate_idx, eff, hint, oracle_price)
3536 {
3537 match self.liquidate_at_oracle_internal(
3538 candidate_idx,
3539 now_slot,
3540 oracle_price,
3541 policy,
3542 &mut ctx,
3543 ) {
3544 Ok(true) => {
3545 num_liquidations += 1;
3546 }
3547 Ok(false) => {}
3548 Err(e) => return Err(e),
3549 }
3550 }
3551 }
3552 }
3553 }
3554 }
3555
3556 self.schedule_end_of_instruction_resets(&mut ctx)?;
3558 self.finalize_end_of_instruction_resets(&ctx);
3559
3560 self.recompute_r_last_from_final_state(funding_rate)?;
3562
3563 assert!(
3565 self.oi_eff_long_q == self.oi_eff_short_q,
3566 "OI_eff_long != OI_eff_short after keeper_crank_not_atomic"
3567 );
3568
3569 Ok(CrankOutcome {
3570 advanced,
3571 slots_forgiven: 0,
3572 caller_settle_ok: true,
3573 force_realize_needed: false,
3574 panic_needed: false,
3575 num_liquidations,
3576 num_liq_errors: 0,
3577 num_gc_closed: 0,
3578 last_cursor: 0,
3579 sweep_complete: false,
3580 })
3581 }
3582
3583 test_visible! {
3593 fn validate_keeper_hint(
3594 &self,
3595 idx: u16,
3596 eff: i128,
3597 hint: &Option<LiquidationPolicy>,
3598 oracle_price: u64,
3599 ) -> Option<LiquidationPolicy> {
3600 match hint {
3601 None => None,
3603 Some(LiquidationPolicy::FullClose) => Some(LiquidationPolicy::FullClose),
3604 Some(LiquidationPolicy::ExactPartial(q_close_q)) => {
3605 let abs_eff = eff.unsigned_abs();
3606 if *q_close_q == 0 || *q_close_q >= abs_eff {
3609 return None;
3610 }
3611
3612 let account = &self.accounts[idx as usize];
3614
3615 let notional_closed = mul_div_floor_u128(*q_close_q, oracle_price as u128, POS_SCALE);
3617 let liq_fee_raw = mul_div_ceil_u128(notional_closed, self.params.liquidation_fee_bps as u128, 10_000);
3618 let liq_fee = core::cmp::min(
3619 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3620 self.params.liquidation_fee_cap.get(),
3621 );
3622
3623 let cap = account.capital.get();
3627 let fee_from_capital = core::cmp::min(liq_fee, cap);
3628 let fee_shortfall = liq_fee - fee_from_capital;
3629 let current_fc = account.fee_credits.get();
3630 let fc_headroom = match current_fc.checked_add(i128::MAX) {
3631 Some(h) if h > 0 => h as u128,
3632 _ => 0u128,
3633 };
3634 let fee_from_debt = core::cmp::min(fee_shortfall, fc_headroom);
3635 let fee_applied = fee_from_capital + fee_from_debt;
3636
3637 let eq_raw_wide = self.account_equity_maint_raw_wide(account);
3638 let predicted_eq = match eq_raw_wide.checked_sub(I256::from_u128(fee_applied)) {
3639 Some(v) => v,
3640 None => return None,
3641 };
3642
3643 let rem_eff = abs_eff - *q_close_q;
3645 let rem_notional = mul_div_floor_u128(rem_eff, oracle_price as u128, POS_SCALE);
3646 let proportional_mm = mul_div_floor_u128(rem_notional, self.params.maintenance_margin_bps as u128, 10_000);
3647 let predicted_mm_req = if rem_eff == 0 {
3648 0u128
3649 } else {
3650 core::cmp::max(proportional_mm, self.params.min_nonzero_mm_req)
3651 };
3652
3653 if predicted_eq <= I256::from_u128(predicted_mm_req) {
3656 return None;
3657 }
3658
3659 Some(LiquidationPolicy::ExactPartial(*q_close_q))
3660 }
3661 }
3662 }
3663 }
3664
3665 pub fn convert_released_pnl_not_atomic(
3671 &mut self,
3672 idx: u16,
3673 x_req: u128,
3674 oracle_price: u64,
3675 now_slot: u64,
3676 funding_rate: i64,
3677 ) -> Result<()> {
3678 Self::validate_funding_rate(funding_rate)?;
3679
3680 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3681 return Err(RiskError::Overflow);
3682 }
3683 if !self.is_used(idx as usize) {
3684 return Err(RiskError::AccountNotFound);
3685 }
3686
3687 let mut ctx = InstructionContext::new();
3688
3689 self.touch_account_full_not_atomic(idx as usize, oracle_price, now_slot)?;
3691
3692 if self.accounts[idx as usize].position_basis_q == 0 {
3694 self.schedule_end_of_instruction_resets(&mut ctx)?;
3695 self.finalize_end_of_instruction_resets(&ctx);
3696 self.recompute_r_last_from_final_state(funding_rate)?;
3697 return Ok(());
3698 }
3699
3700 let released = self.released_pos(idx as usize);
3702 if x_req == 0 || x_req > released {
3703 return Err(RiskError::Overflow);
3704 }
3705
3706 let (h_num, h_den) = self.haircut_ratio();
3709 assert!(
3710 h_den > 0,
3711 "convert_released_pnl_not_atomic: h_den must be > 0 when x_req > 0"
3712 );
3713 let y: u128 = wide_mul_div_floor_u128(x_req, h_num, h_den);
3714
3715 self.consume_released_pnl(idx as usize, x_req);
3717
3718 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), y);
3720 self.set_capital(idx as usize, new_cap);
3721
3722 self.fee_debt_sweep(idx as usize);
3724
3725 let eff = self.effective_pos_q(idx as usize);
3727 if eff != 0 {
3728 if !self.is_above_maintenance_margin(
3729 &self.accounts[idx as usize],
3730 idx as usize,
3731 oracle_price,
3732 ) {
3733 return Err(RiskError::Undercollateralized);
3734 }
3735 }
3736
3737 self.schedule_end_of_instruction_resets(&mut ctx)?;
3739 self.finalize_end_of_instruction_resets(&ctx);
3740 self.recompute_r_last_from_final_state(funding_rate)?;
3741
3742 Ok(())
3743 }
3744
3745 pub fn close_account_not_atomic(
3750 &mut self,
3751 idx: u16,
3752 now_slot: u64,
3753 oracle_price: u64,
3754 funding_rate: i64,
3755 ) -> Result<u128> {
3756 Self::validate_funding_rate(funding_rate)?;
3757
3758 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3759 return Err(RiskError::AccountNotFound);
3760 }
3761
3762 let mut ctx = InstructionContext::new();
3763
3764 self.touch_account_full_not_atomic(idx as usize, oracle_price, now_slot)?;
3765
3766 let eff = self.effective_pos_q(idx as usize);
3768 if eff != 0 {
3769 return Err(RiskError::Undercollateralized);
3770 }
3771
3772 if self.accounts[idx as usize].pnl > 0 {
3775 return Err(RiskError::PnlNotWarmedUp);
3776 }
3777 if self.accounts[idx as usize].pnl < 0 {
3778 return Err(RiskError::Undercollateralized);
3779 }
3780
3781 if self.accounts[idx as usize].fee_credits.get() < 0 {
3783 self.accounts[idx as usize].fee_credits = I128::ZERO;
3784 }
3785
3786 let capital = self.accounts[idx as usize].capital;
3787
3788 if capital > self.vault {
3789 return Err(RiskError::InsufficientBalance);
3790 }
3791 self.vault = self.vault - capital;
3792 self.set_capital(idx as usize, 0);
3793
3794 self.schedule_end_of_instruction_resets(&mut ctx)?;
3796 self.finalize_end_of_instruction_resets(&ctx);
3797 self.recompute_r_last_from_final_state(funding_rate)?;
3798
3799 self.free_slot(idx);
3800
3801 Ok(capital.get())
3802 }
3803
3804 pub fn force_close_resolved_not_atomic(
3820 &mut self,
3821 idx: u16,
3822 resolved_slot: u64,
3823 ) -> Result<u128> {
3824 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3825 return Err(RiskError::AccountNotFound);
3826 }
3827 if resolved_slot < self.current_slot {
3828 return Err(RiskError::Overflow);
3829 }
3830 self.current_slot = resolved_slot;
3831
3832 let i = idx as usize;
3833
3834 if self.accounts[i].position_basis_q != 0 {
3840 let basis = self.accounts[i].position_basis_q;
3841 let abs_basis = basis.unsigned_abs();
3842 let a_basis = self.accounts[i].adl_a_basis;
3843 let k_snap = self.accounts[i].adl_k_snap;
3844 let side = side_of_i128(basis).unwrap();
3845 let epoch_snap = self.accounts[i].adl_epoch_snap;
3846 let epoch_side = self.get_epoch_side(side);
3847
3848 if a_basis == 0 {
3850 return Err(RiskError::CorruptState);
3851 }
3852
3853 let k_end = if epoch_snap == epoch_side {
3855 self.get_k_side(side)
3856 } else {
3857 self.get_k_epoch_start(side)
3858 };
3859 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
3860 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_end, den);
3861
3862 let new_pnl = self.accounts[i]
3864 .pnl
3865 .checked_add(pnl_delta)
3866 .ok_or(RiskError::Overflow)?;
3867 if new_pnl == i128::MIN {
3868 return Err(RiskError::Overflow);
3869 }
3870 let eff = self.effective_pos_q(i);
3875 let eff_abs = eff.unsigned_abs();
3876
3877 if epoch_snap != epoch_side {
3878 if epoch_snap.checked_add(1) != Some(epoch_side) {
3882 return Err(RiskError::CorruptState);
3883 }
3884 let old_stale = self.get_stale_count(side);
3885 if old_stale == 0 {
3886 return Err(RiskError::CorruptState);
3887 }
3888 }
3889
3890 if pnl_delta != 0 {
3892 let old_r = self.accounts[i].reserved_pnl;
3893 self.set_pnl(i, new_pnl);
3894 if self.accounts[i].reserved_pnl > old_r {
3895 self.restart_warmup_after_reserve_increase(i);
3896 }
3897 }
3898
3899 if epoch_snap != epoch_side {
3901 let old_stale = self.get_stale_count(side);
3902 self.set_stale_count(side, old_stale - 1);
3903 }
3904
3905 if eff_abs > 0 {
3908 self.oi_eff_long_q = self.oi_eff_long_q.saturating_sub(eff_abs);
3909 self.oi_eff_short_q = self.oi_eff_short_q.saturating_sub(eff_abs);
3910 }
3911
3912 if epoch_snap == epoch_side && a_basis != 0 {
3915 let a_side_val = self.get_a_side(side);
3916 let product = U256::from_u128(abs_basis).checked_mul(U256::from_u128(a_side_val));
3917 if let Some(p) = product {
3918 let rem = p.checked_rem(U256::from_u128(a_basis));
3919 if let Some(r) = rem {
3920 if !r.is_zero() {
3921 self.inc_phantom_dust_bound(side);
3922 }
3923 }
3924 }
3925 }
3926
3927 self.set_position_basis_q(i, 0);
3929 self.accounts[i].adl_a_basis = ADL_ONE;
3930 self.accounts[i].adl_k_snap = 0;
3931 self.accounts[i].adl_epoch_snap = 0;
3932 }
3933
3934 self.settle_losses(i);
3936
3937 self.resolve_flat_negative(i);
3939
3940 self.settle_maintenance_fee_internal(i, self.current_slot)?;
3944
3945 if self.accounts[i].pnl > 0 {
3952 self.set_reserved_pnl(i, 0);
3954 let released = self.released_pos(i);
3956 if released > 0 {
3957 let (h_num, h_den) = self.haircut_ratio();
3958 let y = if h_den == 0 {
3959 released
3960 } else {
3961 wide_mul_div_floor_u128(released, h_num, h_den)
3962 };
3963 self.consume_released_pnl(i, released);
3964 let new_cap = add_u128(self.accounts[i].capital.get(), y);
3965 self.set_capital(i, new_cap);
3966 }
3967 }
3968
3969 self.fee_debt_sweep(i);
3971
3972 if self.accounts[i].fee_credits.get() < 0 {
3974 self.accounts[i].fee_credits = I128::ZERO;
3975 }
3976
3977 let capital = self.accounts[i].capital;
3979 if capital > self.vault {
3980 return Err(RiskError::InsufficientBalance);
3981 }
3982 self.vault = self.vault - capital;
3983 self.set_capital(i, 0);
3984
3985 self.free_slot(idx);
3986
3987 Ok(capital.get())
3988 }
3989
3990 pub fn reclaim_empty_account_not_atomic(&mut self, idx: u16, now_slot: u64) -> Result<()> {
3999 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4000 return Err(RiskError::AccountNotFound);
4001 }
4002 if now_slot < self.current_slot {
4003 return Err(RiskError::Overflow);
4004 }
4005
4006 let account = &self.accounts[idx as usize];
4008 if account.position_basis_q != 0 {
4009 return Err(RiskError::Undercollateralized);
4010 }
4011 if account.pnl != 0 {
4012 return Err(RiskError::Undercollateralized);
4013 }
4014 if account.reserved_pnl != 0 {
4015 return Err(RiskError::Undercollateralized);
4016 }
4017 if account.fee_credits.get() > 0 {
4018 return Err(RiskError::Undercollateralized);
4019 }
4020
4021 self.current_slot = now_slot;
4023
4024 self.settle_maintenance_fee_internal(idx as usize, now_slot)?;
4026
4027 if self.accounts[idx as usize].capital.get() >= self.params.min_initial_deposit.get()
4030 && !self.accounts[idx as usize].capital.is_zero()
4031 {
4032 return Err(RiskError::Undercollateralized);
4033 }
4034
4035 let dust_cap = self.accounts[idx as usize].capital.get();
4037 if dust_cap > 0 {
4038 self.set_capital(idx as usize, 0);
4039 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
4040 }
4041
4042 if self.accounts[idx as usize].fee_credits.get() < 0 {
4044 self.accounts[idx as usize].fee_credits = I128::new(0);
4045 }
4046
4047 self.free_slot(idx);
4049
4050 Ok(())
4051 }
4052
4053 test_visible! {
4058 fn garbage_collect_dust(&mut self) -> u32 {
4059 let mut to_free: [u16; GC_CLOSE_BUDGET as usize] = [0; GC_CLOSE_BUDGET as usize];
4060 let mut num_to_free = 0usize;
4061
4062 let max_scan = (ACCOUNTS_PER_CRANK as usize).min(MAX_ACCOUNTS);
4063 let start = self.gc_cursor as usize;
4064
4065 let mut scanned: usize = 0;
4066 for offset in 0..max_scan {
4067 if num_to_free >= GC_CLOSE_BUDGET as usize {
4068 break;
4069 }
4070 scanned = offset + 1;
4071
4072 let idx = (start + offset) & ACCOUNT_IDX_MASK;
4073 let block = idx >> 6;
4074 let bit = idx & 63;
4075 if (self.used[block] & (1u64 << bit)) == 0 {
4076 continue;
4077 }
4078
4079 let account = &self.accounts[idx];
4082 if account.position_basis_q != 0 {
4083 continue;
4084 }
4085 if account.pnl != 0 {
4086 continue;
4087 }
4088 if account.reserved_pnl != 0 {
4089 continue;
4090 }
4091 if account.fee_credits.get() > 0 {
4092 continue;
4093 }
4094
4095 if self.settle_maintenance_fee_internal(idx, self.current_slot).is_err() {
4098 continue;
4099 }
4100
4101 if self.accounts[idx].capital.get() >= self.params.min_initial_deposit.get()
4103 && !self.accounts[idx].capital.is_zero() {
4104 continue;
4105 }
4106
4107 let dust_cap = self.accounts[idx].capital.get();
4109 if dust_cap > 0 {
4110 self.set_capital(idx, 0);
4111 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
4112 }
4113
4114 if self.accounts[idx].fee_credits.get() < 0 {
4116 self.accounts[idx].fee_credits = I128::new(0);
4117 }
4118
4119 to_free[num_to_free] = idx as u16;
4120 num_to_free += 1;
4121 }
4122
4123 self.gc_cursor = ((start + scanned) & ACCOUNT_IDX_MASK) as u16;
4126
4127 for i in 0..num_to_free {
4128 self.free_slot(to_free[i]);
4129 }
4130
4131 num_to_free as u32
4132 }
4133 }
4134
4135 fn require_fresh_crank(&self, now_slot: u64) -> Result<()> {
4140 if now_slot.saturating_sub(self.last_crank_slot) > self.max_crank_staleness_slots {
4141 return Err(RiskError::Unauthorized);
4142 }
4143 Ok(())
4144 }
4145
4146 fn require_recent_full_sweep(&self, now_slot: u64) -> Result<()> {
4147 if now_slot.saturating_sub(self.last_full_sweep_start_slot) > self.max_crank_staleness_slots
4148 {
4149 return Err(RiskError::Unauthorized);
4150 }
4151 Ok(())
4152 }
4153
4154 pub fn top_up_insurance_fund(&mut self, amount: u128, now_slot: u64) -> Result<bool> {
4159 if now_slot < self.current_slot {
4161 return Err(RiskError::Overflow);
4162 }
4163 let new_vault = self
4165 .vault
4166 .get()
4167 .checked_add(amount)
4168 .ok_or(RiskError::Overflow)?;
4169 if new_vault > MAX_VAULT_TVL {
4170 return Err(RiskError::Overflow);
4171 }
4172 let new_ins = self
4173 .insurance_fund
4174 .balance
4175 .get()
4176 .checked_add(amount)
4177 .ok_or(RiskError::Overflow)?;
4178 self.current_slot = now_slot;
4180 self.vault = U128::new(new_vault);
4181 self.insurance_fund.balance = U128::new(new_ins);
4182 Ok(self.insurance_fund.balance.get() > self.params.insurance_floor.get())
4183 }
4184
4185 pub fn deposit_fee_credits(&mut self, idx: u16, amount: u128, now_slot: u64) -> Result<()> {
4193 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4194 return Err(RiskError::Unauthorized);
4195 }
4196 if now_slot < self.current_slot {
4197 return Err(RiskError::Unauthorized);
4198 }
4199 let debt = fee_debt_u128_checked(self.accounts[idx as usize].fee_credits.get());
4201 let capped = amount.min(debt);
4202 if capped == 0 {
4203 self.current_slot = now_slot;
4204 return Ok(()); }
4206 if capped > i128::MAX as u128 {
4207 return Err(RiskError::Overflow);
4208 }
4209 let new_vault = self
4210 .vault
4211 .get()
4212 .checked_add(capped)
4213 .ok_or(RiskError::Overflow)?;
4214 if new_vault > MAX_VAULT_TVL {
4215 return Err(RiskError::Overflow);
4216 }
4217 let new_ins = self
4218 .insurance_fund
4219 .balance
4220 .get()
4221 .checked_add(capped)
4222 .ok_or(RiskError::Overflow)?;
4223 let new_credits = self.accounts[idx as usize]
4224 .fee_credits
4225 .checked_add(capped as i128)
4226 .ok_or(RiskError::Overflow)?;
4227 self.current_slot = now_slot;
4229 self.vault = U128::new(new_vault);
4230 self.insurance_fund.balance = U128::new(new_ins);
4231 self.accounts[idx as usize].fee_credits = new_credits;
4232 Ok(())
4233 }
4234
4235 #[cfg(any(test, feature = "test", kani))]
4236 test_visible! {
4237 fn add_fee_credits(&mut self, idx: u16, amount: u128) -> Result<()> {
4238 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4239 return Err(RiskError::Unauthorized);
4240 }
4241 self.accounts[idx as usize].fee_credits = self.accounts[idx as usize]
4242 .fee_credits.saturating_add(amount as i128);
4243 Ok(())
4244 }
4245 }
4246
4247 test_visible! {
4252 fn recompute_aggregates(&mut self) {
4253 let mut c_tot = 0u128;
4254 let mut pnl_pos_tot = 0u128;
4255 let mut pnl_matured_pos_tot = 0u128;
4256 self.for_each_used(|_idx, account| {
4257 c_tot = c_tot.saturating_add(account.capital.get());
4258 let pos_pnl = i128_clamp_pos(account.pnl);
4259 pnl_pos_tot = pnl_pos_tot.saturating_add(pos_pnl);
4260 let released = pos_pnl.saturating_sub(account.reserved_pnl);
4261 pnl_matured_pos_tot = pnl_matured_pos_tot.saturating_add(released);
4262 });
4263 self.c_tot = U128::new(c_tot);
4264 self.pnl_pos_tot = pnl_pos_tot;
4265 self.pnl_matured_pos_tot = pnl_matured_pos_tot;
4266 }
4267 }
4268
4269 test_visible! {
4274 fn advance_slot(&mut self, slots: u64) {
4275 self.current_slot = self.current_slot.saturating_add(slots);
4276 }
4277 }
4278
4279 test_visible! {
4281 fn count_used(&self) -> u64 {
4282 let mut count = 0u64;
4283 self.for_each_used(|_, _| {
4284 count += 1;
4285 });
4286 count
4287 }
4288 }
4289}
4290
4291fn set_pending_reset(ctx: &mut InstructionContext, side: Side) {
4297 match side {
4298 Side::Long => ctx.pending_reset_long = true,
4299 Side::Short => ctx.pending_reset_short = true,
4300 }
4301}
4302
4303pub fn checked_u128_mul_i128(a: u128, b: i128) -> Result<i128> {
4306 if a == 0 || b == 0 {
4307 return Ok(0i128);
4308 }
4309 let negative = b < 0;
4310 let abs_b = if b == i128::MIN {
4311 return Err(RiskError::Overflow);
4312 } else {
4313 b.unsigned_abs()
4314 };
4315 let product = U256::from_u128(a)
4317 .checked_mul(U256::from_u128(abs_b))
4318 .ok_or(RiskError::Overflow)?;
4319 match product.try_into_u128() {
4322 Some(v) if v <= i128::MAX as u128 => {
4323 if negative {
4324 Ok(-(v as i128))
4325 } else {
4326 Ok(v as i128)
4327 }
4328 }
4329 _ => Err(RiskError::Overflow),
4330 }
4331}
4332
4333pub fn compute_trade_pnl(size_q: i128, price_diff: i128) -> Result<i128> {
4336 if size_q == 0 || price_diff == 0 {
4337 return Ok(0i128);
4338 }
4339
4340 let neg_size = size_q < 0;
4342 let neg_price = price_diff < 0;
4343 let result_negative = neg_size != neg_price;
4344
4345 let abs_size = size_q.unsigned_abs();
4346 let abs_price = price_diff.unsigned_abs();
4347
4348 let abs_size_u256 = U256::from_u128(abs_size);
4351 let abs_price_u256 = U256::from_u128(abs_price);
4352 let ps_u256 = U256::from_u128(POS_SCALE);
4353
4354 let (q, r) = mul_div_floor_u256_with_rem(abs_size_u256, abs_price_u256, ps_u256);
4356
4357 if result_negative {
4358 let mag = if !r.is_zero() {
4360 q.checked_add(U256::ONE).ok_or(RiskError::Overflow)?
4361 } else {
4362 q
4363 };
4364 match mag.try_into_u128() {
4367 Some(v) if v <= i128::MAX as u128 => Ok(-(v as i128)),
4368 _ => Err(RiskError::Overflow),
4369 }
4370 } else {
4371 match q.try_into_u128() {
4372 Some(v) if v <= i128::MAX as u128 => Ok(v as i128),
4373 _ => Err(RiskError::Overflow),
4374 }
4375 }
4376}