1#[cfg(feature = "borsh-schema")]
8use alloc::string::ToString;
9
10use crate::math::number::Number;
11use crate::math::wad::Wad;
12use crate::types::{Address, EscrowSettlement, TimestampNs};
13
14pub const MIN_WITHDRAWAL_ASSETS: u128 = 1_000;
17
18pub const MAX_QUEUE_LENGTH: u32 = crate::state::vault::MAX_PENDING as u32;
23
24pub const DEFAULT_COOLDOWN_NS: u64 = 24 * 60 * 60 * 1_000_000_000;
27
28#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, serde)]
33#[derive(Clone, PartialEq, Eq)]
34pub struct PendingWithdrawal {
35 pub owner: Address,
36 pub receiver: Address,
37 pub escrow_shares: u128,
38 pub expected_assets: u128,
39 pub requested_at_ns: TimestampNs,
40}
41
42impl PendingWithdrawal {
43 #[inline]
44 #[must_use]
45 pub fn new(
46 owner: Address,
47 receiver: Address,
48 escrow_shares: u128,
49 expected_assets: u128,
50 requested_at_ns: TimestampNs,
51 ) -> Self {
52 Self {
53 owner,
54 receiver,
55 escrow_shares,
56 expected_assets,
57 requested_at_ns,
58 }
59 }
60}
61
62#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
64#[derive(Clone, PartialEq, Eq)]
65pub struct WithdrawalResult {
66 pub assets_out: u128,
67 pub settlement: EscrowSettlement,
68}
69
70#[inline]
71#[must_use]
72pub fn compute_idle_settlement(
73 escrow_shares: u128,
74 expected_assets: u128,
75 available_assets: u128,
76) -> Option<WithdrawalResult> {
77 if expected_assets == 0 {
78 return Some(WithdrawalResult {
79 assets_out: 0,
80 settlement: if available_assets > 0 {
81 EscrowSettlement::burn_all(escrow_shares)
82 } else {
83 EscrowSettlement::refund_all(escrow_shares)
84 },
85 });
86 }
87
88 if available_assets >= expected_assets {
89 return Some(WithdrawalResult {
90 assets_out: expected_assets,
91 settlement: EscrowSettlement::burn_all(escrow_shares),
92 });
93 }
94
95 if available_assets == 0 {
96 return None;
97 }
98
99 let assets_out = available_assets;
100 Some(WithdrawalResult {
101 assets_out,
102 settlement: compute_settlement(escrow_shares, expected_assets, assets_out),
103 })
104}
105
106#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
108#[derive(Clone, PartialEq, Eq)]
109pub struct WithdrawalRequestStatus {
110 pub index: u32,
111 pub depth_assets: u128,
112 pub withdrawal: PendingWithdrawal,
113}
114
115#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
117#[derive(Clone, Default, PartialEq, Eq)]
118pub struct QueueStatus {
119 pub length: u32,
120 pub total_expected_assets: u128,
121 pub total_escrow_shares: u128,
122}
123
124#[inline]
125#[must_use]
126pub fn is_valid_withdrawal_amount(assets: u128) -> bool {
127 assets >= MIN_WITHDRAWAL_ASSETS
128}
129
130#[inline]
131#[must_use]
132pub fn can_enqueue(current_length: u32) -> bool {
133 current_length < MAX_QUEUE_LENGTH
134}
135
136#[inline]
137#[must_use]
138pub fn is_past_cooldown(
139 requested_at_ns: TimestampNs,
140 now_ns: TimestampNs,
141 cooldown_ns: u64,
142) -> bool {
143 now_ns >= requested_at_ns.saturating_add_u64(cooldown_ns)
144}
145
146#[inline]
155#[must_use]
156pub fn can_satisfy_withdrawal(withdrawal: &PendingWithdrawal, available_assets: u128) -> bool {
157 available_assets >= withdrawal.expected_assets
158}
159
160#[inline]
170#[must_use]
171pub fn can_partially_satisfy(withdrawal: &PendingWithdrawal, available_assets: u128) -> bool {
172 available_assets > 0
173 && available_assets < withdrawal.expected_assets
174 && available_assets >= MIN_WITHDRAWAL_ASSETS
175}
176
177#[must_use]
189pub fn count_satisfiable<'a, I>(withdrawals: I, available_assets: u128) -> (u32, u128)
190where
191 I: IntoIterator<Item = &'a PendingWithdrawal>,
192{
193 let mut count = 0u32;
194 let mut total_assets = 0u128;
195
196 for withdrawal in withdrawals {
197 let new_total = total_assets.saturating_add(withdrawal.expected_assets);
198 if new_total > available_assets {
199 break;
200 }
201 total_assets = new_total;
202 count = count.saturating_add(1);
203 }
204
205 (count, total_assets)
206}
207
208#[inline]
228#[must_use]
229pub fn compute_settlement(
230 escrow_shares: u128,
231 expected_assets: u128,
232 actual_assets: u128,
233) -> EscrowSettlement {
234 if escrow_shares == 0 {
235 return EscrowSettlement {
236 to_burn: 0,
237 refund: 0,
238 };
239 }
240
241 if actual_assets == 0 {
242 return EscrowSettlement::refund_all(escrow_shares);
244 }
245
246 if expected_assets == 0 {
247 return EscrowSettlement::refund_all(escrow_shares);
248 }
249
250 if actual_assets >= expected_assets {
251 return EscrowSettlement::burn_all(escrow_shares);
253 }
254
255 let shares_to_burn = Number::mul_div_ceil(
259 Number::from(escrow_shares),
260 Number::from(actual_assets),
261 Number::from(expected_assets),
262 )
263 .as_u128_trunc();
264
265 let shares_to_refund = escrow_shares.saturating_sub(shares_to_burn);
266
267 EscrowSettlement::partial(shares_to_burn, shares_to_refund)
268}
269
270#[inline]
283#[must_use]
284pub fn compute_settlement_by_price(
285 escrow_shares: u128,
286 share_price_wad: Wad,
287 original_share_price_wad: Wad,
288) -> EscrowSettlement {
289 if escrow_shares == 0 || original_share_price_wad.is_zero() {
290 return EscrowSettlement {
291 to_burn: 0,
292 refund: 0,
293 };
294 }
295
296 if share_price_wad.0 >= original_share_price_wad.0 {
298 return EscrowSettlement::burn_all(escrow_shares);
299 }
300
301 let shares_to_burn = Number::mul_div_ceil(
305 Number::from(escrow_shares),
306 share_price_wad.0,
307 original_share_price_wad.0,
308 )
309 .as_u128_trunc();
310
311 let shares_to_refund = escrow_shares.saturating_sub(shares_to_burn);
312
313 EscrowSettlement::partial(shares_to_burn, shares_to_refund)
314}
315
316#[must_use]
325pub fn compute_full_withdrawal(
326 withdrawal: &PendingWithdrawal,
327 available_assets: u128,
328) -> Option<WithdrawalResult> {
329 compute_idle_settlement(
330 withdrawal.escrow_shares,
331 withdrawal.expected_assets,
332 available_assets,
333 )
334 .filter(|result| result.assets_out == withdrawal.expected_assets)
335}
336
337#[must_use]
346pub fn compute_partial_withdrawal(
347 withdrawal: &PendingWithdrawal,
348 available_assets: u128,
349) -> WithdrawalResult {
350 let actual_assets = available_assets.min(withdrawal.expected_assets);
351
352 let settlement = compute_settlement(
353 withdrawal.escrow_shares,
354 withdrawal.expected_assets,
355 actual_assets,
356 );
357
358 WithdrawalResult {
359 assets_out: actual_assets,
360 settlement,
361 }
362}
363
364#[must_use]
374pub fn compute_queue_status<'a, I>(withdrawals: I) -> QueueStatus
375where
376 I: IntoIterator<Item = &'a PendingWithdrawal>,
377{
378 let mut status = QueueStatus::default();
379
380 for withdrawal in withdrawals {
381 status.length = status.length.saturating_add(1);
382 status.total_expected_assets = status
383 .total_expected_assets
384 .saturating_add(withdrawal.expected_assets);
385 status.total_escrow_shares = status
386 .total_escrow_shares
387 .saturating_add(withdrawal.escrow_shares);
388 }
389
390 status
391}
392
393#[must_use]
402pub fn find_request_status<'a, I>(
403 withdrawals: I,
404 owner: &Address,
405) -> Option<WithdrawalRequestStatus>
406where
407 I: IntoIterator<Item = &'a PendingWithdrawal>,
408{
409 let mut index = 0u32;
410 let mut depth_assets = 0u128;
411
412 for withdrawal in withdrawals {
413 if &withdrawal.owner == owner {
414 return Some(WithdrawalRequestStatus {
415 index,
416 depth_assets,
417 withdrawal: withdrawal.clone(),
418 });
419 }
420 depth_assets = depth_assets.saturating_add(withdrawal.expected_assets);
421 index = index.saturating_add(1);
422 }
423
424 None
425}
426
427use alloc::vec::Vec;
430
431pub use crate::state::vault::MAX_PENDING;
432
433#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, serde)]
434#[derive(Clone, PartialEq, Eq, Default)]
435pub struct PendingWithdrawals {
436 entries: Vec<PendingWithdrawalEntry>,
437}
438
439#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, serde)]
440#[derive(Clone, PartialEq, Eq)]
441struct PendingWithdrawalEntry {
442 id: u64,
443 withdrawal: PendingWithdrawal,
444}
445
446impl PendingWithdrawals {
447 #[inline]
448 #[must_use]
449 pub fn new() -> Self {
450 Self {
451 entries: Vec::new(),
452 }
453 }
454
455 #[inline]
456 #[must_use]
457 pub fn len(&self) -> usize {
458 self.entries.len()
459 }
460
461 #[inline]
462 #[must_use]
463 pub fn is_empty(&self) -> bool {
464 self.entries.is_empty()
465 }
466
467 #[inline]
468 fn locate(&self, id: u64) -> Result<usize, usize> {
469 self.entries.binary_search_by(|entry| entry.id.cmp(&id))
470 }
471
472 pub fn insert(&mut self, id: u64, withdrawal: PendingWithdrawal) -> Option<PendingWithdrawal> {
473 match self.locate(id) {
474 Ok(index) => {
475 let old = core::mem::replace(&mut self.entries[index].withdrawal, withdrawal);
476 Some(old)
477 }
478 Err(index) => {
479 self.entries
480 .insert(index, PendingWithdrawalEntry { id, withdrawal });
481 None
482 }
483 }
484 }
485
486 pub fn remove(&mut self, id: &u64) -> Option<PendingWithdrawal> {
487 self.locate(*id)
488 .ok()
489 .map(|index| self.entries.remove(index).withdrawal)
490 }
491
492 #[inline]
493 #[must_use]
494 pub fn get(&self, id: &u64) -> Option<&PendingWithdrawal> {
495 self.locate(*id)
496 .ok()
497 .map(|index| &self.entries[index].withdrawal)
498 }
499
500 #[inline]
501 #[must_use]
502 pub fn get_mut(&mut self, id: &u64) -> Option<&mut PendingWithdrawal> {
503 self.locate(*id)
504 .ok()
505 .map(|index| &mut self.entries[index].withdrawal)
506 }
507
508 #[inline]
509 #[must_use]
510 pub fn contains_key(&self, id: &u64) -> bool {
511 self.locate(*id).is_ok()
512 }
513
514 #[inline]
515 pub fn iter(&self) -> impl Iterator<Item = (&u64, &PendingWithdrawal)> {
516 self.entries
517 .iter()
518 .map(|entry| (&entry.id, &entry.withdrawal))
519 }
520
521 #[inline]
522 pub fn values(&self) -> impl Iterator<Item = &PendingWithdrawal> {
523 self.entries.iter().map(|entry| &entry.withdrawal)
524 }
525
526 #[inline]
527 pub fn keys(&self) -> impl Iterator<Item = &u64> {
528 self.entries.iter().map(|entry| &entry.id)
529 }
530}
531
532impl FromIterator<(u64, PendingWithdrawal)> for PendingWithdrawals {
533 fn from_iter<T: IntoIterator<Item = (u64, PendingWithdrawal)>>(iter: T) -> Self {
534 let mut pending = Self::new();
535 for (id, withdrawal) in iter {
536 assert!(
537 pending.insert(id, withdrawal).is_none(),
538 "duplicate pending withdrawal id: {id}"
539 );
540 }
541 pending
542 }
543}
544
545#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, serde)]
560#[derive(Clone, PartialEq, Eq)]
561pub struct WithdrawQueue {
562 pending_withdrawals: PendingWithdrawals,
564 pub next_withdraw_to_execute: u64,
566 pub next_pending_withdrawal_id: u64,
568 cached_total_escrow: u128,
571 cached_total_expected: u128,
574}
575
576impl Default for WithdrawQueue {
577 fn default() -> Self {
578 Self::new()
579 }
580}
581
582fn compute_pending_totals<'a>(iter: impl Iterator<Item = &'a PendingWithdrawal>) -> (u128, u128) {
584 iter.fold((0u128, 0u128), |(esc, exp), w| {
585 (
586 esc.saturating_add(w.escrow_shares),
587 exp.saturating_add(w.expected_assets),
588 )
589 })
590}
591
592impl WithdrawQueue {
593 #[inline]
595 #[must_use]
596 pub fn new() -> Self {
597 Self {
598 pending_withdrawals: PendingWithdrawals::new(),
599 next_withdraw_to_execute: 0,
600 next_pending_withdrawal_id: 0,
601 cached_total_escrow: 0,
602 cached_total_expected: 0,
603 }
604 }
605
606 #[must_use]
608 pub fn with_state<I>(
609 pending_withdrawals: I,
610 next_withdraw_to_execute: u64,
611 next_pending_withdrawal_id: u64,
612 ) -> Self
613 where
614 I: IntoIterator<Item = (u64, PendingWithdrawal)>,
615 {
616 let pending_withdrawals: PendingWithdrawals = pending_withdrawals.into_iter().collect();
617 let (cached_total_escrow, cached_total_expected) =
618 compute_pending_totals(pending_withdrawals.values());
619 Self {
620 pending_withdrawals,
621 next_withdraw_to_execute,
622 next_pending_withdrawal_id,
623 cached_total_escrow,
624 cached_total_expected,
625 }
626 }
627
628 #[inline]
630 #[must_use]
631 pub fn len(&self) -> usize {
632 self.pending_withdrawals.len()
633 }
634
635 #[inline]
636 #[must_use]
637 pub fn pending_withdrawals(&self) -> &PendingWithdrawals {
638 &self.pending_withdrawals
639 }
640
641 #[inline]
643 #[must_use]
644 pub fn is_empty(&self) -> bool {
645 self.pending_withdrawals.is_empty()
646 }
647
648 #[inline]
656 #[must_use]
657 pub fn can_enqueue(&self, max_pending: u32) -> bool {
658 self.pending_withdrawals.len() < (max_pending as usize).min(MAX_PENDING)
659 }
660
661 pub fn enqueue(
666 &mut self,
667 owner: Address,
668 receiver: Address,
669 escrow_shares: u128,
670 expected_assets: u128,
671 requested_at_ns: TimestampNs,
672 max_pending: u32,
673 ) -> Result<u64, QueueError> {
674 let withdrawal = PendingWithdrawal::new(
675 owner,
676 receiver,
677 escrow_shares,
678 expected_assets,
679 requested_at_ns,
680 );
681 self.enqueue_withdrawal(withdrawal, max_pending)
682 }
683
684 pub fn enqueue_withdrawal(
691 &mut self,
692 withdrawal: PendingWithdrawal,
693 max_pending: u32,
694 ) -> Result<u64, QueueError> {
695 if !self.can_enqueue(max_pending) {
696 return Err(QueueError::QueueFull {
697 current: self.pending_withdrawals.len() as u32,
698 max: max_pending,
699 });
700 }
701
702 let id = self.next_pending_withdrawal_id;
703 let next_id = id
704 .checked_add(1)
705 .ok_or_else(|| QueueError::InvariantViolation {
706 message: alloc::string::String::from("next_pending_withdrawal_id overflow"),
707 })?;
708
709 let new_escrow = self
711 .cached_total_escrow
712 .checked_add(withdrawal.escrow_shares)
713 .ok_or(QueueError::CacheOverflow)?;
714 let new_expected = self
715 .cached_total_expected
716 .checked_add(withdrawal.expected_assets)
717 .ok_or(QueueError::CacheOverflow)?;
718
719 self.pending_withdrawals.insert(id, withdrawal);
720 self.next_pending_withdrawal_id = next_id;
721
722 self.cached_total_escrow = new_escrow;
724 self.cached_total_expected = new_expected;
725
726 Ok(id)
727 }
728
729 #[inline]
734 #[must_use]
735 pub fn head(&self) -> Option<(u64, &PendingWithdrawal)> {
736 self.pending_withdrawals
737 .get(&self.next_withdraw_to_execute)
738 .map(|w| (self.next_withdraw_to_execute, w))
739 }
740
741 pub fn dequeue(&mut self) -> Option<(u64, PendingWithdrawal)> {
750 if self.is_empty() {
751 return None;
752 }
753
754 let head_id = self.next_withdraw_to_execute;
755 let withdrawal = self.pending_withdrawals.remove(&head_id)?;
756
757 self.cached_total_escrow = self
758 .cached_total_escrow
759 .checked_sub(withdrawal.escrow_shares)
760 .expect("dequeue: cached_total_escrow underflow - queue cache corrupt");
761 self.cached_total_expected = self
762 .cached_total_expected
763 .checked_sub(withdrawal.expected_assets)
764 .expect("dequeue: cached_total_expected underflow - queue cache corrupt");
765
766 self.next_withdraw_to_execute = self
768 .pending_withdrawals
769 .keys()
770 .next()
771 .copied()
772 .unwrap_or(self.next_pending_withdrawal_id);
773
774 Some((head_id, withdrawal))
775 }
776
777 #[inline]
785 #[must_use]
786 pub fn get(&self, id: u64) -> Option<&PendingWithdrawal> {
787 self.pending_withdrawals.get(&id)
788 }
789
790 #[inline]
798 #[must_use]
799 pub fn contains(&self, id: u64) -> bool {
800 self.pending_withdrawals.contains_key(&id)
801 }
802
803 pub fn iter(&self) -> impl Iterator<Item = (u64, &PendingWithdrawal)> {
808 self.pending_withdrawals.iter().map(|(k, v)| (*k, v))
809 }
810
811 #[must_use]
821 pub fn check_invariants(&self) -> bool {
822 if self.next_withdraw_to_execute > self.next_pending_withdrawal_id {
824 return false;
825 }
826
827 if !self.is_empty()
829 && !self
830 .pending_withdrawals
831 .contains_key(&self.next_withdraw_to_execute)
832 {
833 return false;
834 }
835
836 let (computed_escrow, computed_expected) =
838 compute_pending_totals(self.pending_withdrawals.values());
839
840 if self.cached_total_escrow != computed_escrow {
841 return false;
842 }
843 if self.cached_total_expected != computed_expected {
844 return false;
845 }
846
847 true
848 }
849
850 #[must_use]
858 pub fn check_invariants_with_max(&self, max_pending: u32) -> bool {
859 if !self.check_invariants() {
861 return false;
862 }
863
864 let len = self.pending_withdrawals.len();
866 if len > (max_pending as usize) || (max_pending as usize) > MAX_PENDING {
867 return false;
868 }
869
870 true
871 }
872
873 #[inline]
878 #[must_use]
879 pub fn status(&self) -> QueueStatus {
880 QueueStatus {
881 length: self.pending_withdrawals.len() as u32,
882 total_expected_assets: self.cached_total_expected,
883 total_escrow_shares: self.cached_total_escrow,
884 }
885 }
886
887 #[inline]
894 #[must_use]
895 pub fn total_escrow_shares(&self) -> u128 {
896 self.cached_total_escrow
897 }
898
899 #[inline]
906 #[must_use]
907 pub fn total_expected_assets(&self) -> u128 {
908 self.cached_total_expected
909 }
910}
911
912#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
914#[derive(Clone, PartialEq, Eq)]
915pub enum QueueError {
916 QueueFull { current: u32, max: u32 },
918 WithdrawalNotFound { id: u64 },
920 QueueEmpty,
922 InvariantViolation { message: alloc::string::String },
924 CacheOverflow,
926}
927
928#[cfg(test)]
929mod tests;