1#![no_std]
13#![forbid(unsafe_code)]
14
15#[cfg(kani)]
16extern crate kani;
17
18macro_rules! test_visible {
31 (
32 $(#[$meta:meta])*
33 fn $name:ident($($args:tt)*) $(-> $ret:ty)? $body:block
34 ) => {
35 $(#[$meta])*
36 #[cfg(any(feature = "test", kani))]
37 pub fn $name($($args)*) $(-> $ret)? $body
38
39 $(#[$meta])*
40 #[cfg(not(any(feature = "test", kani)))]
41 fn $name($($args)*) $(-> $ret)? $body
42 };
43}
44
45#[cfg(kani)]
50pub const MAX_ACCOUNTS: usize = 4;
51
52#[cfg(all(feature = "test", not(kani)))]
53pub const MAX_ACCOUNTS: usize = 64;
54
55#[cfg(all(not(kani), not(feature = "test")))]
56pub const MAX_ACCOUNTS: usize = 4096;
57
58pub const BITMAP_WORDS: usize = (MAX_ACCOUNTS + 63) / 64;
59pub const MAX_ROUNDING_SLACK: u128 = MAX_ACCOUNTS as u128;
60const ACCOUNT_IDX_MASK: usize = MAX_ACCOUNTS - 1;
61
62pub const GC_CLOSE_BUDGET: u32 = 32;
63pub const ACCOUNTS_PER_CRANK: u16 = 128;
64pub const LIQ_BUDGET_PER_CRANK: u16 = 64;
65
66pub const POS_SCALE: u128 = 1_000_000;
68
69pub const ADL_ONE: u128 = 1_000_000;
71
72pub const MIN_A_SIDE: u128 = 1_000;
74
75pub const MAX_ORACLE_PRICE: u64 = 1_000_000_000_000;
77
78pub const MAX_FUNDING_DT: u64 = u16::MAX as u64;
80
81pub const MAX_ABS_FUNDING_BPS_PER_SLOT: i64 = 10_000;
83
84pub const MAX_VAULT_TVL: u128 = 10_000_000_000_000_000;
86pub const MAX_POSITION_ABS_Q: u128 = 100_000_000_000_000;
87pub const MAX_ACCOUNT_NOTIONAL: u128 = 100_000_000_000_000_000_000;
88pub const MAX_TRADE_SIZE_Q: u128 = MAX_POSITION_ABS_Q; pub const MAX_OI_SIDE_Q: u128 = 100_000_000_000_000;
90pub const MAX_MATERIALIZED_ACCOUNTS: u64 = 1_000_000;
91pub const MAX_ACCOUNT_POSITIVE_PNL: u128 = 100_000_000_000_000_000_000_000_000_000_000;
92pub const MAX_PNL_POS_TOT: u128 = 100_000_000_000_000_000_000_000_000_000_000_000_000;
93pub const MAX_TRADING_FEE_BPS: u64 = 10_000;
94pub const MAX_MARGIN_BPS: u64 = 10_000;
95pub const MAX_LIQUIDATION_FEE_BPS: u64 = 10_000;
96pub const MAX_PROTOCOL_FEE_ABS: u128 = 1_000_000_000_000_000_000_000_000_000_000_000_000; pub const MAX_MAINTENANCE_FEE_PER_SLOT: u128 = 10_000_000_000_000_000; use super::i128_types::{I128, U128};
104
105use super::wide_math::{
110 ceil_div_positive_checked, fee_debt_u128_checked, floor_div_signed_conservative_i128,
111 mul_div_ceil_u128, mul_div_floor_u128, mul_div_floor_u256_with_rem, saturating_mul_u128_u64,
112 wide_mul_div_ceil_u128_or_over_i128max, wide_mul_div_floor_u128,
113 wide_signed_mul_div_floor_from_k_pair, OverI128Magnitude, I256, U256,
114};
115
116#[repr(u8)]
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum SideMode {
129 Normal = 0,
130 DrainOnly = 1,
131 ResetPending = 2,
132}
133
134pub struct InstructionContext {
136 pub pending_reset_long: bool,
137 pub pending_reset_short: bool,
138}
139
140impl InstructionContext {
141 pub fn new() -> Self {
142 Self {
143 pending_reset_long: false,
144 pending_reset_short: false,
145 }
146 }
147}
148
149#[repr(C)]
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct Account {
153 pub account_id: u64,
154 pub capital: U128,
155 pub kind: u8, pub pnl: i128,
159
160 pub reserved_pnl: u128,
162
163 pub warmup_started_at_slot: u64,
165
166 pub warmup_slope_per_step: u128,
168
169 pub position_basis_q: i128,
171
172 pub adl_a_basis: u128,
174
175 pub adl_k_snap: i128,
177
178 pub adl_epoch_snap: u64,
180
181 pub matcher_program: [u8; 32],
183 pub matcher_context: [u8; 32],
184
185 pub owner: [u8; 32],
187
188 pub fee_credits: I128,
190 pub last_fee_slot: u64,
191
192 pub fees_earned_total: U128,
194}
195
196impl Account {
197 pub const KIND_USER: u8 = 0;
198 pub const KIND_LP: u8 = 1;
199
200 pub fn is_lp(&self) -> bool {
201 self.kind == Self::KIND_LP
202 }
203
204 pub fn is_user(&self) -> bool {
205 self.kind == Self::KIND_USER
206 }
207}
208
209fn empty_account() -> Account {
210 Account {
211 account_id: 0,
212 capital: U128::ZERO,
213 kind: Account::KIND_USER,
214 pnl: 0i128,
215 reserved_pnl: 0u128,
216 warmup_started_at_slot: 0,
217 warmup_slope_per_step: 0u128,
218 position_basis_q: 0i128,
219 adl_a_basis: ADL_ONE,
220 adl_k_snap: 0i128,
221 adl_epoch_snap: 0,
222 matcher_program: [0; 32],
223 matcher_context: [0; 32],
224 owner: [0; 32],
225 fee_credits: I128::ZERO,
226 last_fee_slot: 0,
227 fees_earned_total: U128::ZERO,
228 }
229}
230
231#[repr(C)]
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub struct InsuranceFund {
235 pub balance: U128,
236}
237
238#[repr(C)]
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub struct RiskParams {
242 pub warmup_period_slots: u64,
243 pub maintenance_margin_bps: u64,
244 pub initial_margin_bps: u64,
245 pub trading_fee_bps: u64,
246 pub max_accounts: u64,
247 pub new_account_fee: U128,
248 pub maintenance_fee_per_slot: U128,
249 pub max_crank_staleness_slots: u64,
250 pub liquidation_fee_bps: u64,
251 pub liquidation_fee_cap: U128,
252 pub liquidation_buffer_bps: u64,
253 pub min_liquidation_abs: U128,
254 pub min_initial_deposit: U128,
255 pub min_nonzero_mm_req: u128,
257 pub min_nonzero_im_req: u128,
258 pub insurance_floor: U128,
260}
261
262#[repr(C)]
264#[derive(Clone, Debug, PartialEq, Eq)]
265pub struct RiskEngine {
266 pub vault: U128,
267 pub insurance_fund: InsuranceFund,
268 pub params: RiskParams,
269 pub current_slot: u64,
270
271 pub funding_rate_bps_per_slot_last: i64,
273
274 pub last_crank_slot: u64,
276 pub max_crank_staleness_slots: u64,
277
278 pub c_tot: U128,
280 pub pnl_pos_tot: u128,
281 pub pnl_matured_pos_tot: u128,
282
283 pub liq_cursor: u16,
285 pub gc_cursor: u16,
286 pub last_full_sweep_start_slot: u64,
287 pub last_full_sweep_completed_slot: u64,
288 pub crank_cursor: u16,
289 pub sweep_start_idx: u16,
290
291 pub lifetime_liquidations: u64,
293
294 pub adl_mult_long: u128,
296 pub adl_mult_short: u128,
297 pub adl_coeff_long: i128,
298 pub adl_coeff_short: i128,
299 pub adl_epoch_long: u64,
300 pub adl_epoch_short: u64,
301 pub adl_epoch_start_k_long: i128,
302 pub adl_epoch_start_k_short: i128,
303 pub oi_eff_long_q: u128,
304 pub oi_eff_short_q: u128,
305 pub side_mode_long: SideMode,
306 pub side_mode_short: SideMode,
307 pub stored_pos_count_long: u64,
308 pub stored_pos_count_short: u64,
309 pub stale_account_count_long: u64,
310 pub stale_account_count_short: u64,
311
312 pub phantom_dust_bound_long_q: u128,
314 pub phantom_dust_bound_short_q: u128,
315
316 pub materialized_account_count: u64,
318
319 pub last_oracle_price: u64,
321 pub last_market_slot: u64,
323 pub funding_price_sample_last: u64,
325
326 pub insurance_floor: u128,
328
329 pub used: [u64; BITMAP_WORDS],
331 pub num_used_accounts: u16,
332 pub next_account_id: u64,
333 pub free_head: u16,
334 pub next_free: [u16; MAX_ACCOUNTS],
335 pub accounts: [Account; MAX_ACCOUNTS],
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub enum RiskError {
344 InsufficientBalance,
345 Undercollateralized,
346 Unauthorized,
347 InvalidMatchingEngine,
348 PnlNotWarmedUp,
349 Overflow,
350 AccountNotFound,
351 NotAnLPAccount,
352 PositionSizeMismatch,
353 AccountKindMismatch,
354 SideBlocked,
355 CorruptState,
356}
357
358pub type Result<T> = core::result::Result<T, RiskError>;
359
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
362pub enum LiquidationPolicy {
363 FullClose,
364 ExactPartial(u128), }
366
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
369pub struct CrankOutcome {
370 pub advanced: bool,
371 pub slots_forgiven: u64,
372 pub caller_settle_ok: bool,
373 pub force_realize_needed: bool,
374 pub panic_needed: bool,
375 pub num_liquidations: u32,
376 pub num_liq_errors: u16,
377 pub num_gc_closed: u32,
378 pub last_cursor: u16,
379 pub sweep_complete: bool,
380}
381
382#[inline]
387fn add_u128(a: u128, b: u128) -> u128 {
388 a.checked_add(b).expect("add_u128 overflow")
389}
390
391#[inline]
392fn sub_u128(a: u128, b: u128) -> u128 {
393 a.checked_sub(b).expect("sub_u128 underflow")
394}
395
396#[inline]
397fn mul_u128(a: u128, b: u128) -> u128 {
398 a.checked_mul(b).expect("mul_u128 overflow")
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
403pub enum Side {
404 Long,
405 Short,
406}
407
408fn side_of_i128(v: i128) -> Option<Side> {
409 if v == 0 {
410 None
411 } else if v > 0 {
412 Some(Side::Long)
413 } else {
414 Some(Side::Short)
415 }
416}
417
418fn opposite_side(s: Side) -> Side {
419 match s {
420 Side::Long => Side::Short,
421 Side::Short => Side::Long,
422 }
423}
424
425fn i128_clamp_pos(v: i128) -> u128 {
427 if v > 0 {
428 v as u128
429 } else {
430 0u128
431 }
432}
433
434impl RiskEngine {
439 fn validate_params(params: &RiskParams) {
442 assert!(
444 (params.max_accounts as usize) <= MAX_ACCOUNTS && params.max_accounts > 0,
445 "max_accounts must be in 1..=MAX_ACCOUNTS"
446 );
447
448 assert!(
450 params.maintenance_margin_bps <= params.initial_margin_bps,
451 "maintenance_margin_bps must be <= initial_margin_bps (spec §1.4)"
452 );
453 assert!(
454 params.initial_margin_bps <= 10_000,
455 "initial_margin_bps must be <= 10_000"
456 );
457
458 assert!(
460 params.trading_fee_bps <= 10_000,
461 "trading_fee_bps must be <= 10_000"
462 );
463 assert!(
464 params.liquidation_fee_bps <= 10_000,
465 "liquidation_fee_bps must be <= 10_000"
466 );
467
468 assert!(
470 params.min_nonzero_mm_req > 0,
471 "min_nonzero_mm_req must be > 0"
472 );
473 assert!(
474 params.min_nonzero_mm_req < params.min_nonzero_im_req,
475 "min_nonzero_mm_req must be strictly less than min_nonzero_im_req"
476 );
477 assert!(
478 params.min_nonzero_im_req <= params.min_initial_deposit.get(),
479 "min_nonzero_im_req must be <= min_initial_deposit (spec §1.4)"
480 );
481
482 assert!(
484 params.min_initial_deposit.get() > 0,
485 "min_initial_deposit must be > 0 (spec §1.4)"
486 );
487 assert!(
488 params.min_initial_deposit.get() <= MAX_VAULT_TVL,
489 "min_initial_deposit must be <= MAX_VAULT_TVL"
490 );
491
492 assert!(
494 params.min_liquidation_abs.get() <= params.liquidation_fee_cap.get(),
495 "min_liquidation_abs must be <= liquidation_fee_cap (spec §1.4)"
496 );
497 assert!(
498 params.liquidation_fee_cap.get() <= MAX_PROTOCOL_FEE_ABS,
499 "liquidation_fee_cap must be <= MAX_PROTOCOL_FEE_ABS (spec §1.4)"
500 );
501
502 assert!(
504 params.maintenance_fee_per_slot.get() <= MAX_MAINTENANCE_FEE_PER_SLOT,
505 "maintenance_fee_per_slot must be <= MAX_MAINTENANCE_FEE_PER_SLOT (spec §8.2.1)"
506 );
507
508 assert!(
510 params.insurance_floor.get() <= MAX_VAULT_TVL,
511 "insurance_floor must be <= MAX_VAULT_TVL (spec §1.4)"
512 );
513 }
514
515 #[cfg(any(feature = "test", kani))]
518 pub fn new(params: RiskParams) -> Self {
519 Self::new_with_market(params, 0, 1)
520 }
521
522 pub fn new_with_market(params: RiskParams, init_slot: u64, init_oracle_price: u64) -> Self {
525 Self::validate_params(¶ms);
526 assert!(
527 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
528 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
529 );
530 let mut engine = Self {
531 vault: U128::ZERO,
532 insurance_fund: InsuranceFund {
533 balance: U128::ZERO,
534 },
535 params,
536 current_slot: init_slot,
537 funding_rate_bps_per_slot_last: 0,
538 last_crank_slot: 0,
539 max_crank_staleness_slots: params.max_crank_staleness_slots,
540 c_tot: U128::ZERO,
541 pnl_pos_tot: 0u128,
542 pnl_matured_pos_tot: 0u128,
543 liq_cursor: 0,
544 gc_cursor: 0,
545 last_full_sweep_start_slot: 0,
546 last_full_sweep_completed_slot: 0,
547 crank_cursor: 0,
548 sweep_start_idx: 0,
549 lifetime_liquidations: 0,
550 adl_mult_long: ADL_ONE,
551 adl_mult_short: ADL_ONE,
552 adl_coeff_long: 0i128,
553 adl_coeff_short: 0i128,
554 adl_epoch_long: 0,
555 adl_epoch_short: 0,
556 adl_epoch_start_k_long: 0i128,
557 adl_epoch_start_k_short: 0i128,
558 oi_eff_long_q: 0u128,
559 oi_eff_short_q: 0u128,
560 side_mode_long: SideMode::Normal,
561 side_mode_short: SideMode::Normal,
562 stored_pos_count_long: 0,
563 stored_pos_count_short: 0,
564 stale_account_count_long: 0,
565 stale_account_count_short: 0,
566 phantom_dust_bound_long_q: 0u128,
567 phantom_dust_bound_short_q: 0u128,
568 materialized_account_count: 0,
569 last_oracle_price: init_oracle_price,
570 last_market_slot: init_slot,
571 funding_price_sample_last: init_oracle_price,
572 insurance_floor: params.insurance_floor.get(),
573 used: [0; BITMAP_WORDS],
574 num_used_accounts: 0,
575 next_account_id: 0,
576 free_head: 0,
577 next_free: [0; MAX_ACCOUNTS],
578 accounts: [empty_account(); MAX_ACCOUNTS],
579 };
580
581 for i in 0..MAX_ACCOUNTS - 1 {
582 engine.next_free[i] = (i + 1) as u16;
583 }
584 engine.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
585
586 engine
587 }
588
589 pub fn init_in_place(&mut self, params: RiskParams, init_slot: u64, init_oracle_price: u64) {
592 Self::validate_params(¶ms);
593 assert!(
594 init_oracle_price > 0 && init_oracle_price <= MAX_ORACLE_PRICE,
595 "init_oracle_price must be in (0, MAX_ORACLE_PRICE] per spec §2.7"
596 );
597 self.vault = U128::ZERO;
598 self.insurance_fund = InsuranceFund {
599 balance: U128::ZERO,
600 };
601 self.params = params;
602 self.current_slot = init_slot;
603 self.funding_rate_bps_per_slot_last = 0;
604 self.last_crank_slot = 0;
605 self.max_crank_staleness_slots = params.max_crank_staleness_slots;
606 self.c_tot = U128::ZERO;
607 self.pnl_pos_tot = 0;
608 self.pnl_matured_pos_tot = 0;
609 self.liq_cursor = 0;
610 self.gc_cursor = 0;
611 self.last_full_sweep_start_slot = 0;
612 self.last_full_sweep_completed_slot = 0;
613 self.crank_cursor = 0;
614 self.sweep_start_idx = 0;
615 self.lifetime_liquidations = 0;
616 self.adl_mult_long = ADL_ONE;
617 self.adl_mult_short = ADL_ONE;
618 self.adl_coeff_long = 0;
619 self.adl_coeff_short = 0;
620 self.adl_epoch_long = 0;
621 self.adl_epoch_short = 0;
622 self.adl_epoch_start_k_long = 0;
623 self.adl_epoch_start_k_short = 0;
624 self.oi_eff_long_q = 0;
625 self.oi_eff_short_q = 0;
626 self.side_mode_long = SideMode::Normal;
627 self.side_mode_short = SideMode::Normal;
628 self.stored_pos_count_long = 0;
629 self.stored_pos_count_short = 0;
630 self.stale_account_count_long = 0;
631 self.stale_account_count_short = 0;
632 self.phantom_dust_bound_long_q = 0;
633 self.phantom_dust_bound_short_q = 0;
634 self.materialized_account_count = 0;
635 self.last_oracle_price = init_oracle_price;
636 self.last_market_slot = init_slot;
637 self.funding_price_sample_last = init_oracle_price;
638 self.insurance_floor = params.insurance_floor.get();
639 self.used = [0; BITMAP_WORDS];
640 self.num_used_accounts = 0;
641 self.next_account_id = 0;
642 self.free_head = 0;
643 self.accounts = [empty_account(); MAX_ACCOUNTS];
644 for i in 0..MAX_ACCOUNTS - 1 {
645 self.next_free[i] = (i + 1) as u16;
646 }
647 self.next_free[MAX_ACCOUNTS - 1] = u16::MAX;
648 }
649
650 pub fn is_used(&self, idx: usize) -> bool {
655 if idx >= MAX_ACCOUNTS {
656 return false;
657 }
658 let w = idx >> 6;
659 let b = idx & 63;
660 ((self.used[w] >> b) & 1) == 1
661 }
662
663 fn set_used(&mut self, idx: usize) {
664 let w = idx >> 6;
665 let b = idx & 63;
666 self.used[w] |= 1u64 << b;
667 }
668
669 fn clear_used(&mut self, idx: usize) {
670 let w = idx >> 6;
671 let b = idx & 63;
672 self.used[w] &= !(1u64 << b);
673 }
674
675 #[allow(dead_code)]
676 fn for_each_used_mut<F: FnMut(usize, &mut Account)>(&mut self, mut f: F) {
677 for (block, word) in self.used.iter().copied().enumerate() {
678 let mut w = word;
679 while w != 0 {
680 let bit = w.trailing_zeros() as usize;
681 let idx = block * 64 + bit;
682 w &= w - 1;
683 if idx >= MAX_ACCOUNTS {
684 continue;
685 }
686 f(idx, &mut self.accounts[idx]);
687 }
688 }
689 }
690
691 fn for_each_used<F: FnMut(usize, &Account)>(&self, mut f: F) {
692 for (block, word) in self.used.iter().copied().enumerate() {
693 let mut w = word;
694 while w != 0 {
695 let bit = w.trailing_zeros() as usize;
696 let idx = block * 64 + bit;
697 w &= w - 1;
698 if idx >= MAX_ACCOUNTS {
699 continue;
700 }
701 f(idx, &self.accounts[idx]);
702 }
703 }
704 }
705
706 fn alloc_slot(&mut self) -> Result<u16> {
711 if self.free_head == u16::MAX {
712 return Err(RiskError::Overflow);
713 }
714 let idx = self.free_head;
715 self.free_head = self.next_free[idx as usize];
716 self.set_used(idx as usize);
717 self.num_used_accounts = self.num_used_accounts.saturating_add(1);
718 Ok(idx)
719 }
720
721 test_visible! {
722 fn free_slot(&mut self, idx: u16) {
723 self.accounts[idx as usize] = empty_account();
724 self.clear_used(idx as usize);
725 self.next_free[idx as usize] = self.free_head;
726 self.free_head = idx;
727 self.num_used_accounts = self.num_used_accounts.saturating_sub(1);
728 self.materialized_account_count = self.materialized_account_count.saturating_sub(1);
730 }
731 }
732
733 fn materialize_at(&mut self, idx: u16, slot_anchor: u64) -> Result<()> {
737 if idx as usize >= MAX_ACCOUNTS {
738 return Err(RiskError::AccountNotFound);
739 }
740
741 let used_count = self.num_used_accounts as u64;
742 if used_count >= self.params.max_accounts {
743 return Err(RiskError::Overflow);
744 }
745
746 self.materialized_account_count = self
748 .materialized_account_count
749 .checked_add(1)
750 .ok_or(RiskError::Overflow)?;
751 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
752 self.materialized_account_count -= 1;
753 return Err(RiskError::Overflow);
754 }
755
756 let mut found = false;
759 if self.free_head == idx {
760 self.free_head = self.next_free[idx as usize];
761 found = true;
762 } else {
763 let mut prev = self.free_head;
764 let mut steps = 0usize;
765 while prev != u16::MAX && steps < MAX_ACCOUNTS {
766 if self.next_free[prev as usize] == idx {
767 self.next_free[prev as usize] = self.next_free[idx as usize];
768 found = true;
769 break;
770 }
771 prev = self.next_free[prev as usize];
772 steps += 1;
773 }
774 }
775 if !found {
776 self.materialized_account_count -= 1;
778 return Err(RiskError::CorruptState);
779 }
780
781 self.set_used(idx as usize);
782 self.num_used_accounts = self.num_used_accounts.saturating_add(1);
783
784 let account_id = self.next_account_id;
785 self.next_account_id = self.next_account_id.saturating_add(1);
786
787 self.accounts[idx as usize] = Account {
789 kind: Account::KIND_USER,
790 account_id,
791 capital: U128::ZERO,
792 pnl: 0i128,
793 reserved_pnl: 0u128,
794 warmup_started_at_slot: slot_anchor,
795 warmup_slope_per_step: 0u128,
796 position_basis_q: 0i128,
797 adl_a_basis: ADL_ONE,
798 adl_k_snap: 0i128,
799 adl_epoch_snap: 0,
800 matcher_program: [0; 32],
801 matcher_context: [0; 32],
802 owner: [0; 32],
803 fee_credits: I128::ZERO,
804 last_fee_slot: slot_anchor,
805 fees_earned_total: U128::ZERO,
806 };
807
808 Ok(())
809 }
810
811 test_visible! {
818 fn set_pnl(&mut self, idx: usize, new_pnl: i128) {
819 assert!(new_pnl != i128::MIN, "set_pnl: i128::MIN forbidden");
821
822 let old = self.accounts[idx].pnl;
823 let old_pos = i128_clamp_pos(old);
824 let old_r = self.accounts[idx].reserved_pnl;
825 let old_rel = old_pos - old_r;
826 let new_pos = i128_clamp_pos(new_pnl);
827
828 assert!(new_pos <= MAX_ACCOUNT_POSITIVE_PNL, "set_pnl: exceeds MAX_ACCOUNT_POSITIVE_PNL");
830
831 let new_r = if new_pos > old_pos {
833 let reserve_add = new_pos - old_pos;
835 let nr = old_r.checked_add(reserve_add)
836 .expect("set_pnl: new_R overflow");
837 assert!(nr <= new_pos, "set_pnl: new_R > new_pos");
838 nr
839 } else {
840 let pos_loss = old_pos - new_pos;
842 let nr = old_r.saturating_sub(pos_loss);
843 assert!(nr <= new_pos, "set_pnl: new_R > new_pos");
844 nr
845 };
846
847 let new_rel = new_pos - new_r;
848
849 if new_pos > old_pos {
851 let delta = new_pos - old_pos;
852 self.pnl_pos_tot = self.pnl_pos_tot.checked_add(delta)
853 .expect("set_pnl: pnl_pos_tot overflow");
854 } else if old_pos > new_pos {
855 let delta = old_pos - new_pos;
856 self.pnl_pos_tot = self.pnl_pos_tot.checked_sub(delta)
857 .expect("set_pnl: pnl_pos_tot underflow");
858 }
859 assert!(self.pnl_pos_tot <= MAX_PNL_POS_TOT, "set_pnl: exceeds MAX_PNL_POS_TOT");
860
861 if new_rel > old_rel {
863 let delta = new_rel - old_rel;
864 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_add(delta)
865 .expect("set_pnl: pnl_matured_pos_tot overflow");
866 } else if old_rel > new_rel {
867 let delta = old_rel - new_rel;
868 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(delta)
869 .expect("set_pnl: pnl_matured_pos_tot underflow");
870 }
871 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
872 "set_pnl: pnl_matured_pos_tot > pnl_pos_tot");
873
874 self.accounts[idx].pnl = new_pnl;
876 self.accounts[idx].reserved_pnl = new_r;
877 }
878 }
879
880 test_visible! {
882 fn set_reserved_pnl(&mut self, idx: usize, new_r: u128) {
883 let pos = i128_clamp_pos(self.accounts[idx].pnl);
884 assert!(new_r <= pos, "set_reserved_pnl: new_R > max(PNL_i, 0)");
885
886 let old_r = self.accounts[idx].reserved_pnl;
887 let old_rel = pos - old_r;
888 let new_rel = pos - new_r;
889
890 if new_rel > old_rel {
892 let delta = new_rel - old_rel;
893 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_add(delta)
894 .expect("set_reserved_pnl: pnl_matured_pos_tot overflow");
895 } else if old_rel > new_rel {
896 let delta = old_rel - new_rel;
897 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(delta)
898 .expect("set_reserved_pnl: pnl_matured_pos_tot underflow");
899 }
900 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
901 "set_reserved_pnl: pnl_matured_pos_tot > pnl_pos_tot");
902
903 self.accounts[idx].reserved_pnl = new_r;
904 }
905 }
906
907 test_visible! {
910 fn consume_released_pnl(&mut self, idx: usize, x: u128) {
911 assert!(x > 0, "consume_released_pnl: x must be > 0");
912
913 let old_pos = i128_clamp_pos(self.accounts[idx].pnl);
914 let old_r = self.accounts[idx].reserved_pnl;
915 let old_rel = old_pos - old_r;
916 assert!(x <= old_rel, "consume_released_pnl: x > ReleasedPos_i");
917
918 let new_pos = old_pos - x;
919 let new_rel = old_rel - x;
920 assert!(new_pos >= old_r, "consume_released_pnl: new_pos < old_R");
921
922 self.pnl_pos_tot = self.pnl_pos_tot.checked_sub(x)
924 .expect("consume_released_pnl: pnl_pos_tot underflow");
925
926 self.pnl_matured_pos_tot = self.pnl_matured_pos_tot.checked_sub(x)
928 .expect("consume_released_pnl: pnl_matured_pos_tot underflow");
929 assert!(self.pnl_matured_pos_tot <= self.pnl_pos_tot,
930 "consume_released_pnl: pnl_matured_pos_tot > pnl_pos_tot");
931
932 let x_i128: i128 = x.try_into().expect("consume_released_pnl: x > i128::MAX");
934 let new_pnl = self.accounts[idx].pnl.checked_sub(x_i128)
935 .expect("consume_released_pnl: PNL underflow");
936 assert!(new_pnl != i128::MIN, "consume_released_pnl: PNL == i128::MIN");
937 self.accounts[idx].pnl = new_pnl;
938 }
940 }
941
942 test_visible! {
944 fn set_capital(&mut self, idx: usize, new_capital: u128) {
945 let old = self.accounts[idx].capital.get();
946 if new_capital >= old {
947 let delta = new_capital - old;
948 self.c_tot = U128::new(self.c_tot.get().checked_add(delta)
949 .expect("set_capital: c_tot overflow"));
950 } else {
951 let delta = old - new_capital;
952 self.c_tot = U128::new(self.c_tot.get().checked_sub(delta)
953 .expect("set_capital: c_tot underflow"));
954 }
955 self.accounts[idx].capital = U128::new(new_capital);
956 }
957 }
958
959 test_visible! {
961 fn set_position_basis_q(&mut self, idx: usize, new_basis: i128) {
962 let old = self.accounts[idx].position_basis_q;
963 let old_side = side_of_i128(old);
964 let new_side = side_of_i128(new_basis);
965
966 if let Some(s) = old_side {
968 match s {
969 Side::Long => {
970 self.stored_pos_count_long = self.stored_pos_count_long
971 .checked_sub(1).expect("set_position_basis_q: long count underflow");
972 }
973 Side::Short => {
974 self.stored_pos_count_short = self.stored_pos_count_short
975 .checked_sub(1).expect("set_position_basis_q: short count underflow");
976 }
977 }
978 }
979
980 if let Some(s) = new_side {
982 match s {
983 Side::Long => {
984 self.stored_pos_count_long = self.stored_pos_count_long
985 .checked_add(1).expect("set_position_basis_q: long count overflow");
986 }
987 Side::Short => {
988 self.stored_pos_count_short = self.stored_pos_count_short
989 .checked_add(1).expect("set_position_basis_q: short count overflow");
990 }
991 }
992 }
993
994 self.accounts[idx].position_basis_q = new_basis;
995 }
996 }
997
998 test_visible! {
1000 fn attach_effective_position(&mut self, idx: usize, new_eff_pos_q: i128) {
1001 let old_basis = self.accounts[idx].position_basis_q;
1004 if old_basis != 0 {
1005 if let Some(old_side) = side_of_i128(old_basis) {
1006 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1007 let epoch_side = self.get_epoch_side(old_side);
1008 if epoch_snap == epoch_side {
1009 let a_basis = self.accounts[idx].adl_a_basis;
1010 if a_basis != 0 {
1011 let a_side = self.get_a_side(old_side);
1012 let abs_basis = old_basis.unsigned_abs();
1013 let product = U256::from_u128(abs_basis)
1015 .checked_mul(U256::from_u128(a_side));
1016 if let Some(p) = product {
1017 let rem = p.checked_rem(U256::from_u128(a_basis));
1018 if let Some(r) = rem {
1019 if !r.is_zero() {
1020 self.inc_phantom_dust_bound(old_side);
1021 }
1022 }
1023 }
1024 }
1025 }
1026 }
1027 }
1028
1029 if new_eff_pos_q == 0 {
1030 self.set_position_basis_q(idx, 0i128);
1031 self.accounts[idx].adl_a_basis = ADL_ONE;
1033 self.accounts[idx].adl_k_snap = 0i128;
1034 self.accounts[idx].adl_epoch_snap = 0;
1035 } else {
1036 assert!(
1038 new_eff_pos_q.unsigned_abs() <= MAX_POSITION_ABS_Q,
1039 "attach: abs(new_eff_pos_q) exceeds MAX_POSITION_ABS_Q"
1040 );
1041 let side = side_of_i128(new_eff_pos_q).expect("attach: nonzero must have side");
1042 self.set_position_basis_q(idx, new_eff_pos_q);
1043
1044 match side {
1045 Side::Long => {
1046 self.accounts[idx].adl_a_basis = self.adl_mult_long;
1047 self.accounts[idx].adl_k_snap = self.adl_coeff_long;
1048 self.accounts[idx].adl_epoch_snap = self.adl_epoch_long;
1049 }
1050 Side::Short => {
1051 self.accounts[idx].adl_a_basis = self.adl_mult_short;
1052 self.accounts[idx].adl_k_snap = self.adl_coeff_short;
1053 self.accounts[idx].adl_epoch_snap = self.adl_epoch_short;
1054 }
1055 }
1056 }
1057 }
1058 }
1059
1060 fn get_a_side(&self, s: Side) -> u128 {
1065 match s {
1066 Side::Long => self.adl_mult_long,
1067 Side::Short => self.adl_mult_short,
1068 }
1069 }
1070
1071 fn get_k_side(&self, s: Side) -> i128 {
1072 match s {
1073 Side::Long => self.adl_coeff_long,
1074 Side::Short => self.adl_coeff_short,
1075 }
1076 }
1077
1078 fn get_epoch_side(&self, s: Side) -> u64 {
1079 match s {
1080 Side::Long => self.adl_epoch_long,
1081 Side::Short => self.adl_epoch_short,
1082 }
1083 }
1084
1085 fn get_k_epoch_start(&self, s: Side) -> i128 {
1086 match s {
1087 Side::Long => self.adl_epoch_start_k_long,
1088 Side::Short => self.adl_epoch_start_k_short,
1089 }
1090 }
1091
1092 fn get_side_mode(&self, s: Side) -> SideMode {
1093 match s {
1094 Side::Long => self.side_mode_long,
1095 Side::Short => self.side_mode_short,
1096 }
1097 }
1098
1099 fn get_oi_eff(&self, s: Side) -> u128 {
1100 match s {
1101 Side::Long => self.oi_eff_long_q,
1102 Side::Short => self.oi_eff_short_q,
1103 }
1104 }
1105
1106 fn set_oi_eff(&mut self, s: Side, v: u128) {
1107 match s {
1108 Side::Long => self.oi_eff_long_q = v,
1109 Side::Short => self.oi_eff_short_q = v,
1110 }
1111 }
1112
1113 fn set_side_mode(&mut self, s: Side, m: SideMode) {
1114 match s {
1115 Side::Long => self.side_mode_long = m,
1116 Side::Short => self.side_mode_short = m,
1117 }
1118 }
1119
1120 fn set_a_side(&mut self, s: Side, v: u128) {
1121 match s {
1122 Side::Long => self.adl_mult_long = v,
1123 Side::Short => self.adl_mult_short = v,
1124 }
1125 }
1126
1127 fn set_k_side(&mut self, s: Side, v: i128) {
1128 match s {
1129 Side::Long => self.adl_coeff_long = v,
1130 Side::Short => self.adl_coeff_short = v,
1131 }
1132 }
1133
1134 fn get_stale_count(&self, s: Side) -> u64 {
1135 match s {
1136 Side::Long => self.stale_account_count_long,
1137 Side::Short => self.stale_account_count_short,
1138 }
1139 }
1140
1141 fn set_stale_count(&mut self, s: Side, v: u64) {
1142 match s {
1143 Side::Long => self.stale_account_count_long = v,
1144 Side::Short => self.stale_account_count_short = v,
1145 }
1146 }
1147
1148 fn get_stored_pos_count(&self, s: Side) -> u64 {
1149 match s {
1150 Side::Long => self.stored_pos_count_long,
1151 Side::Short => self.stored_pos_count_short,
1152 }
1153 }
1154
1155 fn inc_phantom_dust_bound(&mut self, s: Side) {
1157 match s {
1158 Side::Long => {
1159 self.phantom_dust_bound_long_q = self
1160 .phantom_dust_bound_long_q
1161 .checked_add(1u128)
1162 .expect("phantom_dust_bound_long_q overflow");
1163 }
1164 Side::Short => {
1165 self.phantom_dust_bound_short_q = self
1166 .phantom_dust_bound_short_q
1167 .checked_add(1u128)
1168 .expect("phantom_dust_bound_short_q overflow");
1169 }
1170 }
1171 }
1172
1173 fn inc_phantom_dust_bound_by(&mut self, s: Side, amount_q: u128) {
1175 match s {
1176 Side::Long => {
1177 self.phantom_dust_bound_long_q = self
1178 .phantom_dust_bound_long_q
1179 .checked_add(amount_q)
1180 .expect("phantom_dust_bound_long_q overflow");
1181 }
1182 Side::Short => {
1183 self.phantom_dust_bound_short_q = self
1184 .phantom_dust_bound_short_q
1185 .checked_add(amount_q)
1186 .expect("phantom_dust_bound_short_q overflow");
1187 }
1188 }
1189 }
1190
1191 pub fn effective_pos_q(&self, idx: usize) -> i128 {
1197 let basis = self.accounts[idx].position_basis_q;
1198 if basis == 0 {
1199 return 0i128;
1200 }
1201
1202 let side = side_of_i128(basis).unwrap();
1203 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1204 let epoch_side = self.get_epoch_side(side);
1205
1206 if epoch_snap != epoch_side {
1207 return 0i128;
1209 }
1210
1211 let a_side = self.get_a_side(side);
1212 let a_basis = self.accounts[idx].adl_a_basis;
1213
1214 if a_basis == 0 {
1215 return 0i128;
1216 }
1217
1218 let abs_basis = basis.unsigned_abs();
1219 let effective_abs = mul_div_floor_u128(abs_basis, a_side, a_basis);
1221
1222 if basis < 0 {
1223 if effective_abs == 0 {
1224 0i128
1225 } else {
1226 assert!(
1227 effective_abs <= i128::MAX as u128,
1228 "effective_pos_q: overflow"
1229 );
1230 -(effective_abs as i128)
1231 }
1232 } else {
1233 assert!(
1234 effective_abs <= i128::MAX as u128,
1235 "effective_pos_q: overflow"
1236 );
1237 effective_abs as i128
1238 }
1239 }
1240
1241 test_visible! {
1246 fn settle_side_effects(&mut self, idx: usize) -> Result<()> {
1247 let basis = self.accounts[idx].position_basis_q;
1248 if basis == 0 {
1249 return Ok(());
1250 }
1251
1252 let side = side_of_i128(basis).unwrap();
1253 let epoch_snap = self.accounts[idx].adl_epoch_snap;
1254 let epoch_side = self.get_epoch_side(side);
1255 let a_basis = self.accounts[idx].adl_a_basis;
1256
1257 if a_basis == 0 {
1258 return Err(RiskError::CorruptState);
1259 }
1260
1261 let abs_basis = basis.unsigned_abs();
1262
1263 if epoch_snap == epoch_side {
1264 let a_side = self.get_a_side(side);
1266 let k_side = self.get_k_side(side);
1267 let k_snap = self.accounts[idx].adl_k_snap;
1268
1269 let q_eff_new = mul_div_floor_u128(abs_basis, a_side, a_basis);
1271
1272 let old_r = self.accounts[idx].reserved_pnl;
1274
1275 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1277 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_side, den);
1278
1279 let old_pnl = self.accounts[idx].pnl;
1280 let new_pnl = old_pnl.checked_add(pnl_delta).ok_or(RiskError::Overflow)?;
1281 if new_pnl == i128::MIN {
1282 return Err(RiskError::Overflow);
1283 }
1284 self.set_pnl(idx, new_pnl);
1285
1286 if self.accounts[idx].reserved_pnl > old_r {
1288 self.restart_warmup_after_reserve_increase(idx);
1289 }
1290
1291 if q_eff_new == 0 {
1292 self.inc_phantom_dust_bound(side);
1295 self.set_position_basis_q(idx, 0i128);
1296 self.accounts[idx].adl_a_basis = ADL_ONE;
1297 self.accounts[idx].adl_k_snap = 0i128;
1298 self.accounts[idx].adl_epoch_snap = 0;
1299 } else {
1300 self.accounts[idx].adl_k_snap = k_side;
1302 self.accounts[idx].adl_epoch_snap = epoch_side;
1303 }
1304 } else {
1305 let side_mode = self.get_side_mode(side);
1307 if side_mode != SideMode::ResetPending {
1308 return Err(RiskError::CorruptState);
1309 }
1310 if epoch_snap.checked_add(1) != Some(epoch_side) {
1311 return Err(RiskError::CorruptState);
1312 }
1313
1314 let k_epoch_start = self.get_k_epoch_start(side);
1315 let k_snap = self.accounts[idx].adl_k_snap;
1316
1317 let old_r = self.accounts[idx].reserved_pnl;
1319
1320 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1322 let pnl_delta = wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_epoch_start, den);
1323
1324 let old_pnl = self.accounts[idx].pnl;
1325 let new_pnl = old_pnl.checked_add(pnl_delta).ok_or(RiskError::Overflow)?;
1326 if new_pnl == i128::MIN {
1327 return Err(RiskError::Overflow);
1328 }
1329 self.set_pnl(idx, new_pnl);
1330
1331 if self.accounts[idx].reserved_pnl > old_r {
1333 self.restart_warmup_after_reserve_increase(idx);
1334 }
1335
1336 self.set_position_basis_q(idx, 0i128);
1337
1338 let old_stale = self.get_stale_count(side);
1340 let new_stale = old_stale.checked_sub(1).ok_or(RiskError::CorruptState)?;
1341 self.set_stale_count(side, new_stale);
1342
1343 self.accounts[idx].adl_a_basis = ADL_ONE;
1345 self.accounts[idx].adl_k_snap = 0i128;
1346 self.accounts[idx].adl_epoch_snap = 0;
1347 }
1348
1349 Ok(())
1350 }
1351 }
1352
1353 pub fn accrue_market_to(&mut self, now_slot: u64, oracle_price: u64) -> Result<()> {
1358 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
1359 return Err(RiskError::Overflow);
1360 }
1361
1362 if now_slot < self.current_slot {
1364 return Err(RiskError::Overflow);
1365 }
1366 if now_slot < self.last_market_slot {
1367 return Err(RiskError::Overflow);
1368 }
1369
1370 let long_live = self.oi_eff_long_q != 0;
1372 let short_live = self.oi_eff_short_q != 0;
1373
1374 let total_dt = now_slot.saturating_sub(self.last_market_slot);
1375 if total_dt == 0 && self.last_oracle_price == oracle_price {
1376 self.current_slot = now_slot;
1378 return Ok(());
1379 }
1380
1381 let current_price = self.last_oracle_price;
1383 let delta_p = (oracle_price as i128)
1384 .checked_sub(current_price as i128)
1385 .ok_or(RiskError::Overflow)?;
1386 if delta_p != 0 {
1387 if long_live {
1388 let delta_k = checked_u128_mul_i128(self.adl_mult_long, delta_p)?;
1389 self.adl_coeff_long = self
1390 .adl_coeff_long
1391 .checked_add(delta_k)
1392 .ok_or(RiskError::Overflow)?;
1393 }
1394 if short_live {
1395 let delta_k = checked_u128_mul_i128(self.adl_mult_short, delta_p)?;
1396 self.adl_coeff_short = self
1397 .adl_coeff_short
1398 .checked_sub(delta_k)
1399 .ok_or(RiskError::Overflow)?;
1400 }
1401 }
1402
1403 let r_last = self.funding_rate_bps_per_slot_last;
1405 if r_last != 0 && total_dt > 0 && long_live && short_live {
1406 let fund_px_0 = self.funding_price_sample_last;
1409
1410 if fund_px_0 > 0 {
1411 let mut dt_remaining = total_dt;
1412
1413 while dt_remaining > 0 {
1414 let dt_sub = core::cmp::min(dt_remaining, MAX_FUNDING_DT);
1415 dt_remaining -= dt_sub;
1416
1417 let fund_num: i128 = (fund_px_0 as i128)
1419 .checked_mul(r_last as i128)
1420 .ok_or(RiskError::Overflow)?
1421 .checked_mul(dt_sub as i128)
1422 .ok_or(RiskError::Overflow)?;
1423
1424 let fund_term = floor_div_signed_conservative_i128(fund_num, 10_000u128);
1426
1427 if fund_term != 0 {
1428 let delta_k_long = checked_u128_mul_i128(self.adl_mult_long, fund_term)?;
1430 self.adl_coeff_long = self
1431 .adl_coeff_long
1432 .checked_sub(delta_k_long)
1433 .ok_or(RiskError::Overflow)?;
1434
1435 let delta_k_short = checked_u128_mul_i128(self.adl_mult_short, fund_term)?;
1437 self.adl_coeff_short = self
1438 .adl_coeff_short
1439 .checked_add(delta_k_short)
1440 .ok_or(RiskError::Overflow)?;
1441 }
1442 }
1443 }
1444 }
1445
1446 self.current_slot = now_slot;
1448 self.last_market_slot = now_slot;
1449 self.last_oracle_price = oracle_price;
1450 self.funding_price_sample_last = oracle_price;
1451
1452 Ok(())
1453 }
1454
1455 test_visible! {
1459 fn recompute_r_last_from_final_state(&mut self, externally_computed_rate: i64) -> Result<()> {
1460 if externally_computed_rate.unsigned_abs() > MAX_ABS_FUNDING_BPS_PER_SLOT as u64 {
1461 return Err(RiskError::Overflow);
1462 }
1463 self.funding_rate_bps_per_slot_last = externally_computed_rate;
1464 Ok(())
1465 }
1466 }
1467
1468 pub fn run_end_of_instruction_lifecycle(
1476 &mut self,
1477 ctx: &mut InstructionContext,
1478 funding_rate: i64,
1479 ) -> Result<()> {
1480 self.schedule_end_of_instruction_resets(ctx)?;
1481 self.finalize_end_of_instruction_resets(ctx);
1482 self.recompute_r_last_from_final_state(funding_rate)?;
1483 Ok(())
1484 }
1485
1486 fn use_insurance_buffer(&mut self, loss: u128) -> u128 {
1493 if loss == 0 {
1494 return 0;
1495 }
1496 let ins_bal = self.insurance_fund.balance.get();
1497 let available = ins_bal.saturating_sub(self.insurance_floor);
1498 let pay = core::cmp::min(loss, available);
1499 if pay > 0 {
1500 self.insurance_fund.balance = U128::new(ins_bal - pay);
1501 }
1502 loss - pay
1503 }
1504
1505 test_visible! {
1508 fn absorb_protocol_loss(&mut self, loss: u128) {
1509 if loss == 0 {
1510 return;
1511 }
1512 let _rem = self.use_insurance_buffer(loss);
1513 }
1515 }
1516
1517 test_visible! {
1522 fn enqueue_adl(&mut self, ctx: &mut InstructionContext, liq_side: Side, q_close_q: u128, d: u128) -> Result<()> {
1523 let opp = opposite_side(liq_side);
1524
1525 if q_close_q != 0 {
1527 let old_oi = self.get_oi_eff(liq_side);
1528 let new_oi = old_oi.checked_sub(q_close_q).ok_or(RiskError::CorruptState)?;
1529 self.set_oi_eff(liq_side, new_oi);
1530 }
1531
1532 let d_rem = if d > 0 { self.use_insurance_buffer(d) } else { 0u128 };
1534
1535 let oi = self.get_oi_eff(opp);
1537
1538 if oi == 0 {
1540 if self.get_oi_eff(liq_side) == 0 {
1542 set_pending_reset(ctx, liq_side);
1543 set_pending_reset(ctx, opp);
1544 }
1545 return Ok(());
1546 }
1547
1548 if self.get_stored_pos_count(opp) == 0 {
1551 if q_close_q > oi {
1552 return Err(RiskError::CorruptState);
1553 }
1554 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1555 self.set_oi_eff(opp, oi_post);
1557 if oi_post == 0 {
1558 set_pending_reset(ctx, opp);
1560 if self.get_oi_eff(liq_side) == 0 {
1562 set_pending_reset(ctx, liq_side);
1563 }
1564 }
1565 return Ok(());
1566 }
1567
1568 if q_close_q > oi {
1570 return Err(RiskError::CorruptState);
1571 }
1572
1573 let a_old = self.get_a_side(opp);
1574 let oi_post = oi.checked_sub(q_close_q).ok_or(RiskError::Overflow)?;
1575
1576 if d_rem != 0 {
1581 let a_ps = a_old.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
1582 match wide_mul_div_ceil_u128_or_over_i128max(d_rem, a_ps, oi) {
1583 Ok(delta_k_abs) => {
1584 let delta_k = -(delta_k_abs as i128);
1585 let k_opp = self.get_k_side(opp);
1586 match k_opp.checked_add(delta_k) {
1587 Some(new_k) => {
1588 self.set_k_side(opp, new_k);
1589 }
1590 None => {
1591 }
1593 }
1594 }
1595 Err(OverI128Magnitude) => {
1596 }
1598 }
1599 }
1600
1601 if oi_post == 0 {
1603 self.set_oi_eff(opp, 0u128);
1604 set_pending_reset(ctx, opp);
1605 if self.get_oi_eff(liq_side) == 0 {
1606 set_pending_reset(ctx, liq_side);
1607 }
1608 return Ok(());
1609 }
1610
1611 let a_old_u256 = U256::from_u128(a_old);
1613 let oi_post_u256 = U256::from_u128(oi_post);
1614 let oi_u256 = U256::from_u128(oi);
1615 let (a_candidate_u256, a_trunc_rem) = mul_div_floor_u256_with_rem(
1616 a_old_u256,
1617 oi_post_u256,
1618 oi_u256,
1619 );
1620
1621 if !a_candidate_u256.is_zero() {
1623 let a_new = a_candidate_u256.try_into_u128().expect("A_candidate exceeds u128");
1624 self.set_a_side(opp, a_new);
1625 self.set_oi_eff(opp, oi_post);
1626 if !a_trunc_rem.is_zero() {
1628 let n_opp = self.get_stored_pos_count(opp) as u128;
1629 let n_opp_u256 = U256::from_u128(n_opp);
1630 let oi_plus_n = oi_u256.checked_add(n_opp_u256).unwrap_or(U256::MAX);
1632 let ceil_term = ceil_div_positive_checked(oi_plus_n, a_old_u256);
1633 let global_a_dust_bound = n_opp_u256.checked_add(ceil_term)
1634 .unwrap_or(U256::MAX);
1635 let bound_u128 = global_a_dust_bound.try_into_u128().unwrap_or(u128::MAX);
1636 self.inc_phantom_dust_bound_by(opp, bound_u128);
1637 }
1638 if a_new < MIN_A_SIDE {
1639 self.set_side_mode(opp, SideMode::DrainOnly);
1640 }
1641 return Ok(());
1642 }
1643
1644 self.set_oi_eff(opp, 0u128);
1646 self.set_oi_eff(liq_side, 0u128);
1647 set_pending_reset(ctx, opp);
1648 set_pending_reset(ctx, liq_side);
1649
1650 Ok(())
1651 }
1652 }
1653
1654 test_visible! {
1659 fn begin_full_drain_reset(&mut self, side: Side) {
1660 assert!(self.get_oi_eff(side) == 0, "begin_full_drain_reset: OI not zero");
1662
1663 let k = self.get_k_side(side);
1665 match side {
1666 Side::Long => self.adl_epoch_start_k_long = k,
1667 Side::Short => self.adl_epoch_start_k_short = k,
1668 }
1669
1670 match side {
1672 Side::Long => self.adl_epoch_long = self.adl_epoch_long.checked_add(1)
1673 .expect("epoch overflow"),
1674 Side::Short => self.adl_epoch_short = self.adl_epoch_short.checked_add(1)
1675 .expect("epoch overflow"),
1676 }
1677
1678 self.set_a_side(side, ADL_ONE);
1680
1681 let spc = self.get_stored_pos_count(side);
1683 self.set_stale_count(side, spc);
1684
1685 match side {
1687 Side::Long => self.phantom_dust_bound_long_q = 0u128,
1688 Side::Short => self.phantom_dust_bound_short_q = 0u128,
1689 }
1690
1691 self.set_side_mode(side, SideMode::ResetPending);
1693 }
1694 }
1695
1696 test_visible! {
1697 fn finalize_side_reset(&mut self, side: Side) -> Result<()> {
1698 if self.get_side_mode(side) != SideMode::ResetPending {
1699 return Err(RiskError::CorruptState);
1700 }
1701 if self.get_oi_eff(side) != 0 {
1702 return Err(RiskError::CorruptState);
1703 }
1704 if self.get_stale_count(side) != 0 {
1705 return Err(RiskError::CorruptState);
1706 }
1707 if self.get_stored_pos_count(side) != 0 {
1708 return Err(RiskError::CorruptState);
1709 }
1710 self.set_side_mode(side, SideMode::Normal);
1711 Ok(())
1712 }
1713 }
1714
1715 test_visible! {
1720 fn schedule_end_of_instruction_resets(&mut self, ctx: &mut InstructionContext) -> Result<()> {
1721 if self.stored_pos_count_long == 0 && self.stored_pos_count_short == 0 {
1723 let clear_bound_q = self.phantom_dust_bound_long_q
1724 .checked_add(self.phantom_dust_bound_short_q)
1725 .ok_or(RiskError::CorruptState)?;
1726 let has_residual = self.oi_eff_long_q != 0
1727 || self.oi_eff_short_q != 0
1728 || self.phantom_dust_bound_long_q != 0
1729 || self.phantom_dust_bound_short_q != 0;
1730 if has_residual {
1731 if self.oi_eff_long_q != self.oi_eff_short_q {
1732 return Err(RiskError::CorruptState);
1733 }
1734 if self.oi_eff_long_q <= clear_bound_q && self.oi_eff_short_q <= clear_bound_q {
1735 self.oi_eff_long_q = 0u128;
1736 self.oi_eff_short_q = 0u128;
1737 ctx.pending_reset_long = true;
1738 ctx.pending_reset_short = true;
1739 } else {
1740 return Err(RiskError::CorruptState);
1741 }
1742 }
1743 }
1744 else if self.stored_pos_count_long == 0 && self.stored_pos_count_short > 0 {
1746 let has_residual = self.oi_eff_long_q != 0
1747 || self.oi_eff_short_q != 0
1748 || self.phantom_dust_bound_long_q != 0;
1749 if has_residual {
1750 if self.oi_eff_long_q != self.oi_eff_short_q {
1751 return Err(RiskError::CorruptState);
1752 }
1753 if self.oi_eff_long_q <= self.phantom_dust_bound_long_q {
1754 self.oi_eff_long_q = 0u128;
1755 self.oi_eff_short_q = 0u128;
1756 ctx.pending_reset_long = true;
1757 ctx.pending_reset_short = true;
1758 } else {
1759 return Err(RiskError::CorruptState);
1760 }
1761 }
1762 }
1763 else if self.stored_pos_count_short == 0 && self.stored_pos_count_long > 0 {
1765 let has_residual = self.oi_eff_long_q != 0
1766 || self.oi_eff_short_q != 0
1767 || self.phantom_dust_bound_short_q != 0;
1768 if has_residual {
1769 if self.oi_eff_long_q != self.oi_eff_short_q {
1770 return Err(RiskError::CorruptState);
1771 }
1772 if self.oi_eff_short_q <= self.phantom_dust_bound_short_q {
1773 self.oi_eff_long_q = 0u128;
1774 self.oi_eff_short_q = 0u128;
1775 ctx.pending_reset_long = true;
1776 ctx.pending_reset_short = true;
1777 } else {
1778 return Err(RiskError::CorruptState);
1779 }
1780 }
1781 }
1782
1783 if self.side_mode_long == SideMode::DrainOnly && self.oi_eff_long_q == 0 {
1785 ctx.pending_reset_long = true;
1786 }
1787 if self.side_mode_short == SideMode::DrainOnly && self.oi_eff_short_q == 0 {
1788 ctx.pending_reset_short = true;
1789 }
1790
1791 Ok(())
1792 }
1793 }
1794
1795 test_visible! {
1796 fn finalize_end_of_instruction_resets(&mut self, ctx: &InstructionContext) {
1797 if ctx.pending_reset_long && self.side_mode_long != SideMode::ResetPending {
1798 self.begin_full_drain_reset(Side::Long);
1799 }
1800 if ctx.pending_reset_short && self.side_mode_short != SideMode::ResetPending {
1801 self.begin_full_drain_reset(Side::Short);
1802 }
1803 self.maybe_finalize_ready_reset_sides();
1805 }
1806 }
1807
1808 fn maybe_finalize_ready_reset_sides(&mut self) {
1812 if self.side_mode_long == SideMode::ResetPending
1813 && self.get_oi_eff(Side::Long) == 0
1814 && self.get_stale_count(Side::Long) == 0
1815 && self.get_stored_pos_count(Side::Long) == 0
1816 {
1817 self.set_side_mode(Side::Long, SideMode::Normal);
1818 }
1819 if self.side_mode_short == SideMode::ResetPending
1820 && self.get_oi_eff(Side::Short) == 0
1821 && self.get_stale_count(Side::Short) == 0
1822 && self.get_stored_pos_count(Side::Short) == 0
1823 {
1824 self.set_side_mode(Side::Short, SideMode::Normal);
1825 }
1826 }
1827
1828 pub fn haircut_ratio(&self) -> (u128, u128) {
1835 if self.pnl_matured_pos_tot == 0 {
1836 return (1u128, 1u128);
1837 }
1838 let senior_sum = self
1839 .c_tot
1840 .get()
1841 .checked_add(self.insurance_fund.balance.get());
1842 let residual: u128 = match senior_sum {
1843 Some(ss) => {
1844 if self.vault.get() >= ss {
1845 self.vault.get() - ss
1846 } else {
1847 0u128
1848 }
1849 }
1850 None => 0u128, };
1852 let h_num = if residual < self.pnl_matured_pos_tot {
1853 residual
1854 } else {
1855 self.pnl_matured_pos_tot
1856 };
1857 (h_num, self.pnl_matured_pos_tot)
1858 }
1859
1860 pub fn effective_matured_pnl(&self, idx: usize) -> u128 {
1862 let released = self.released_pos(idx);
1863 if released == 0 {
1864 return 0u128;
1865 }
1866 let (h_num, h_den) = self.haircut_ratio();
1867 if h_den == 0 {
1868 return released;
1869 }
1870 wide_mul_div_floor_u128(released, h_num, h_den)
1871 }
1872
1873 pub fn account_equity_maint_raw(&self, account: &Account) -> i128 {
1879 let wide = self.account_equity_maint_raw_wide(account);
1880 match wide.try_into_i128() {
1881 Some(v) => v,
1882 None => {
1883 if wide.is_negative() {
1888 i128::MIN + 1
1889 } else {
1890 i128::MAX
1891 }
1892 }
1893 }
1894 }
1895
1896 pub fn account_equity_maint_raw_wide(&self, account: &Account) -> I256 {
1900 let cap = I256::from_u128(account.capital.get());
1901 let pnl = I256::from_i128(account.pnl);
1902 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1903
1904 let sum = cap.checked_add(pnl).expect("I256 add overflow");
1906 sum.checked_sub(fee_debt).expect("I256 sub overflow")
1907 }
1908
1909 pub fn account_equity_net(&self, account: &Account, _oracle_price: u64) -> i128 {
1911 let raw = self.account_equity_maint_raw(account);
1912 if raw < 0 {
1913 0i128
1914 } else {
1915 raw
1916 }
1917 }
1918
1919 pub fn account_equity_init_raw(&self, account: &Account, idx: usize) -> i128 {
1923 let cap = I256::from_u128(account.capital.get());
1924 let neg_pnl = I256::from_i128(if account.pnl < 0 { account.pnl } else { 0i128 });
1925 let eff_matured = I256::from_u128(self.effective_matured_pnl(idx));
1926 let fee_debt = I256::from_u128(fee_debt_u128_checked(account.fee_credits.get()));
1927
1928 let sum = cap
1929 .checked_add(neg_pnl)
1930 .expect("I256 add overflow")
1931 .checked_add(eff_matured)
1932 .expect("I256 add overflow")
1933 .checked_sub(fee_debt)
1934 .expect("I256 sub overflow");
1935
1936 match sum.try_into_i128() {
1937 Some(v) => v,
1938 None => {
1939 if sum.is_negative() {
1943 i128::MIN + 1
1944 } else {
1945 i128::MAX
1946 }
1947 }
1948 }
1949 }
1950
1951 pub fn account_equity_init_net(&self, account: &Account, idx: usize) -> i128 {
1953 let raw = self.account_equity_init_raw(account, idx);
1954 if raw < 0 {
1955 0i128
1956 } else {
1957 raw
1958 }
1959 }
1960
1961 pub fn notional(&self, idx: usize, oracle_price: u64) -> u128 {
1963 let eff = self.effective_pos_q(idx);
1964 if eff == 0 {
1965 return 0;
1966 }
1967 let abs_eff = eff.unsigned_abs();
1968 mul_div_floor_u128(abs_eff, oracle_price as u128, POS_SCALE)
1969 }
1970
1971 pub fn is_above_maintenance_margin(
1974 &self,
1975 account: &Account,
1976 idx: usize,
1977 oracle_price: u64,
1978 ) -> bool {
1979 let eq_net = self.account_equity_net(account, oracle_price);
1980 let eff = self.effective_pos_q(idx);
1981 if eff == 0 {
1982 return eq_net > 0;
1983 }
1984 let not = self.notional(idx, oracle_price);
1985 let proportional =
1986 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000);
1987 let mm_req = core::cmp::max(proportional, self.params.min_nonzero_mm_req);
1988 let mm_req_i128 = if mm_req > i128::MAX as u128 {
1989 i128::MAX
1990 } else {
1991 mm_req as i128
1992 };
1993 eq_net > mm_req_i128
1994 }
1995
1996 pub fn is_above_initial_margin(
2001 &self,
2002 account: &Account,
2003 idx: usize,
2004 oracle_price: u64,
2005 ) -> bool {
2006 let eq_init_raw = self.account_equity_init_raw(account, idx);
2007 let eff = self.effective_pos_q(idx);
2008 if eff == 0 {
2009 return eq_init_raw >= 0;
2010 }
2011 let not = self.notional(idx, oracle_price);
2012 let proportional = mul_div_floor_u128(not, self.params.initial_margin_bps as u128, 10_000);
2013 let im_req = core::cmp::max(proportional, self.params.min_nonzero_im_req);
2014 let im_req_i128 = if im_req > i128::MAX as u128 {
2015 i128::MAX
2016 } else {
2017 im_req as i128
2018 };
2019 eq_init_raw >= im_req_i128
2020 }
2021
2022 pub fn check_conservation(&self) -> bool {
2027 let senior = self
2028 .c_tot
2029 .get()
2030 .checked_add(self.insurance_fund.balance.get());
2031 match senior {
2032 Some(s) => self.vault.get() >= s,
2033 None => false,
2034 }
2035 }
2036
2037 pub fn released_pos(&self, idx: usize) -> u128 {
2043 let pnl = self.accounts[idx].pnl;
2044 let pos_pnl = i128_clamp_pos(pnl);
2045 pos_pnl.saturating_sub(self.accounts[idx].reserved_pnl)
2046 }
2047
2048 test_visible! {
2051 fn restart_warmup_after_reserve_increase(&mut self, idx: usize) {
2052 let t = self.params.warmup_period_slots;
2053 if t == 0 {
2054 self.set_reserved_pnl(idx, 0);
2056 self.accounts[idx].warmup_slope_per_step = 0;
2057 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2058 return;
2059 }
2060 let r = self.accounts[idx].reserved_pnl;
2061 if r == 0 {
2062 self.accounts[idx].warmup_slope_per_step = 0;
2063 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2064 return;
2065 }
2066 let base = r / (t as u128);
2068 let slope = if base == 0 { 1u128 } else { base };
2069 self.accounts[idx].warmup_slope_per_step = slope;
2070 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2071 }
2072 }
2073
2074 test_visible! {
2076 fn advance_profit_warmup(&mut self, idx: usize) {
2077 let r = self.accounts[idx].reserved_pnl;
2078 if r == 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 t = self.params.warmup_period_slots;
2084 if t == 0 {
2085 self.set_reserved_pnl(idx, 0);
2086 self.accounts[idx].warmup_slope_per_step = 0;
2087 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2088 return;
2089 }
2090 let elapsed = self.current_slot.saturating_sub(self.accounts[idx].warmup_started_at_slot);
2091 let cap = saturating_mul_u128_u64(self.accounts[idx].warmup_slope_per_step, elapsed);
2092 let release = core::cmp::min(r, cap);
2093 if release > 0 {
2094 self.set_reserved_pnl(idx, r - release);
2095 }
2096 if self.accounts[idx].reserved_pnl == 0 {
2097 self.accounts[idx].warmup_slope_per_step = 0;
2098 }
2099 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2100 }
2101 }
2102
2103 fn settle_losses(&mut self, idx: usize) {
2109 let pnl = self.accounts[idx].pnl;
2110 if pnl >= 0 {
2111 return;
2112 }
2113 assert!(pnl != i128::MIN, "settle_losses: i128::MIN");
2114 let need = pnl.unsigned_abs();
2115 let cap = self.accounts[idx].capital.get();
2116 let pay = core::cmp::min(need, cap);
2117 if pay > 0 {
2118 self.set_capital(idx, cap - pay);
2119 let pay_i128 = pay as i128; let new_pnl = pnl.checked_add(pay_i128).unwrap_or(0i128);
2121 if new_pnl == i128::MIN {
2122 self.set_pnl(idx, 0i128);
2123 } else {
2124 self.set_pnl(idx, new_pnl);
2125 }
2126 }
2127 }
2128
2129 fn resolve_flat_negative(&mut self, idx: usize) {
2131 let eff = self.effective_pos_q(idx);
2132 if eff != 0 {
2133 return; }
2135 let pnl = self.accounts[idx].pnl;
2136 if pnl < 0 {
2137 assert!(pnl != i128::MIN, "resolve_flat_negative: i128::MIN");
2138 let loss = pnl.unsigned_abs();
2139 self.absorb_protocol_loss(loss);
2140 self.set_pnl(idx, 0i128);
2141 }
2142 }
2143
2144 fn do_profit_conversion(&mut self, idx: usize) {
2147 let x = self.released_pos(idx);
2148 if x == 0 {
2149 return;
2150 }
2151
2152 let (h_num, h_den) = self.haircut_ratio();
2156 assert!(
2157 h_den > 0,
2158 "do_profit_conversion: h_den must be > 0 when x > 0"
2159 );
2160 let y: u128 = wide_mul_div_floor_u128(x, h_num, h_den);
2161
2162 self.consume_released_pnl(idx, x);
2164
2165 let new_cap = add_u128(self.accounts[idx].capital.get(), y);
2167 self.set_capital(idx, new_cap);
2168
2169 if self.accounts[idx].reserved_pnl == 0 {
2171 self.accounts[idx].warmup_slope_per_step = 0;
2172 self.accounts[idx].warmup_started_at_slot = self.current_slot;
2173 }
2174 }
2176
2177 test_visible! {
2179 fn fee_debt_sweep(&mut self, idx: usize) {
2180 let fc = self.accounts[idx].fee_credits.get();
2181 let debt = fee_debt_u128_checked(fc);
2182 if debt == 0 {
2183 return;
2184 }
2185 let cap = self.accounts[idx].capital.get();
2186 let pay = core::cmp::min(debt, cap);
2187 if pay > 0 {
2188 self.set_capital(idx, cap - pay);
2189 let pay_i128 = core::cmp::min(pay, i128::MAX as u128) as i128;
2191 self.accounts[idx].fee_credits = I128::new(self.accounts[idx].fee_credits.get()
2192 .checked_add(pay_i128).expect("fee_debt_sweep: pay <= debt guarantees no overflow"));
2193 self.insurance_fund.balance = self.insurance_fund.balance + pay;
2194 }
2195 }
2199 }
2200
2201 pub fn touch_account_full(
2206 &mut self,
2207 idx: usize,
2208 oracle_price: u64,
2209 now_slot: u64,
2210 ) -> Result<()> {
2211 if idx >= MAX_ACCOUNTS || !self.is_used(idx) {
2213 return Err(RiskError::AccountNotFound);
2214 }
2215 if now_slot < self.current_slot {
2217 return Err(RiskError::Overflow);
2218 }
2219 if now_slot < self.last_market_slot {
2220 return Err(RiskError::Overflow);
2221 }
2222 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2223 return Err(RiskError::Overflow);
2224 }
2225
2226 self.current_slot = now_slot;
2228
2229 self.accrue_market_to(now_slot, oracle_price)?;
2231
2232 self.advance_profit_warmup(idx);
2234
2235 self.settle_side_effects(idx)?;
2237
2238 self.settle_losses(idx);
2240
2241 if self.effective_pos_q(idx) == 0 && self.accounts[idx].pnl < 0 {
2243 self.resolve_flat_negative(idx);
2244 }
2245
2246 self.settle_maintenance_fee_internal(idx, now_slot)?;
2248
2249 if self.accounts[idx].position_basis_q == 0 {
2251 self.do_profit_conversion(idx);
2252 }
2253
2254 self.fee_debt_sweep(idx);
2256
2257 Ok(())
2258 }
2259
2260 fn settle_maintenance_fee_internal(&mut self, idx: usize, now_slot: u64) -> Result<()> {
2262 let fee_per_slot = self.params.maintenance_fee_per_slot.get();
2263 if fee_per_slot == 0 {
2264 self.accounts[idx].last_fee_slot = now_slot;
2265 return Ok(());
2266 }
2267
2268 let last = self.accounts[idx].last_fee_slot;
2269 let dt_fee = now_slot.saturating_sub(last);
2270 if dt_fee == 0 {
2271 self.accounts[idx].last_fee_slot = now_slot;
2272 return Ok(());
2273 }
2274
2275 let fee_due = (dt_fee as u128)
2277 .checked_mul(fee_per_slot)
2278 .ok_or(RiskError::Overflow)?;
2279
2280 if fee_due > MAX_PROTOCOL_FEE_ABS {
2282 return Err(RiskError::Overflow);
2283 }
2284
2285 self.accounts[idx].last_fee_slot = now_slot;
2287
2288 if fee_due > 0 {
2290 self.charge_fee_to_insurance(idx, fee_due)?;
2291 }
2292
2293 Ok(())
2294 }
2295
2296 test_visible! {
2301 fn add_user(&mut self, fee_payment: u128) -> Result<u16> {
2302 let used_count = self.num_used_accounts as u64;
2303 if used_count >= self.params.max_accounts {
2304 return Err(RiskError::Overflow);
2305 }
2306
2307 let required_fee = self.params.new_account_fee.get();
2308 if fee_payment < required_fee {
2309 return Err(RiskError::InsufficientBalance);
2310 }
2311
2312 let v_candidate = self.vault.get().checked_add(fee_payment)
2314 .ok_or(RiskError::Overflow)?;
2315 if v_candidate > MAX_VAULT_TVL {
2316 return Err(RiskError::Overflow);
2317 }
2318
2319 self.materialized_account_count = self.materialized_account_count
2322 .checked_add(1).ok_or(RiskError::Overflow)?;
2323 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2324 self.materialized_account_count -= 1;
2325 return Err(RiskError::Overflow);
2326 }
2327
2328 let idx = match self.alloc_slot() {
2329 Ok(i) => i,
2330 Err(e) => {
2331 self.materialized_account_count -= 1;
2332 return Err(e);
2333 }
2334 };
2335
2336 let excess = fee_payment.saturating_sub(required_fee);
2338 self.vault = U128::new(v_candidate);
2339 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2340
2341 let account_id = self.next_account_id;
2342 self.next_account_id = self.next_account_id.saturating_add(1);
2343
2344 self.accounts[idx as usize] = Account {
2345 kind: Account::KIND_USER,
2346 account_id,
2347 capital: U128::new(excess),
2348 pnl: 0i128,
2349 reserved_pnl: 0u128,
2350 warmup_started_at_slot: self.current_slot,
2351 warmup_slope_per_step: 0u128,
2352 position_basis_q: 0i128,
2353 adl_a_basis: ADL_ONE,
2354 adl_k_snap: 0i128,
2355 adl_epoch_snap: 0,
2356 matcher_program: [0; 32],
2357 matcher_context: [0; 32],
2358 owner: [0; 32],
2359 fee_credits: I128::ZERO,
2360 last_fee_slot: self.current_slot,
2361 fees_earned_total: U128::ZERO,
2362 };
2363
2364 if excess > 0 {
2365 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2366 .ok_or(RiskError::Overflow)?);
2367 }
2368
2369 Ok(idx)
2370 }
2371 }
2372
2373 test_visible! {
2374 fn add_lp(
2375 &mut self,
2376 matching_engine_program: [u8; 32],
2377 matching_engine_context: [u8; 32],
2378 fee_payment: u128,
2379 ) -> Result<u16> {
2380 let used_count = self.num_used_accounts as u64;
2381 if used_count >= self.params.max_accounts {
2382 return Err(RiskError::Overflow);
2383 }
2384
2385 let required_fee = self.params.new_account_fee.get();
2386 if fee_payment < required_fee {
2387 return Err(RiskError::InsufficientBalance);
2388 }
2389
2390 let v_candidate = self.vault.get().checked_add(fee_payment)
2392 .ok_or(RiskError::Overflow)?;
2393 if v_candidate > MAX_VAULT_TVL {
2394 return Err(RiskError::Overflow);
2395 }
2396
2397 self.materialized_account_count = self.materialized_account_count
2399 .checked_add(1).ok_or(RiskError::Overflow)?;
2400 if self.materialized_account_count > MAX_MATERIALIZED_ACCOUNTS {
2401 self.materialized_account_count -= 1;
2402 return Err(RiskError::Overflow);
2403 }
2404
2405 let idx = match self.alloc_slot() {
2406 Ok(i) => i,
2407 Err(e) => {
2408 self.materialized_account_count -= 1;
2409 return Err(e);
2410 }
2411 };
2412
2413 let excess = fee_payment.saturating_sub(required_fee);
2415 self.vault = U128::new(v_candidate);
2416 self.insurance_fund.balance = self.insurance_fund.balance + required_fee;
2417
2418 let account_id = self.next_account_id;
2419 self.next_account_id = self.next_account_id.saturating_add(1);
2420
2421 self.accounts[idx as usize] = Account {
2422 kind: Account::KIND_LP,
2423 account_id,
2424 capital: U128::new(excess),
2425 pnl: 0i128,
2426 reserved_pnl: 0u128,
2427 warmup_started_at_slot: self.current_slot,
2428 warmup_slope_per_step: 0u128,
2429 position_basis_q: 0i128,
2430 adl_a_basis: ADL_ONE,
2431 adl_k_snap: 0i128,
2432 adl_epoch_snap: 0,
2433 matcher_program: matching_engine_program,
2434 matcher_context: matching_engine_context,
2435 owner: [0; 32],
2436 fee_credits: I128::ZERO,
2437 last_fee_slot: self.current_slot,
2438 fees_earned_total: U128::ZERO,
2439 };
2440
2441 if excess > 0 {
2442 self.c_tot = U128::new(self.c_tot.get().checked_add(excess)
2443 .ok_or(RiskError::Overflow)?);
2444 }
2445
2446 Ok(idx)
2447 }
2448 }
2449
2450 pub fn set_owner(&mut self, idx: u16, owner: [u8; 32]) -> Result<()> {
2451 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
2452 return Err(RiskError::Unauthorized);
2453 }
2454 if self.accounts[idx as usize].owner != [0u8; 32] {
2458 return Err(RiskError::Unauthorized);
2459 }
2460 self.accounts[idx as usize].owner = owner;
2461 Ok(())
2462 }
2463
2464 pub fn deposit(
2469 &mut self,
2470 idx: u16,
2471 amount: u128,
2472 _oracle_price: u64,
2473 now_slot: u64,
2474 ) -> Result<()> {
2475 if now_slot < self.current_slot {
2477 return Err(RiskError::Overflow);
2478 }
2479 if now_slot < self.last_market_slot {
2480 return Err(RiskError::Overflow);
2481 }
2482
2483 if !self.is_used(idx as usize) {
2486 let min_dep = self.params.min_initial_deposit.get();
2487 if amount < min_dep {
2488 return Err(RiskError::InsufficientBalance);
2489 }
2490 self.materialize_at(idx, now_slot)?;
2491 }
2492
2493 self.current_slot = now_slot;
2495
2496 let v_candidate = self
2498 .vault
2499 .get()
2500 .checked_add(amount)
2501 .ok_or(RiskError::Overflow)?;
2502 if v_candidate > MAX_VAULT_TVL {
2503 return Err(RiskError::Overflow);
2504 }
2505 self.vault = U128::new(v_candidate);
2506
2507 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), amount);
2509 self.set_capital(idx as usize, new_cap);
2510
2511 self.settle_losses(idx as usize);
2513
2514 if self.accounts[idx as usize].position_basis_q == 0 && self.accounts[idx as usize].pnl >= 0
2523 {
2524 self.fee_debt_sweep(idx as usize);
2525 }
2526
2527 Ok(())
2528 }
2529
2530 pub fn withdraw(
2535 &mut self,
2536 idx: u16,
2537 amount: u128,
2538 oracle_price: u64,
2539 now_slot: u64,
2540 funding_rate: i64,
2541 ) -> Result<()> {
2542 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2543 return Err(RiskError::Overflow);
2544 }
2545
2546 if !self.is_used(idx as usize) {
2551 return Err(RiskError::AccountNotFound);
2552 }
2553
2554 let mut ctx = InstructionContext::new();
2555
2556 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
2558
2559 if self.accounts[idx as usize].capital.get() < amount {
2561 return Err(RiskError::InsufficientBalance);
2562 }
2563
2564 let post_cap = self.accounts[idx as usize].capital.get() - amount;
2566 if post_cap != 0 && post_cap < self.params.min_initial_deposit.get() {
2567 return Err(RiskError::InsufficientBalance);
2568 }
2569
2570 let eff = self.effective_pos_q(idx as usize);
2572 if eff != 0 {
2573 let old_cap = self.accounts[idx as usize].capital.get();
2575 let old_vault = self.vault;
2576 self.set_capital(idx as usize, post_cap);
2577 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2578 let passes_im = self.is_above_initial_margin(
2579 &self.accounts[idx as usize],
2580 idx as usize,
2581 oracle_price,
2582 );
2583 self.set_capital(idx as usize, old_cap);
2585 self.vault = old_vault;
2586 if !passes_im {
2587 return Err(RiskError::Undercollateralized);
2588 }
2589 }
2590
2591 self.set_capital(
2593 idx as usize,
2594 self.accounts[idx as usize].capital.get() - amount,
2595 );
2596 self.vault = U128::new(sub_u128(self.vault.get(), amount));
2597
2598 self.schedule_end_of_instruction_resets(&mut ctx)?;
2600 self.finalize_end_of_instruction_resets(&ctx);
2601 self.recompute_r_last_from_final_state(funding_rate)?;
2602
2603 Ok(())
2604 }
2605
2606 pub fn settle_account(
2613 &mut self,
2614 idx: u16,
2615 oracle_price: u64,
2616 now_slot: u64,
2617 funding_rate: i64,
2618 ) -> Result<()> {
2619 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2620 return Err(RiskError::Overflow);
2621 }
2622 if !self.is_used(idx as usize) {
2623 return Err(RiskError::AccountNotFound);
2624 }
2625
2626 let mut ctx = InstructionContext::new();
2627
2628 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
2630
2631 self.schedule_end_of_instruction_resets(&mut ctx)?;
2633 self.finalize_end_of_instruction_resets(&ctx);
2634 self.recompute_r_last_from_final_state(funding_rate)?;
2635
2636 assert!(
2638 self.oi_eff_long_q == self.oi_eff_short_q,
2639 "OI_eff_long != OI_eff_short after settle"
2640 );
2641
2642 Ok(())
2643 }
2644
2645 pub fn execute_trade(
2650 &mut self,
2651 a: u16,
2652 b: u16,
2653 oracle_price: u64,
2654 now_slot: u64,
2655 size_q: i128,
2656 exec_price: u64,
2657 funding_rate: i64,
2658 ) -> Result<()> {
2659 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
2660 return Err(RiskError::Overflow);
2661 }
2662 if exec_price == 0 || exec_price > MAX_ORACLE_PRICE {
2663 return Err(RiskError::Overflow);
2664 }
2665 if size_q <= 0 {
2667 return Err(RiskError::Overflow);
2668 }
2669 if size_q as u128 > MAX_TRADE_SIZE_Q {
2670 return Err(RiskError::Overflow);
2671 }
2672
2673 let trade_notional_check =
2675 mul_div_floor_u128(size_q as u128, exec_price as u128, POS_SCALE);
2676 if trade_notional_check > MAX_ACCOUNT_NOTIONAL {
2677 return Err(RiskError::Overflow);
2678 }
2679
2680 if !self.is_used(a as usize) || !self.is_used(b as usize) {
2685 return Err(RiskError::AccountNotFound);
2686 }
2687 if a == b {
2688 return Err(RiskError::Overflow);
2689 }
2690
2691 let mut ctx = InstructionContext::new();
2692
2693 self.touch_account_full(a as usize, oracle_price, now_slot)?;
2695 self.touch_account_full(b as usize, oracle_price, now_slot)?;
2696
2697 let old_eff_a = self.effective_pos_q(a as usize);
2699 let old_eff_b = self.effective_pos_q(b as usize);
2700
2701 let mm_req_pre_a = if old_eff_a == 0 {
2704 0u128
2705 } else {
2706 let not = self.notional(a as usize, oracle_price);
2707 core::cmp::max(
2708 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
2709 self.params.min_nonzero_mm_req,
2710 )
2711 };
2712 let mm_req_pre_b = if old_eff_b == 0 {
2713 0u128
2714 } else {
2715 let not = self.notional(b as usize, oracle_price);
2716 core::cmp::max(
2717 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
2718 self.params.min_nonzero_mm_req,
2719 )
2720 };
2721 let maint_raw_wide_pre_a = self.account_equity_maint_raw_wide(&self.accounts[a as usize]);
2722 let maint_raw_wide_pre_b = self.account_equity_maint_raw_wide(&self.accounts[b as usize]);
2723 let buffer_pre_a = maint_raw_wide_pre_a
2724 .checked_sub(I256::from_u128(mm_req_pre_a))
2725 .expect("I256 sub");
2726 let buffer_pre_b = maint_raw_wide_pre_b
2727 .checked_sub(I256::from_u128(mm_req_pre_b))
2728 .expect("I256 sub");
2729
2730 let new_eff_a = old_eff_a.checked_add(size_q).ok_or(RiskError::Overflow)?;
2732 let neg_size_q = size_q.checked_neg().ok_or(RiskError::Overflow)?;
2733 let new_eff_b = old_eff_b
2734 .checked_add(neg_size_q)
2735 .ok_or(RiskError::Overflow)?;
2736
2737 if new_eff_a != 0 && new_eff_a.unsigned_abs() > MAX_POSITION_ABS_Q {
2739 return Err(RiskError::Overflow);
2740 }
2741 if new_eff_b != 0 && new_eff_b.unsigned_abs() > MAX_POSITION_ABS_Q {
2742 return Err(RiskError::Overflow);
2743 }
2744
2745 {
2747 let notional_a =
2748 mul_div_floor_u128(new_eff_a.unsigned_abs(), oracle_price as u128, POS_SCALE);
2749 if notional_a > MAX_ACCOUNT_NOTIONAL {
2750 return Err(RiskError::Overflow);
2751 }
2752 let notional_b =
2753 mul_div_floor_u128(new_eff_b.unsigned_abs(), oracle_price as u128, POS_SCALE);
2754 if notional_b > MAX_ACCOUNT_NOTIONAL {
2755 return Err(RiskError::Overflow);
2756 }
2757 }
2758
2759 self.maybe_finalize_ready_reset_sides();
2762
2763 self.check_side_mode_for_trade(&old_eff_a, &new_eff_a, &old_eff_b, &new_eff_b)?;
2765
2766 let price_diff = (oracle_price as i128) - (exec_price as i128);
2768 let trade_pnl_a = compute_trade_pnl(size_q, price_diff)?;
2769 let trade_pnl_b = trade_pnl_a.checked_neg().ok_or(RiskError::Overflow)?;
2770
2771 let old_r_a = self.accounts[a as usize].reserved_pnl;
2772 let old_r_b = self.accounts[b as usize].reserved_pnl;
2773
2774 let pnl_a = self.accounts[a as usize]
2775 .pnl
2776 .checked_add(trade_pnl_a)
2777 .ok_or(RiskError::Overflow)?;
2778 if pnl_a == i128::MIN {
2779 return Err(RiskError::Overflow);
2780 }
2781 self.set_pnl(a as usize, pnl_a);
2782
2783 let pnl_b = self.accounts[b as usize]
2784 .pnl
2785 .checked_add(trade_pnl_b)
2786 .ok_or(RiskError::Overflow)?;
2787 if pnl_b == i128::MIN {
2788 return Err(RiskError::Overflow);
2789 }
2790 self.set_pnl(b as usize, pnl_b);
2791
2792 if self.accounts[a as usize].reserved_pnl > old_r_a {
2794 self.restart_warmup_after_reserve_increase(a as usize);
2795 }
2796 if self.accounts[b as usize].reserved_pnl > old_r_b {
2797 self.restart_warmup_after_reserve_increase(b as usize);
2798 }
2799
2800 self.attach_effective_position(a as usize, new_eff_a);
2802 self.attach_effective_position(b as usize, new_eff_b);
2803
2804 self.update_oi_from_positions(&old_eff_a, &new_eff_a, &old_eff_b, &new_eff_b)?;
2806
2807 self.settle_losses(a as usize);
2810 self.settle_losses(b as usize);
2811
2812 let trade_notional =
2814 mul_div_floor_u128(size_q.unsigned_abs(), exec_price as u128, POS_SCALE);
2815 let fee = if trade_notional > 0 && self.params.trading_fee_bps > 0 {
2816 mul_div_ceil_u128(trade_notional, self.params.trading_fee_bps as u128, 10_000)
2817 } else {
2818 0
2819 };
2820
2821 if fee > 0 {
2823 if fee > MAX_PROTOCOL_FEE_ABS {
2824 return Err(RiskError::Overflow);
2825 }
2826 self.charge_fee_to_insurance(a as usize, fee)?;
2827 self.charge_fee_to_insurance(b as usize, fee)?;
2828 }
2829
2830 if self.accounts[a as usize].is_lp() {
2832 self.accounts[a as usize].fees_earned_total = U128::new(add_u128(
2833 self.accounts[a as usize].fees_earned_total.get(),
2834 fee,
2835 ));
2836 }
2837 if self.accounts[b as usize].is_lp() {
2838 self.accounts[b as usize].fees_earned_total = U128::new(add_u128(
2839 self.accounts[b as usize].fees_earned_total.get(),
2840 fee,
2841 ));
2842 }
2843
2844 self.enforce_post_trade_margin(
2846 a as usize,
2847 b as usize,
2848 oracle_price,
2849 &old_eff_a,
2850 &new_eff_a,
2851 &old_eff_b,
2852 &new_eff_b,
2853 buffer_pre_a,
2854 buffer_pre_b,
2855 fee,
2856 )?;
2857
2858 self.schedule_end_of_instruction_resets(&mut ctx)?;
2860 self.finalize_end_of_instruction_resets(&ctx);
2861
2862 self.recompute_r_last_from_final_state(funding_rate)?;
2864
2865 assert!(
2867 self.oi_eff_long_q == self.oi_eff_short_q,
2868 "OI_eff_long != OI_eff_short after trade"
2869 );
2870
2871 Ok(())
2872 }
2873
2874 fn charge_fee_to_insurance(&mut self, idx: usize, fee: u128) -> Result<()> {
2877 if fee > MAX_PROTOCOL_FEE_ABS {
2878 return Err(RiskError::Overflow);
2879 }
2880 let cap = self.accounts[idx].capital.get();
2881 let fee_paid = core::cmp::min(fee, cap);
2882 if fee_paid > 0 {
2883 self.set_capital(idx, cap - fee_paid);
2884 self.insurance_fund.balance = self.insurance_fund.balance + fee_paid;
2885 }
2886 let fee_shortfall = fee - fee_paid;
2887 if fee_shortfall > 0 {
2888 let current_fc = self.accounts[idx].fee_credits.get();
2892 let headroom = match current_fc.checked_add(i128::MAX) {
2894 Some(h) if h > 0 => h as u128,
2895 _ => 0u128, };
2897 let collectible = core::cmp::min(fee_shortfall, headroom);
2898 if collectible > 0 {
2899 let new_fc = current_fc - (collectible as i128);
2902 self.accounts[idx].fee_credits = I128::new(new_fc);
2903 }
2904 }
2906 Ok(())
2907 }
2908
2909 fn oi_long_component(pos: i128) -> u128 {
2911 if pos > 0 {
2912 pos as u128
2913 } else {
2914 0u128
2915 }
2916 }
2917
2918 fn oi_short_component(pos: i128) -> u128 {
2919 if pos < 0 {
2920 pos.unsigned_abs()
2921 } else {
2922 0u128
2923 }
2924 }
2925
2926 fn bilateral_oi_after(
2929 &self,
2930 old_a: &i128,
2931 new_a: &i128,
2932 old_b: &i128,
2933 new_b: &i128,
2934 ) -> Result<(u128, u128)> {
2935 let oi_long_after = self
2936 .oi_eff_long_q
2937 .checked_sub(Self::oi_long_component(*old_a))
2938 .ok_or(RiskError::CorruptState)?
2939 .checked_sub(Self::oi_long_component(*old_b))
2940 .ok_or(RiskError::CorruptState)?
2941 .checked_add(Self::oi_long_component(*new_a))
2942 .ok_or(RiskError::Overflow)?
2943 .checked_add(Self::oi_long_component(*new_b))
2944 .ok_or(RiskError::Overflow)?;
2945
2946 let oi_short_after = self
2947 .oi_eff_short_q
2948 .checked_sub(Self::oi_short_component(*old_a))
2949 .ok_or(RiskError::CorruptState)?
2950 .checked_sub(Self::oi_short_component(*old_b))
2951 .ok_or(RiskError::CorruptState)?
2952 .checked_add(Self::oi_short_component(*new_a))
2953 .ok_or(RiskError::Overflow)?
2954 .checked_add(Self::oi_short_component(*new_b))
2955 .ok_or(RiskError::Overflow)?;
2956
2957 Ok((oi_long_after, oi_short_after))
2958 }
2959
2960 fn check_side_mode_for_trade(
2963 &self,
2964 old_a: &i128,
2965 new_a: &i128,
2966 old_b: &i128,
2967 new_b: &i128,
2968 ) -> Result<()> {
2969 let (oi_long_after, oi_short_after) =
2970 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
2971
2972 for &side in &[Side::Long, Side::Short] {
2973 let mode = self.get_side_mode(side);
2974 if mode != SideMode::DrainOnly && mode != SideMode::ResetPending {
2975 continue;
2976 }
2977 let (oi_after, oi_before) = match side {
2978 Side::Long => (oi_long_after, self.oi_eff_long_q),
2979 Side::Short => (oi_short_after, self.oi_eff_short_q),
2980 };
2981 if oi_after > oi_before {
2982 return Err(RiskError::SideBlocked);
2983 }
2984 }
2985 Ok(())
2986 }
2987
2988 fn enforce_post_trade_margin(
2991 &self,
2992 a: usize,
2993 b: usize,
2994 oracle_price: u64,
2995 old_eff_a: &i128,
2996 new_eff_a: &i128,
2997 old_eff_b: &i128,
2998 new_eff_b: &i128,
2999 buffer_pre_a: I256,
3000 buffer_pre_b: I256,
3001 fee: u128,
3002 ) -> Result<()> {
3003 self.enforce_one_side_margin(a, oracle_price, old_eff_a, new_eff_a, buffer_pre_a, fee)?;
3004 self.enforce_one_side_margin(b, oracle_price, old_eff_b, new_eff_b, buffer_pre_b, fee)?;
3005 Ok(())
3006 }
3007
3008 fn enforce_one_side_margin(
3009 &self,
3010 idx: usize,
3011 oracle_price: u64,
3012 old_eff: &i128,
3013 new_eff: &i128,
3014 buffer_pre: I256,
3015 fee: u128,
3016 ) -> Result<()> {
3017 if *new_eff == 0 {
3018 let maint_raw = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3021 if maint_raw.is_negative() {
3022 return Err(RiskError::Undercollateralized);
3023 }
3024 return Ok(());
3025 }
3026
3027 let abs_old: u128 = if *old_eff == 0 {
3028 0u128
3029 } else {
3030 old_eff.unsigned_abs()
3031 };
3032 let abs_new = new_eff.unsigned_abs();
3033
3034 let risk_increasing = abs_new > abs_old
3036 || (*old_eff > 0 && *new_eff < 0)
3037 || (*old_eff < 0 && *new_eff > 0)
3038 || *old_eff == 0;
3039
3040 let strictly_reducing = *old_eff != 0
3042 && *new_eff != 0
3043 && ((*old_eff > 0 && *new_eff > 0) || (*old_eff < 0 && *new_eff < 0))
3044 && abs_new < abs_old;
3045
3046 if risk_increasing {
3047 if !self.is_above_initial_margin(&self.accounts[idx], idx, oracle_price) {
3049 return Err(RiskError::Undercollateralized);
3050 }
3051 } else if self.is_above_maintenance_margin(&self.accounts[idx], idx, oracle_price) {
3052 } else if strictly_reducing {
3054 let maint_raw_wide_post = self.account_equity_maint_raw_wide(&self.accounts[idx]);
3059 let fee_wide = I256::from_u128(fee);
3060
3061 let maint_raw_fee_neutral =
3063 maint_raw_wide_post.checked_add(fee_wide).expect("I256 add");
3064 let mm_req_post = {
3065 let not = self.notional(idx, oracle_price);
3066 core::cmp::max(
3067 mul_div_floor_u128(not, self.params.maintenance_margin_bps as u128, 10_000),
3068 self.params.min_nonzero_mm_req,
3069 )
3070 };
3071 let buffer_post_fee_neutral = maint_raw_fee_neutral
3072 .checked_sub(I256::from_u128(mm_req_post))
3073 .expect("I256 sub");
3074
3075 let mm_req_pre = {
3077 let not_pre = if *old_eff == 0 {
3078 0u128
3079 } else {
3080 mul_div_floor_u128(old_eff.unsigned_abs(), oracle_price as u128, POS_SCALE)
3081 };
3082 core::cmp::max(
3083 mul_div_floor_u128(not_pre, self.params.maintenance_margin_bps as u128, 10_000),
3084 self.params.min_nonzero_mm_req,
3085 )
3086 };
3087 let maint_raw_pre = buffer_pre
3088 .checked_add(I256::from_u128(mm_req_pre))
3089 .expect("I256 add");
3090
3091 let cond1 = buffer_post_fee_neutral > buffer_pre;
3093
3094 let zero = I256::from_i128(0);
3097 let shortfall_post = if maint_raw_fee_neutral < zero {
3098 maint_raw_fee_neutral
3099 } else {
3100 zero
3101 };
3102 let shortfall_pre = if maint_raw_pre < zero {
3103 maint_raw_pre
3104 } else {
3105 zero
3106 };
3107 let cond2 = shortfall_post >= shortfall_pre;
3108
3109 if cond1 && cond2 {
3110 } else {
3112 return Err(RiskError::Undercollateralized);
3113 }
3114 } else {
3115 return Err(RiskError::Undercollateralized);
3116 }
3117 Ok(())
3118 }
3119
3120 fn update_oi_from_positions(
3123 &mut self,
3124 old_a: &i128,
3125 new_a: &i128,
3126 old_b: &i128,
3127 new_b: &i128,
3128 ) -> Result<()> {
3129 let (oi_long_after, oi_short_after) =
3130 self.bilateral_oi_after(old_a, new_a, old_b, new_b)?;
3131
3132 if oi_long_after > MAX_OI_SIDE_Q {
3134 return Err(RiskError::Overflow);
3135 }
3136 if oi_short_after > MAX_OI_SIDE_Q {
3137 return Err(RiskError::Overflow);
3138 }
3139
3140 self.oi_eff_long_q = oi_long_after;
3141 self.oi_eff_short_q = oi_short_after;
3142
3143 Ok(())
3144 }
3145
3146 pub fn liquidate_at_oracle(
3153 &mut self,
3154 idx: u16,
3155 now_slot: u64,
3156 oracle_price: u64,
3157 policy: LiquidationPolicy,
3158 funding_rate: i64,
3159 ) -> Result<bool> {
3160 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3163 return Ok(false);
3164 }
3165
3166 let mut ctx = InstructionContext::new();
3167
3168 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3170
3171 let result =
3172 self.liquidate_at_oracle_internal(idx, now_slot, oracle_price, policy, &mut ctx)?;
3173
3174 self.schedule_end_of_instruction_resets(&mut ctx)?;
3177 self.finalize_end_of_instruction_resets(&ctx);
3178 self.recompute_r_last_from_final_state(funding_rate)?;
3179
3180 assert!(
3182 self.oi_eff_long_q == self.oi_eff_short_q,
3183 "OI_eff_long != OI_eff_short after liquidation"
3184 );
3185 Ok(result)
3186 }
3187
3188 fn liquidate_at_oracle_internal(
3192 &mut self,
3193 idx: u16,
3194 _now_slot: u64,
3195 oracle_price: u64,
3196 policy: LiquidationPolicy,
3197 ctx: &mut InstructionContext,
3198 ) -> Result<bool> {
3199 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3200 return Ok(false);
3201 }
3202
3203 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3204 return Err(RiskError::Overflow);
3205 }
3206
3207 let old_eff = self.effective_pos_q(idx as usize);
3209 if old_eff == 0 {
3210 return Ok(false);
3211 }
3212
3213 if self.is_above_maintenance_margin(
3215 &self.accounts[idx as usize],
3216 idx as usize,
3217 oracle_price,
3218 ) {
3219 return Ok(false);
3220 }
3221
3222 let liq_side = side_of_i128(old_eff).unwrap();
3223 let abs_old_eff = old_eff.unsigned_abs();
3224
3225 match policy {
3226 LiquidationPolicy::ExactPartial(q_close_q) => {
3227 if q_close_q == 0 || q_close_q >= abs_old_eff {
3230 return Err(RiskError::Overflow);
3231 }
3232 let new_eff_abs_q = abs_old_eff
3234 .checked_sub(q_close_q)
3235 .ok_or(RiskError::Overflow)?;
3236 if new_eff_abs_q == 0 {
3238 return Err(RiskError::Overflow);
3239 }
3240 let sign = if old_eff > 0 { 1i128 } else { -1i128 };
3242 let new_eff = sign
3243 .checked_mul(new_eff_abs_q as i128)
3244 .ok_or(RiskError::Overflow)?;
3245
3246 self.attach_effective_position(idx as usize, new_eff);
3248
3249 self.settle_losses(idx as usize);
3251
3252 let liq_fee = {
3254 let notional_val =
3255 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3256 let liq_fee_raw = mul_div_ceil_u128(
3257 notional_val,
3258 self.params.liquidation_fee_bps as u128,
3259 10_000,
3260 );
3261 core::cmp::min(
3262 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3263 self.params.liquidation_fee_cap.get(),
3264 )
3265 };
3266 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3267
3268 self.enqueue_adl(ctx, liq_side, q_close_q, 0)?;
3270
3271 if !self.is_above_maintenance_margin(
3277 &self.accounts[idx as usize],
3278 idx as usize,
3279 oracle_price,
3280 ) {
3281 return Err(RiskError::Undercollateralized);
3282 }
3283
3284 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3285 Ok(true)
3286 }
3287 LiquidationPolicy::FullClose => {
3288 let q_close_q = abs_old_eff;
3290
3291 self.attach_effective_position(idx as usize, 0i128);
3293
3294 self.settle_losses(idx as usize);
3296
3297 let liq_fee = if q_close_q == 0 {
3299 0u128
3300 } else {
3301 let notional_val =
3302 mul_div_floor_u128(q_close_q, oracle_price as u128, POS_SCALE);
3303 let liq_fee_raw = mul_div_ceil_u128(
3304 notional_val,
3305 self.params.liquidation_fee_bps as u128,
3306 10_000,
3307 );
3308 core::cmp::min(
3309 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3310 self.params.liquidation_fee_cap.get(),
3311 )
3312 };
3313 self.charge_fee_to_insurance(idx as usize, liq_fee)?;
3314
3315 let eff_post = self.effective_pos_q(idx as usize);
3317 let d: u128 = if eff_post == 0 && self.accounts[idx as usize].pnl < 0 {
3318 assert!(
3319 self.accounts[idx as usize].pnl != i128::MIN,
3320 "liquidate: i128::MIN pnl"
3321 );
3322 self.accounts[idx as usize].pnl.unsigned_abs()
3323 } else {
3324 0u128
3325 };
3326
3327 if q_close_q != 0 || d != 0 {
3329 self.enqueue_adl(ctx, liq_side, q_close_q, d)?;
3330 }
3331
3332 if d != 0 {
3334 self.set_pnl(idx as usize, 0i128);
3335 }
3336
3337 self.lifetime_liquidations = self.lifetime_liquidations.saturating_add(1);
3338 Ok(true)
3339 }
3340 }
3341 }
3342
3343 pub fn keeper_crank(
3351 &mut self,
3352 now_slot: u64,
3353 oracle_price: u64,
3354 ordered_candidates: &[(u16, Option<LiquidationPolicy>)],
3355 max_revalidations: u16,
3356 funding_rate: i64,
3357 ) -> Result<CrankOutcome> {
3358 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3359 return Err(RiskError::Overflow);
3360 }
3361
3362 let mut ctx = InstructionContext::new();
3364
3365 if now_slot < self.current_slot {
3367 return Err(RiskError::Overflow);
3368 }
3369 if now_slot < self.last_market_slot {
3370 return Err(RiskError::Overflow);
3371 }
3372
3373 self.accrue_market_to(now_slot, oracle_price)?;
3375
3376 self.current_slot = now_slot;
3378
3379 let advanced = now_slot > self.last_crank_slot;
3380 if advanced {
3381 self.last_crank_slot = now_slot;
3382 }
3383
3384 let mut attempts: u16 = 0;
3386 let mut num_liquidations: u32 = 0;
3387
3388 for &(candidate_idx, ref hint) in ordered_candidates {
3389 if attempts >= max_revalidations {
3391 break;
3392 }
3393 if ctx.pending_reset_long || ctx.pending_reset_short {
3395 break;
3396 }
3397 if (candidate_idx as usize) >= MAX_ACCOUNTS || !self.is_used(candidate_idx as usize) {
3399 continue;
3400 }
3401
3402 attempts += 1;
3404 let cidx = candidate_idx as usize;
3405
3406 self.advance_profit_warmup(cidx);
3411
3412 self.settle_side_effects(cidx)?;
3414
3415 self.settle_losses(cidx);
3417
3418 if self.effective_pos_q(cidx) == 0 && self.accounts[cidx].pnl < 0 {
3420 self.resolve_flat_negative(cidx);
3421 }
3422
3423 self.settle_maintenance_fee_internal(cidx, now_slot)?;
3425
3426 if self.accounts[cidx].position_basis_q == 0 {
3428 self.do_profit_conversion(cidx);
3429 }
3430
3431 self.fee_debt_sweep(cidx);
3433
3434 if !ctx.pending_reset_long && !ctx.pending_reset_short {
3437 let eff = self.effective_pos_q(cidx);
3438 if eff != 0 {
3439 if !self.is_above_maintenance_margin(&self.accounts[cidx], cidx, oracle_price) {
3440 if let Some(policy) =
3444 self.validate_keeper_hint(candidate_idx, eff, hint, oracle_price)
3445 {
3446 match self.liquidate_at_oracle_internal(
3447 candidate_idx,
3448 now_slot,
3449 oracle_price,
3450 policy,
3451 &mut ctx,
3452 ) {
3453 Ok(true) => {
3454 num_liquidations += 1;
3455 }
3456 Ok(false) => {}
3457 Err(e) => return Err(e),
3458 }
3459 }
3460 }
3461 }
3462 }
3463 }
3464
3465 self.schedule_end_of_instruction_resets(&mut ctx)?;
3467 self.finalize_end_of_instruction_resets(&ctx);
3468
3469 self.recompute_r_last_from_final_state(funding_rate)?;
3471
3472 assert!(
3474 self.oi_eff_long_q == self.oi_eff_short_q,
3475 "OI_eff_long != OI_eff_short after keeper_crank"
3476 );
3477
3478 Ok(CrankOutcome {
3479 advanced,
3480 slots_forgiven: 0,
3481 caller_settle_ok: true,
3482 force_realize_needed: false,
3483 panic_needed: false,
3484 num_liquidations,
3485 num_liq_errors: 0,
3486 num_gc_closed: 0,
3487 last_cursor: 0,
3488 sweep_complete: false,
3489 })
3490 }
3491
3492 test_visible! {
3502 fn validate_keeper_hint(
3503 &self,
3504 idx: u16,
3505 eff: i128,
3506 hint: &Option<LiquidationPolicy>,
3507 oracle_price: u64,
3508 ) -> Option<LiquidationPolicy> {
3509 match hint {
3510 None => None,
3512 Some(LiquidationPolicy::FullClose) => Some(LiquidationPolicy::FullClose),
3513 Some(LiquidationPolicy::ExactPartial(q_close_q)) => {
3514 let abs_eff = eff.unsigned_abs();
3515 if *q_close_q == 0 || *q_close_q >= abs_eff {
3518 return None;
3519 }
3520
3521 let account = &self.accounts[idx as usize];
3523
3524 let notional_closed = mul_div_floor_u128(*q_close_q, oracle_price as u128, POS_SCALE);
3526 let liq_fee_raw = mul_div_ceil_u128(notional_closed, self.params.liquidation_fee_bps as u128, 10_000);
3527 let liq_fee = core::cmp::min(
3528 core::cmp::max(liq_fee_raw, self.params.min_liquidation_abs.get()),
3529 self.params.liquidation_fee_cap.get(),
3530 );
3531
3532 let cap = account.capital.get();
3536 let fee_from_capital = core::cmp::min(liq_fee, cap);
3537 let fee_shortfall = liq_fee - fee_from_capital;
3538 let current_fc = account.fee_credits.get();
3539 let fc_headroom = match current_fc.checked_add(i128::MAX) {
3540 Some(h) if h > 0 => h as u128,
3541 _ => 0u128,
3542 };
3543 let fee_from_debt = core::cmp::min(fee_shortfall, fc_headroom);
3544 let fee_applied = fee_from_capital + fee_from_debt;
3545
3546 let eq_raw_wide = self.account_equity_maint_raw_wide(account);
3547 let predicted_eq = match eq_raw_wide.checked_sub(I256::from_u128(fee_applied)) {
3548 Some(v) => v,
3549 None => return None,
3550 };
3551
3552 let rem_eff = abs_eff - *q_close_q;
3554 let rem_notional = mul_div_floor_u128(rem_eff, oracle_price as u128, POS_SCALE);
3555 let proportional_mm = mul_div_floor_u128(rem_notional, self.params.maintenance_margin_bps as u128, 10_000);
3556 let predicted_mm_req = if rem_eff == 0 {
3557 0u128
3558 } else {
3559 core::cmp::max(proportional_mm, self.params.min_nonzero_mm_req)
3560 };
3561
3562 if predicted_eq <= I256::from_u128(predicted_mm_req) {
3565 return None;
3566 }
3567
3568 Some(LiquidationPolicy::ExactPartial(*q_close_q))
3569 }
3570 }
3571 }
3572 }
3573
3574 pub fn convert_released_pnl(
3580 &mut self,
3581 idx: u16,
3582 x_req: u128,
3583 oracle_price: u64,
3584 now_slot: u64,
3585 funding_rate: i64,
3586 ) -> Result<()> {
3587 if oracle_price == 0 || oracle_price > MAX_ORACLE_PRICE {
3588 return Err(RiskError::Overflow);
3589 }
3590 if !self.is_used(idx as usize) {
3591 return Err(RiskError::AccountNotFound);
3592 }
3593
3594 let mut ctx = InstructionContext::new();
3595
3596 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3598
3599 if self.accounts[idx as usize].position_basis_q == 0 {
3601 self.schedule_end_of_instruction_resets(&mut ctx)?;
3602 self.finalize_end_of_instruction_resets(&ctx);
3603 self.recompute_r_last_from_final_state(funding_rate)?;
3604 return Ok(());
3605 }
3606
3607 let released = self.released_pos(idx as usize);
3609 if x_req == 0 || x_req > released {
3610 return Err(RiskError::Overflow);
3611 }
3612
3613 let (h_num, h_den) = self.haircut_ratio();
3616 assert!(
3617 h_den > 0,
3618 "convert_released_pnl: h_den must be > 0 when x_req > 0"
3619 );
3620 let y: u128 = wide_mul_div_floor_u128(x_req, h_num, h_den);
3621
3622 self.consume_released_pnl(idx as usize, x_req);
3624
3625 let new_cap = add_u128(self.accounts[idx as usize].capital.get(), y);
3627 self.set_capital(idx as usize, new_cap);
3628
3629 self.fee_debt_sweep(idx as usize);
3631
3632 let eff = self.effective_pos_q(idx as usize);
3634 if eff != 0 {
3635 if !self.is_above_maintenance_margin(
3636 &self.accounts[idx as usize],
3637 idx as usize,
3638 oracle_price,
3639 ) {
3640 return Err(RiskError::Undercollateralized);
3641 }
3642 }
3643
3644 self.schedule_end_of_instruction_resets(&mut ctx)?;
3646 self.finalize_end_of_instruction_resets(&ctx);
3647 self.recompute_r_last_from_final_state(funding_rate)?;
3648
3649 Ok(())
3650 }
3651
3652 pub fn close_account(
3657 &mut self,
3658 idx: u16,
3659 now_slot: u64,
3660 oracle_price: u64,
3661 funding_rate: i64,
3662 ) -> Result<u128> {
3663 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3664 return Err(RiskError::AccountNotFound);
3665 }
3666
3667 let mut ctx = InstructionContext::new();
3668
3669 self.touch_account_full(idx as usize, oracle_price, now_slot)?;
3670
3671 let eff = self.effective_pos_q(idx as usize);
3673 if eff != 0 {
3674 return Err(RiskError::Undercollateralized);
3675 }
3676
3677 if self.accounts[idx as usize].pnl > 0 {
3680 return Err(RiskError::PnlNotWarmedUp);
3681 }
3682 if self.accounts[idx as usize].pnl < 0 {
3683 return Err(RiskError::Undercollateralized);
3684 }
3685
3686 if self.accounts[idx as usize].fee_credits.get() < 0 {
3688 self.accounts[idx as usize].fee_credits = I128::ZERO;
3689 }
3690
3691 let capital = self.accounts[idx as usize].capital;
3692
3693 if capital > self.vault {
3694 return Err(RiskError::InsufficientBalance);
3695 }
3696 self.vault = self.vault - capital;
3697 self.set_capital(idx as usize, 0);
3698
3699 self.schedule_end_of_instruction_resets(&mut ctx)?;
3701 self.finalize_end_of_instruction_resets(&ctx);
3702 self.recompute_r_last_from_final_state(funding_rate)?;
3703
3704 self.free_slot(idx);
3705
3706 Ok(capital.get())
3707 }
3708
3709 pub fn force_close_resolved(&mut self, idx: u16) -> Result<u128> {
3724 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3725 return Err(RiskError::AccountNotFound);
3726 }
3727
3728 let i = idx as usize;
3729
3730 if self.accounts[i].position_basis_q != 0 {
3732 let settle_ok = self.settle_side_effects(i).is_ok();
3734
3735 if !settle_ok {
3736 let basis = self.accounts[i].position_basis_q;
3740 let abs_basis = basis.unsigned_abs();
3741 let a_basis = self.accounts[i].adl_a_basis;
3742 let k_snap = self.accounts[i].adl_k_snap;
3743
3744 if a_basis > 0 {
3745 let side = side_of_i128(basis).unwrap();
3746 let epoch_snap = self.accounts[i].adl_epoch_snap;
3747 let epoch_side = self.get_epoch_side(side);
3748
3749 let k_end = if epoch_snap == epoch_side {
3751 self.get_k_side(side)
3752 } else {
3753 self.get_k_epoch_start(side)
3754 };
3755
3756 let den = a_basis.checked_mul(POS_SCALE).ok_or(RiskError::Overflow)?;
3757 let pnl_delta =
3758 wide_signed_mul_div_floor_from_k_pair(abs_basis, k_snap, k_end, den);
3759
3760 if pnl_delta != 0 {
3761 let old_r = self.accounts[i].reserved_pnl;
3762 let new_pnl = self.accounts[i]
3763 .pnl
3764 .checked_add(pnl_delta)
3765 .ok_or(RiskError::Overflow)?;
3766 if new_pnl == i128::MIN {
3767 return Err(RiskError::Overflow);
3768 }
3769 self.set_pnl(i, new_pnl);
3770 if self.accounts[i].reserved_pnl > old_r {
3771 self.restart_warmup_after_reserve_increase(i);
3772 }
3773 }
3774
3775 if epoch_snap != epoch_side {
3777 let old_stale = self.get_stale_count(side);
3778 if old_stale > 0 {
3779 self.set_stale_count(side, old_stale - 1);
3780 }
3781 }
3782 }
3783
3784 self.set_position_basis_q(i, 0);
3786 self.accounts[i].adl_a_basis = ADL_ONE;
3787 self.accounts[i].adl_k_snap = 0;
3788 self.accounts[i].adl_epoch_snap = 0;
3789 }
3790
3791 if self.accounts[i].position_basis_q != 0 {
3794 self.set_position_basis_q(i, 0);
3795 self.accounts[i].adl_a_basis = ADL_ONE;
3796 self.accounts[i].adl_k_snap = 0;
3797 self.accounts[i].adl_epoch_snap = 0;
3798 }
3799 }
3800
3801 self.settle_losses(i);
3803
3804 self.resolve_flat_negative(i);
3806
3807 if self.accounts[i].pnl > 0 {
3809 self.set_reserved_pnl(i, 0);
3811 let pos_pnl = self.accounts[i].pnl as u128;
3813 let released = self.released_pos(i);
3814 if released > 0 {
3815 let (h_num, h_den) = self.haircut_ratio();
3816 let y = if h_den == 0 {
3817 released
3818 } else {
3819 wide_mul_div_floor_u128(released, h_num, h_den)
3820 };
3821 self.consume_released_pnl(i, released);
3822 let new_cap = add_u128(self.accounts[i].capital.get(), y);
3823 self.set_capital(i, new_cap);
3824 }
3825 }
3829
3830 self.fee_debt_sweep(i);
3832
3833 if self.accounts[i].fee_credits.get() < 0 {
3835 self.accounts[i].fee_credits = I128::ZERO;
3836 }
3837
3838 let capital = self.accounts[i].capital;
3840 if capital > self.vault {
3841 return Err(RiskError::InsufficientBalance);
3842 }
3843 self.vault = self.vault - capital;
3844 self.set_capital(i, 0);
3845
3846 self.free_slot(idx);
3847
3848 Ok(capital.get())
3849 }
3850
3851 pub fn reclaim_empty_account(&mut self, idx: u16, now_slot: u64) -> Result<()> {
3860 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
3861 return Err(RiskError::AccountNotFound);
3862 }
3863 if now_slot < self.current_slot {
3864 return Err(RiskError::Overflow);
3865 }
3866
3867 let account = &self.accounts[idx as usize];
3869 if account.position_basis_q != 0 {
3870 return Err(RiskError::Undercollateralized);
3871 }
3872 if account.pnl != 0 {
3873 return Err(RiskError::Undercollateralized);
3874 }
3875 if account.reserved_pnl != 0 {
3876 return Err(RiskError::Undercollateralized);
3877 }
3878 if account.fee_credits.get() > 0 {
3879 return Err(RiskError::Undercollateralized);
3880 }
3881
3882 self.current_slot = now_slot;
3884
3885 self.settle_maintenance_fee_internal(idx as usize, now_slot)?;
3887
3888 if self.accounts[idx as usize].capital.get() >= self.params.min_initial_deposit.get()
3891 && !self.accounts[idx as usize].capital.is_zero()
3892 {
3893 return Err(RiskError::Undercollateralized);
3894 }
3895
3896 let dust_cap = self.accounts[idx as usize].capital.get();
3898 if dust_cap > 0 {
3899 self.set_capital(idx as usize, 0);
3900 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
3901 }
3902
3903 if self.accounts[idx as usize].fee_credits.get() < 0 {
3905 self.accounts[idx as usize].fee_credits = I128::new(0);
3906 }
3907
3908 self.free_slot(idx);
3910
3911 Ok(())
3912 }
3913
3914 test_visible! {
3919 fn garbage_collect_dust(&mut self) -> u32 {
3920 let mut to_free: [u16; GC_CLOSE_BUDGET as usize] = [0; GC_CLOSE_BUDGET as usize];
3921 let mut num_to_free = 0usize;
3922
3923 let max_scan = (ACCOUNTS_PER_CRANK as usize).min(MAX_ACCOUNTS);
3924 let start = self.gc_cursor as usize;
3925
3926 let mut scanned: usize = 0;
3927 for offset in 0..max_scan {
3928 if num_to_free >= GC_CLOSE_BUDGET as usize {
3929 break;
3930 }
3931 scanned = offset + 1;
3932
3933 let idx = (start + offset) & ACCOUNT_IDX_MASK;
3934 let block = idx >> 6;
3935 let bit = idx & 63;
3936 if (self.used[block] & (1u64 << bit)) == 0 {
3937 continue;
3938 }
3939
3940 let account = &self.accounts[idx];
3947 if account.position_basis_q != 0 {
3948 continue;
3949 }
3950 if account.capital.get() >= self.params.min_initial_deposit.get()
3952 && !account.capital.is_zero() {
3953 continue;
3954 }
3955 if account.reserved_pnl != 0 {
3956 continue;
3957 }
3958 if account.pnl != 0 {
3961 continue;
3962 }
3963 if account.fee_credits.get() > 0 {
3964 continue;
3965 }
3966
3967 let dust_cap = self.accounts[idx].capital.get();
3969 if dust_cap > 0 {
3970 self.set_capital(idx, 0);
3971 self.insurance_fund.balance = self.insurance_fund.balance + dust_cap;
3972 }
3973
3974 if self.accounts[idx].fee_credits.get() < 0 {
3976 self.accounts[idx].fee_credits = I128::new(0);
3977 }
3978
3979 to_free[num_to_free] = idx as u16;
3980 num_to_free += 1;
3981 }
3982
3983 self.gc_cursor = ((start + scanned) & ACCOUNT_IDX_MASK) as u16;
3986
3987 for i in 0..num_to_free {
3988 self.free_slot(to_free[i]);
3989 }
3990
3991 num_to_free as u32
3992 }
3993 }
3994
3995 fn require_fresh_crank(&self, now_slot: u64) -> Result<()> {
4000 if now_slot.saturating_sub(self.last_crank_slot) > self.max_crank_staleness_slots {
4001 return Err(RiskError::Unauthorized);
4002 }
4003 Ok(())
4004 }
4005
4006 fn require_recent_full_sweep(&self, now_slot: u64) -> Result<()> {
4007 if now_slot.saturating_sub(self.last_full_sweep_start_slot) > self.max_crank_staleness_slots
4008 {
4009 return Err(RiskError::Unauthorized);
4010 }
4011 Ok(())
4012 }
4013
4014 pub fn top_up_insurance_fund(&mut self, amount: u128, now_slot: u64) -> Result<bool> {
4019 if now_slot < self.current_slot {
4021 return Err(RiskError::Overflow);
4022 }
4023 self.current_slot = now_slot;
4024 let new_vault = self
4025 .vault
4026 .get()
4027 .checked_add(amount)
4028 .ok_or(RiskError::Overflow)?;
4029 if new_vault > MAX_VAULT_TVL {
4030 return Err(RiskError::Overflow);
4031 }
4032 let new_ins = self
4033 .insurance_fund
4034 .balance
4035 .get()
4036 .checked_add(amount)
4037 .ok_or(RiskError::Overflow)?;
4038 self.vault = U128::new(new_vault);
4039 self.insurance_fund.balance = U128::new(new_ins);
4040 Ok(self.insurance_fund.balance.get() > self.insurance_floor)
4041 }
4042
4043 pub fn deposit_fee_credits(&mut self, idx: u16, amount: u128, now_slot: u64) -> Result<()> {
4051 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4052 return Err(RiskError::Unauthorized);
4053 }
4054 if now_slot < self.current_slot {
4055 return Err(RiskError::Unauthorized);
4056 }
4057 let debt = fee_debt_u128_checked(self.accounts[idx as usize].fee_credits.get());
4059 let capped = amount.min(debt);
4060 if capped == 0 {
4061 self.current_slot = now_slot;
4062 return Ok(()); }
4064 if capped > i128::MAX as u128 {
4065 return Err(RiskError::Overflow);
4066 }
4067 let new_vault = self
4068 .vault
4069 .get()
4070 .checked_add(capped)
4071 .ok_or(RiskError::Overflow)?;
4072 if new_vault > MAX_VAULT_TVL {
4073 return Err(RiskError::Overflow);
4074 }
4075 let new_ins = self
4076 .insurance_fund
4077 .balance
4078 .get()
4079 .checked_add(capped)
4080 .ok_or(RiskError::Overflow)?;
4081 let new_credits = self.accounts[idx as usize]
4082 .fee_credits
4083 .checked_add(capped as i128)
4084 .ok_or(RiskError::Overflow)?;
4085 self.current_slot = now_slot;
4087 self.vault = U128::new(new_vault);
4088 self.insurance_fund.balance = U128::new(new_ins);
4089 self.accounts[idx as usize].fee_credits = new_credits;
4090 Ok(())
4091 }
4092
4093 #[cfg(any(test, feature = "test", kani))]
4094 test_visible! {
4095 fn add_fee_credits(&mut self, idx: u16, amount: u128) -> Result<()> {
4096 if idx as usize >= MAX_ACCOUNTS || !self.is_used(idx as usize) {
4097 return Err(RiskError::Unauthorized);
4098 }
4099 self.accounts[idx as usize].fee_credits = self.accounts[idx as usize]
4100 .fee_credits.saturating_add(amount as i128);
4101 Ok(())
4102 }
4103 }
4104
4105 test_visible! {
4110 fn recompute_aggregates(&mut self) {
4111 let mut c_tot = 0u128;
4112 let mut pnl_pos_tot = 0u128;
4113 self.for_each_used(|_idx, account| {
4114 c_tot = c_tot.saturating_add(account.capital.get());
4115 if account.pnl > 0 {
4116 pnl_pos_tot = pnl_pos_tot.saturating_add(account.pnl as u128);
4117 }
4118 });
4119 self.c_tot = U128::new(c_tot);
4120 self.pnl_pos_tot = pnl_pos_tot;
4121 }
4122 }
4123
4124 test_visible! {
4129 fn advance_slot(&mut self, slots: u64) {
4130 self.current_slot = self.current_slot.saturating_add(slots);
4131 }
4132 }
4133
4134 test_visible! {
4136 fn count_used(&self) -> u64 {
4137 let mut count = 0u64;
4138 self.for_each_used(|_, _| {
4139 count += 1;
4140 });
4141 count
4142 }
4143 }
4144}
4145
4146fn set_pending_reset(ctx: &mut InstructionContext, side: Side) {
4152 match side {
4153 Side::Long => ctx.pending_reset_long = true,
4154 Side::Short => ctx.pending_reset_short = true,
4155 }
4156}
4157
4158pub fn checked_u128_mul_i128(a: u128, b: i128) -> Result<i128> {
4161 if a == 0 || b == 0 {
4162 return Ok(0i128);
4163 }
4164 let negative = b < 0;
4165 let abs_b = if b == i128::MIN {
4166 return Err(RiskError::Overflow);
4167 } else {
4168 b.unsigned_abs()
4169 };
4170 let product = U256::from_u128(a)
4172 .checked_mul(U256::from_u128(abs_b))
4173 .ok_or(RiskError::Overflow)?;
4174 match product.try_into_u128() {
4177 Some(v) if v <= i128::MAX as u128 => {
4178 if negative {
4179 Ok(-(v as i128))
4180 } else {
4181 Ok(v as i128)
4182 }
4183 }
4184 _ => Err(RiskError::Overflow),
4185 }
4186}
4187
4188pub fn compute_trade_pnl(size_q: i128, price_diff: i128) -> Result<i128> {
4191 if size_q == 0 || price_diff == 0 {
4192 return Ok(0i128);
4193 }
4194
4195 let neg_size = size_q < 0;
4197 let neg_price = price_diff < 0;
4198 let result_negative = neg_size != neg_price;
4199
4200 let abs_size = size_q.unsigned_abs();
4201 let abs_price = price_diff.unsigned_abs();
4202
4203 let abs_size_u256 = U256::from_u128(abs_size);
4206 let abs_price_u256 = U256::from_u128(abs_price);
4207 let ps_u256 = U256::from_u128(POS_SCALE);
4208
4209 let (q, r) = mul_div_floor_u256_with_rem(abs_size_u256, abs_price_u256, ps_u256);
4211
4212 if result_negative {
4213 let mag = if !r.is_zero() {
4215 q.checked_add(U256::ONE).ok_or(RiskError::Overflow)?
4216 } else {
4217 q
4218 };
4219 match mag.try_into_u128() {
4222 Some(v) if v <= i128::MAX as u128 => Ok(-(v as i128)),
4223 _ => Err(RiskError::Overflow),
4224 }
4225 } else {
4226 match q.try_into_u128() {
4227 Some(v) if v <= i128::MAX as u128 => Ok(v as i128),
4228 _ => Err(RiskError::Overflow),
4229 }
4230 }
4231}
4232
4233#[cfg(kani)]
4238mod proofs {
4239 use super::*;
4240
4241 fn simple_params() -> RiskParams {
4246 RiskParams {
4247 warmup_period_slots: 0,
4248 maintenance_margin_bps: 500,
4249 initial_margin_bps: 1000,
4250 trading_fee_bps: 0,
4251 max_accounts: MAX_ACCOUNTS as u64,
4252 new_account_fee: U128::ZERO,
4253 maintenance_fee_per_slot: U128::ZERO,
4254 max_crank_staleness_slots: u64::MAX,
4255 liquidation_fee_bps: 0,
4256 liquidation_fee_cap: U128::ZERO,
4257 liquidation_buffer_bps: 50,
4258 min_liquidation_abs: U128::ZERO,
4259 min_initial_deposit: U128::new(3),
4260 min_nonzero_mm_req: 1,
4261 min_nonzero_im_req: 2,
4262 insurance_floor: U128::ZERO,
4263 }
4264 }
4265
4266 #[kani::proof]
4272 fn prove_set_capital_aggregate_consistency() {
4273 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4274
4275 let cap0: u128 = kani::any();
4277 let cap1: u128 = kani::any();
4278 kani::assume(cap0 >= 3 && cap0 <= 1_000_000_000);
4279 kani::assume(cap1 >= 3 && cap1 <= 1_000_000_000);
4280 eng.deposit(0, cap0, 1000, 100).unwrap();
4281 eng.deposit(1, cap1, 1000, 100).unwrap();
4282
4283 let sum = eng.accounts[0].capital.get() + eng.accounts[1].capital.get();
4285 assert!(eng.c_tot.get() == sum);
4286
4287 let new_cap: u128 = kani::any();
4289 kani::assume(new_cap <= 2_000_000_000);
4290 eng.set_capital(0, new_cap);
4291
4292 let sum_after = eng.accounts[0].capital.get() + eng.accounts[1].capital.get();
4293 assert!(eng.c_tot.get() == sum_after);
4294 }
4295
4296 #[kani::proof]
4298 #[kani::unwind(5)]
4299 fn prove_deposit_vault_monotonic_engine() {
4300 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4301
4302 let cap: u128 = kani::any();
4303 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4304 eng.deposit(0, cap, 1000, 100).unwrap();
4305
4306 let vault_before = eng.vault.get();
4307 let amount: u128 = kani::any();
4308 kani::assume(amount >= 3 && amount <= 1_000_000_000);
4309
4310 if eng.deposit(0, amount, 1000, 100).is_ok() {
4311 assert!(eng.vault.get() == vault_before + amount);
4312 }
4313 }
4314
4315 #[kani::proof]
4317 fn prove_settle_losses_correctness() {
4318 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4319
4320 let cap: u128 = kani::any();
4321 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4322 eng.deposit(0, cap, 1000, 100).unwrap();
4323
4324 let loss: u128 = kani::any();
4326 kani::assume(loss >= 1 && loss <= 1_000_000_000);
4327 let neg_pnl = -(loss as i128);
4328 eng.set_pnl(0, neg_pnl);
4329
4330 let cap_before = eng.accounts[0].capital.get();
4331 let pnl_before = eng.accounts[0].pnl;
4332
4333 eng.settle_losses(0);
4334
4335 assert!(eng.accounts[0].capital.get() <= cap_before);
4337 assert!(eng.accounts[0].pnl >= pnl_before);
4339 }
4340
4341 #[kani::proof]
4348 #[kani::unwind(5)]
4349 fn prove_conservation_after_deposit() {
4350 let oracle: u64 = kani::any();
4351 kani::assume(oracle > 0 && oracle <= 1_000_000);
4352 let mut eng = RiskEngine::new_with_market(simple_params(), 100, oracle);
4353
4354 let cap0: u128 = kani::any();
4356 let cap1: u128 = kani::any();
4357 kani::assume(cap0 >= 3 && cap0 <= 1_000_000_000);
4358 kani::assume(cap1 >= 3 && cap1 <= 1_000_000_000);
4359 eng.deposit(0, cap0, oracle, 100).unwrap();
4360 eng.deposit(1, cap1, oracle, 100).unwrap();
4361
4362 assert!(eng.check_conservation());
4364
4365 let amount: u128 = kani::any();
4367 kani::assume(amount >= 3 && amount <= 1_000_000_000);
4368
4369 if eng.deposit(0, amount, oracle, 100).is_ok() {
4370 assert!(eng.check_conservation());
4371 }
4372 }
4373
4374 #[kani::proof]
4376 #[kani::unwind(6)]
4377 fn prove_withdraw_vault_exact_decrease() {
4378 let oracle: u64 = kani::any();
4379 kani::assume(oracle > 0 && oracle <= 1_000_000);
4380 let slot: u64 = 100;
4381 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4382
4383 let cap: u128 = kani::any();
4384 kani::assume(cap >= 6 && cap <= 1_000_000_000);
4385 eng.deposit(0, cap, oracle, slot).unwrap();
4386
4387 let vault_before = eng.vault.get();
4388 let amount: u128 = kani::any();
4389 kani::assume(amount >= 3 && amount <= cap);
4390
4391 if eng.withdraw(0, amount, oracle, slot, 0).is_ok() {
4393 assert!(eng.vault.get() == vault_before - amount);
4394 }
4395 }
4396
4397 #[kani::proof]
4404 #[kani::unwind(6)]
4405 fn prove_conservation_after_withdraw() {
4406 let oracle: u64 = kani::any();
4407 kani::assume(oracle > 0 && oracle <= 1_000_000);
4408 let slot: u64 = 100;
4409 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4410
4411 let cap: u128 = kani::any();
4412 kani::assume(cap >= 6 && cap <= 1_000_000_000);
4413 eng.deposit(0, cap, oracle, slot).unwrap();
4414
4415 assert!(eng.check_conservation());
4416
4417 let withdraw_amt: u128 = kani::any();
4418 kani::assume(withdraw_amt >= 3 && withdraw_amt <= cap);
4419
4420 if eng.withdraw(0, withdraw_amt, oracle, slot, 0).is_ok() {
4421 assert!(eng.check_conservation());
4422 }
4423 }
4424
4425 #[kani::proof]
4438 fn prove_use_insurance_never_exceeds_balance() {
4439 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4440
4441 let cap: u128 = kani::any();
4443 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4444 eng.deposit(0, cap, 1000, 100).unwrap();
4445
4446 let ins_bal: u128 = kani::any();
4447 kani::assume(ins_bal <= 1_000_000_000);
4448 eng.insurance_fund.balance = U128::new(ins_bal);
4449
4450 let loss: u128 = kani::any();
4451 kani::assume(loss <= 2_000_000_000);
4452
4453 let ins_before = eng.insurance_fund.balance.get();
4454 let _remaining = eng.use_insurance_buffer(loss);
4455
4456 assert!(eng.insurance_fund.balance.get() <= ins_before);
4459 }
4460
4461 #[kani::proof]
4463 fn prove_absorb_loss_preserves_vault_and_capital() {
4464 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4465
4466 let cap: u128 = kani::any();
4467 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4468 eng.deposit(0, cap, 1000, 100).unwrap();
4469
4470 let ins_bal: u128 = kani::any();
4471 kani::assume(ins_bal <= 1_000_000_000);
4472 eng.insurance_fund.balance = U128::new(ins_bal);
4473
4474 let vault_before = eng.vault.get();
4475 let ctot_before = eng.c_tot.get();
4476
4477 let loss: u128 = kani::any();
4478 kani::assume(loss <= 2_000_000_000);
4479 eng.absorb_protocol_loss(loss);
4480
4481 assert!(eng.vault.get() == vault_before);
4483 assert!(eng.c_tot.get() == ctot_before);
4484 }
4485
4486 #[kani::proof]
4488 fn prove_haircut_ratio_bounded_engine() {
4489 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4490
4491 let cap: u128 = kani::any();
4493 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4494 eng.deposit(0, cap, 1000, 100).unwrap();
4495
4496 let matured: u128 = kani::any();
4498 kani::assume(matured <= 1_000_000_000);
4499 eng.pnl_matured_pos_tot = matured;
4500
4501 let (h_num, h_den) = eng.haircut_ratio();
4502
4503 if h_den > 0 {
4504 assert!(h_num <= h_den);
4505 } else {
4506 assert!(h_num == 1 && h_den == 1);
4508 }
4509 }
4510
4511 #[kani::proof]
4514 #[kani::unwind(5)]
4515 fn prove_conservation_after_two_deposits() {
4516 let oracle: u64 = kani::any();
4517 kani::assume(oracle > 0 && oracle <= 1_000_000);
4518 let mut eng = RiskEngine::new_with_market(simple_params(), 100, oracle);
4519
4520 let cap0: u128 = kani::any();
4521 let cap1: u128 = kani::any();
4522 let cap2: u128 = kani::any();
4523 kani::assume(cap0 >= 3 && cap0 <= 500_000_000);
4524 kani::assume(cap1 >= 3 && cap1 <= 500_000_000);
4525 kani::assume(cap2 >= 3 && cap2 <= 500_000_000);
4526
4527 if eng.deposit(0, cap0, oracle, 100).is_ok() {
4529 assert!(eng.check_conservation());
4530
4531 if eng.deposit(1, cap1, oracle, 100).is_ok() {
4533 assert!(eng.check_conservation());
4534
4535 if eng.deposit(0, cap2, oracle, 100).is_ok() {
4537 assert!(eng.check_conservation());
4538 }
4539 }
4540 }
4541 }
4542
4543 #[kani::proof]
4546 fn prove_settle_losses_preserves_conservation() {
4547 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4548
4549 let cap: u128 = kani::any();
4550 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4551 eng.deposit(0, cap, 1000, 100).unwrap();
4552
4553 assert!(eng.check_conservation());
4554
4555 let loss: u128 = kani::any();
4557 kani::assume(loss >= 1 && loss <= 1_000_000_000);
4558 let neg_pnl = -(loss as i128);
4559 eng.set_pnl(0, neg_pnl);
4560
4561 eng.settle_losses(0);
4565 assert!(eng.check_conservation());
4566 }
4567
4568 #[kani::proof]
4574 fn prove_accrue_market_to_noop() {
4575 let oracle: u64 = kani::any();
4576 kani::assume(oracle > 0 && oracle <= 1_000_000);
4577 let slot: u64 = 100;
4578 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4579
4580 let slot_before = eng.last_market_slot;
4581 let oracle_before = eng.last_oracle_price;
4582
4583 eng.accrue_market_to(slot, oracle).unwrap();
4584
4585 assert!(eng.last_market_slot == slot_before);
4586 assert!(eng.last_oracle_price == oracle_before);
4587 assert!(eng.current_slot == slot);
4588 }
4589
4590 #[kani::proof]
4592 fn prove_accrue_market_to_time_monotonicity() {
4593 let oracle: u64 = kani::any();
4594 kani::assume(oracle > 0 && oracle <= 1_000_000);
4595 let slot1: u64 = kani::any();
4596 let slot2: u64 = kani::any();
4597 kani::assume(slot1 >= 100 && slot1 <= 200);
4598 kani::assume(slot2 > slot1 && slot2 <= 300);
4599
4600 let mut eng = RiskEngine::new_with_market(simple_params(), slot1, oracle);
4601
4602 eng.accrue_market_to(slot2, oracle).unwrap();
4604 assert!(eng.last_market_slot == slot2);
4605 assert!(eng.current_slot == slot2);
4606
4607 assert!(eng.accrue_market_to(slot1, oracle).is_err());
4609 }
4610
4611 #[kani::proof]
4618 fn prove_charge_fee_to_insurance_bounded() {
4619 let mut eng = RiskEngine::new_with_market(simple_params(), 100, 1000);
4620
4621 let cap: u128 = kani::any();
4622 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4623 eng.deposit(0, cap, 1000, 100).unwrap();
4624
4625 let fee: u128 = kani::any();
4626 kani::assume(fee <= 1_000_000_000);
4627
4628 let cap_before = eng.accounts[0].capital.get();
4629 let ins_before = eng.insurance_fund.balance.get();
4630
4631 if eng.charge_fee_to_insurance(0, fee).is_ok() {
4632 let fee_paid = cap_before - eng.accounts[0].capital.get();
4633 assert!(fee_paid <= fee);
4635 assert!(fee_paid <= cap_before);
4636 assert!(eng.insurance_fund.balance.get() == ins_before + fee_paid);
4638 }
4639 }
4640
4641 #[kani::proof]
4644 #[kani::unwind(6)]
4645 fn prove_liquidate_flat_account_noop() {
4646 let oracle: u64 = kani::any();
4647 kani::assume(oracle > 0 && oracle <= 1_000_000);
4648 let slot: u64 = 100;
4649 let mut eng = RiskEngine::new_with_market(simple_params(), slot, oracle);
4650
4651 let cap: u128 = kani::any();
4652 kani::assume(cap >= 3 && cap <= 1_000_000_000);
4653 eng.deposit(0, cap, oracle, slot).unwrap();
4654
4655 let cap_before = eng.accounts[0].capital.get();
4657 let result = eng.liquidate_at_oracle(0, slot, oracle, LiquidationPolicy::FullClose, 0);
4658
4659 assert!(result == Ok(false));
4660 assert!(eng.accounts[0].capital.get() == cap_before);
4661 }
4662
4663 #[kani::proof]
4669 fn prove_checked_u128_mul_i128_sign() {
4670 let a: u128 = kani::any();
4671 let b: i128 = kani::any();
4672 kani::assume(a <= 1_000_000);
4673 kani::assume(b != i128::MIN);
4674 kani::assume(b.unsigned_abs() <= 1_000_000);
4675
4676 if let Ok(result) = checked_u128_mul_i128(a, b) {
4677 if a > 0 && b > 0 {
4678 assert!(result > 0);
4679 }
4680 if a > 0 && b < 0 {
4681 assert!(result < 0);
4682 }
4683 if a == 0 || b == 0 {
4684 assert!(result == 0);
4685 }
4686 }
4687 }
4688}