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 = 60 * 60 * 1_000_000_000;
27
28#[templar_vault_macros::vault_derive(borsh, borsh_schema, 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)]
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)]
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)]
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, 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, 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 #[inline]
532 #[must_use]
533 pub fn from_sorted_entries(entries: Vec<(u64, PendingWithdrawal)>) -> Self {
534 let mut last_id = None;
535 for (id, _) in &entries {
536 if last_id.is_some_and(|last| last >= *id) {
537 crate::abort!("pending withdrawal entries must be sorted by unique id");
538 }
539 last_id = Some(*id);
540 }
541
542 Self {
543 entries: entries
544 .into_iter()
545 .map(|(id, withdrawal)| PendingWithdrawalEntry { id, withdrawal })
546 .collect(),
547 }
548 }
549}
550
551impl FromIterator<(u64, PendingWithdrawal)> for PendingWithdrawals {
552 fn from_iter<T: IntoIterator<Item = (u64, PendingWithdrawal)>>(iter: T) -> Self {
553 let mut pending = Self::new();
554 for (id, withdrawal) in iter {
555 assert!(
556 pending.insert(id, withdrawal).is_none(),
557 "duplicate pending withdrawal id: {id}"
558 );
559 }
560 pending
561 }
562}
563
564#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
579#[derive(Clone, PartialEq, Eq)]
580pub struct WithdrawQueue {
581 pending_withdrawals: PendingWithdrawals,
583 pub next_withdraw_to_execute: u64,
585 pub next_pending_withdrawal_id: u64,
587 cached_total_escrow: u128,
590 cached_total_expected: u128,
593}
594
595impl Default for WithdrawQueue {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601fn compute_pending_totals<'a>(iter: impl Iterator<Item = &'a PendingWithdrawal>) -> (u128, u128) {
603 iter.fold((0u128, 0u128), |(esc, exp), w| {
604 (
605 esc.saturating_add(w.escrow_shares),
606 exp.saturating_add(w.expected_assets),
607 )
608 })
609}
610
611impl WithdrawQueue {
612 #[inline]
614 #[must_use]
615 pub fn new() -> Self {
616 Self {
617 pending_withdrawals: PendingWithdrawals::new(),
618 next_withdraw_to_execute: 0,
619 next_pending_withdrawal_id: 0,
620 cached_total_escrow: 0,
621 cached_total_expected: 0,
622 }
623 }
624
625 #[must_use]
627 pub fn with_state<I>(
628 pending_withdrawals: I,
629 next_withdraw_to_execute: u64,
630 next_pending_withdrawal_id: u64,
631 ) -> Self
632 where
633 I: IntoIterator<Item = (u64, PendingWithdrawal)>,
634 {
635 let pending_withdrawals =
636 PendingWithdrawals::from_sorted_entries(pending_withdrawals.into_iter().collect());
637 let (cached_total_escrow, cached_total_expected) =
638 compute_pending_totals(pending_withdrawals.values());
639 Self {
640 pending_withdrawals,
641 next_withdraw_to_execute,
642 next_pending_withdrawal_id,
643 cached_total_escrow,
644 cached_total_expected,
645 }
646 }
647
648 #[inline]
650 #[must_use]
651 pub fn len(&self) -> usize {
652 self.pending_withdrawals.len()
653 }
654
655 #[inline]
656 #[must_use]
657 pub fn pending_withdrawals(&self) -> &PendingWithdrawals {
658 &self.pending_withdrawals
659 }
660
661 #[inline]
663 #[must_use]
664 pub fn is_empty(&self) -> bool {
665 self.pending_withdrawals.is_empty()
666 }
667
668 #[inline]
676 #[must_use]
677 pub fn can_enqueue(&self, max_pending: u32) -> bool {
678 self.pending_withdrawals.len() < (max_pending as usize).min(MAX_PENDING)
679 }
680
681 pub fn enqueue(
686 &mut self,
687 owner: Address,
688 receiver: Address,
689 escrow_shares: u128,
690 expected_assets: u128,
691 requested_at_ns: TimestampNs,
692 max_pending: u32,
693 ) -> Result<u64, QueueError> {
694 let withdrawal = PendingWithdrawal::new(
695 owner,
696 receiver,
697 escrow_shares,
698 expected_assets,
699 requested_at_ns,
700 );
701 self.enqueue_withdrawal(withdrawal, max_pending)
702 }
703
704 pub fn enqueue_withdrawal(
711 &mut self,
712 withdrawal: PendingWithdrawal,
713 max_pending: u32,
714 ) -> Result<u64, QueueError> {
715 if !self.can_enqueue(max_pending) {
716 return Err(QueueError::QueueFull {
717 current: self.pending_withdrawals.len() as u32,
718 max: max_pending,
719 });
720 }
721
722 let id = self.next_pending_withdrawal_id;
723 let next_id = id
724 .checked_add(1)
725 .ok_or_else(|| QueueError::InvariantViolation {
726 message: alloc::string::String::from("next_pending_withdrawal_id overflow"),
727 })?;
728
729 let new_escrow = self
731 .cached_total_escrow
732 .checked_add(withdrawal.escrow_shares)
733 .ok_or(QueueError::CacheOverflow)?;
734 let new_expected = self
735 .cached_total_expected
736 .checked_add(withdrawal.expected_assets)
737 .ok_or(QueueError::CacheOverflow)?;
738
739 self.pending_withdrawals.insert(id, withdrawal);
740 self.next_pending_withdrawal_id = next_id;
741
742 self.cached_total_escrow = new_escrow;
744 self.cached_total_expected = new_expected;
745
746 Ok(id)
747 }
748
749 #[inline]
754 #[must_use]
755 pub fn head(&self) -> Option<(u64, &PendingWithdrawal)> {
756 self.pending_withdrawals
757 .get(&self.next_withdraw_to_execute)
758 .map(|w| (self.next_withdraw_to_execute, w))
759 }
760
761 pub fn dequeue(&mut self) -> Option<(u64, PendingWithdrawal)> {
770 if self.is_empty() {
771 return None;
772 }
773
774 let head_id = self.next_withdraw_to_execute;
775 let withdrawal = self.pending_withdrawals.remove(&head_id)?;
776
777 self.cached_total_escrow = crate::unwrap_abort!(
778 self.cached_total_escrow
779 .checked_sub(withdrawal.escrow_shares),
780 crate::abort::OVERFLOW,
781 );
782 self.cached_total_expected = crate::unwrap_abort!(
783 self.cached_total_expected
784 .checked_sub(withdrawal.expected_assets),
785 crate::abort::OVERFLOW,
786 );
787
788 self.next_withdraw_to_execute = self
790 .pending_withdrawals
791 .keys()
792 .next()
793 .copied()
794 .unwrap_or(self.next_pending_withdrawal_id);
795
796 Some((head_id, withdrawal))
797 }
798
799 #[inline]
807 #[must_use]
808 pub fn get(&self, id: u64) -> Option<&PendingWithdrawal> {
809 self.pending_withdrawals.get(&id)
810 }
811
812 #[inline]
820 #[must_use]
821 pub fn contains(&self, id: u64) -> bool {
822 self.pending_withdrawals.contains_key(&id)
823 }
824
825 pub fn iter(&self) -> impl Iterator<Item = (u64, &PendingWithdrawal)> {
830 self.pending_withdrawals.iter().map(|(k, v)| (*k, v))
831 }
832
833 #[must_use]
843 pub fn check_invariants(&self) -> bool {
844 if self.next_withdraw_to_execute > self.next_pending_withdrawal_id {
846 return false;
847 }
848
849 if !self.is_empty()
851 && !self
852 .pending_withdrawals
853 .contains_key(&self.next_withdraw_to_execute)
854 {
855 return false;
856 }
857
858 let (computed_escrow, computed_expected) =
860 compute_pending_totals(self.pending_withdrawals.values());
861
862 if self.cached_total_escrow != computed_escrow {
863 return false;
864 }
865 if self.cached_total_expected != computed_expected {
866 return false;
867 }
868
869 true
870 }
871
872 #[must_use]
880 pub fn check_invariants_with_max(&self, max_pending: u32) -> bool {
881 if !self.check_invariants() {
883 return false;
884 }
885
886 let len = self.pending_withdrawals.len();
888 if len > (max_pending as usize) || (max_pending as usize) > MAX_PENDING {
889 return false;
890 }
891
892 true
893 }
894
895 #[inline]
900 #[must_use]
901 pub fn status(&self) -> QueueStatus {
902 QueueStatus {
903 length: self.pending_withdrawals.len() as u32,
904 total_expected_assets: self.cached_total_expected,
905 total_escrow_shares: self.cached_total_escrow,
906 }
907 }
908
909 #[inline]
916 #[must_use]
917 pub fn total_escrow_shares(&self) -> u128 {
918 self.cached_total_escrow
919 }
920
921 #[inline]
928 #[must_use]
929 pub fn total_expected_assets(&self) -> u128 {
930 self.cached_total_expected
931 }
932}
933
934#[templar_vault_macros::vault_derive(borsh, serde)]
936#[derive(Clone, PartialEq, Eq)]
937pub enum QueueError {
938 QueueFull { current: u32, max: u32 },
940 WithdrawalNotFound { id: u64 },
942 QueueEmpty,
944 InvariantViolation { message: alloc::string::String },
946 CacheOverflow,
948}
949
950#[cfg(test)]
951mod tests;