templar_vault_kernel/state/escrow/
mod.rs1use crate::math::number::Number;
8use crate::types::{Address, TimestampNs};
9
10pub use crate::types::EscrowSettlement;
11
12#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
16#[derive(Clone, PartialEq, Eq)]
17pub struct EscrowEntry {
18 pub owner: Address,
19 pub shares: u128,
20 pub created_at_ns: TimestampNs,
21 pub expected_assets: u128,
22}
23
24impl EscrowEntry {
25 #[inline]
26 #[must_use]
27 pub fn new(
28 owner: Address,
29 shares: u128,
30 created_at_ns: TimestampNs,
31 expected_assets: u128,
32 ) -> Self {
33 Self {
34 owner,
35 shares,
36 created_at_ns,
37 expected_assets,
38 }
39 }
40
41 #[inline]
42 #[must_use]
43 pub fn is_empty(&self) -> bool {
44 self.shares == 0
45 }
46}
47
48#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
50#[derive(Clone, PartialEq, Eq)]
51pub struct SettlementResult {
52 pub burned: u128,
53 pub refunded: u128,
54 pub remaining: u128,
55}
56
57#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
59#[derive(Clone, Default, PartialEq, Eq)]
60pub struct EscrowStats {
61 pub count: u32,
62 pub total_shares: u128,
63 pub total_expected_assets: u128,
64}
65
66#[must_use]
68pub fn apply_settlement(
69 entry: &EscrowEntry,
70 settlement: &EscrowSettlement,
71) -> Option<SettlementResult> {
72 let total_settled = settlement.to_burn.checked_add(settlement.refund)?;
73
74 if total_settled > entry.shares {
75 return None;
76 }
77
78 let remaining = entry.shares.saturating_sub(total_settled);
79
80 Some(SettlementResult {
81 burned: settlement.to_burn,
82 refunded: settlement.refund,
83 remaining,
84 })
85}
86
87#[must_use]
89pub fn settle_proportional(entry: &EscrowEntry, actual_assets: u128) -> EscrowSettlement {
90 if entry.shares == 0 {
91 return EscrowSettlement {
92 to_burn: 0,
93 refund: 0,
94 };
95 }
96
97 if actual_assets == 0 {
98 return EscrowSettlement::refund_all(entry.shares);
99 }
100
101 if entry.expected_assets == 0 {
102 return EscrowSettlement::refund_all(entry.shares);
103 }
104
105 if actual_assets >= entry.expected_assets {
106 return EscrowSettlement::burn_all(entry.shares);
107 }
108
109 let to_burn = Number::mul_div_ceil(
112 Number::from(entry.shares),
113 Number::from(actual_assets),
114 Number::from(entry.expected_assets),
115 )
116 .as_u128_trunc();
117
118 let refund = entry.shares.saturating_sub(to_burn);
119
120 EscrowSettlement::partial(to_burn, refund)
121}
122
123#[inline]
125#[must_use]
126pub fn can_apply_settlement(entry: &EscrowEntry, settlement: &EscrowSettlement) -> bool {
127 settlement
128 .to_burn
129 .checked_add(settlement.refund)
130 .is_some_and(|total| total <= entry.shares)
131}
132
133#[inline]
135#[must_use]
136pub fn is_stale(entry: &EscrowEntry, now_ns: TimestampNs, max_age_ns: u64) -> bool {
137 now_ns > entry.created_at_ns.saturating_add_u64(max_age_ns)
138}
139
140#[must_use]
142pub fn compute_escrow_stats<'a, I>(entries: I) -> EscrowStats
143where
144 I: IntoIterator<Item = &'a EscrowEntry>,
145{
146 let mut stats = EscrowStats::default();
147
148 for entry in entries {
149 stats.count = stats.count.saturating_add(1);
150 stats.total_shares = stats.total_shares.saturating_add(entry.shares);
151 stats.total_expected_assets = stats
152 .total_expected_assets
153 .saturating_add(entry.expected_assets);
154 }
155
156 stats
157}
158
159#[must_use]
161pub fn find_by_owner<'a, I>(entries: I, owner: &Address) -> Option<&'a EscrowEntry>
162where
163 I: IntoIterator<Item = &'a EscrowEntry>,
164{
165 entries.into_iter().find(|e| &e.owner == owner)
166}
167
168#[must_use]
170pub fn total_burn<'a, I>(settlements: I) -> u128
171where
172 I: IntoIterator<Item = &'a EscrowSettlement>,
173{
174 settlements
175 .into_iter()
176 .map(|s| s.to_burn)
177 .fold(0u128, |acc, x| acc.saturating_add(x))
178}
179
180#[must_use]
182pub fn total_refund<'a, I>(settlements: I) -> u128
183where
184 I: IntoIterator<Item = &'a EscrowSettlement>,
185{
186 settlements
187 .into_iter()
188 .map(|s| s.refund)
189 .fold(0u128, |acc, x| acc.saturating_add(x))
190}
191
192#[cfg(test)]
193mod tests;