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)]
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)]
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)]
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 settle_proportional_raw(entry.shares, entry.expected_assets, actual_assets)
91}
92
93#[must_use]
95pub fn settle_proportional_raw(
96 shares: u128,
97 expected_assets: u128,
98 actual_assets: u128,
99) -> EscrowSettlement {
100 if shares == 0 {
101 return EscrowSettlement {
102 to_burn: 0,
103 refund: 0,
104 };
105 }
106
107 if actual_assets == 0 {
108 return EscrowSettlement::refund_all(shares);
109 }
110
111 if expected_assets == 0 {
112 return EscrowSettlement::refund_all(shares);
113 }
114
115 if actual_assets >= expected_assets {
116 return EscrowSettlement::burn_all(shares);
117 }
118
119 let to_burn = Number::mul_div_ceil(
122 Number::from(shares),
123 Number::from(actual_assets),
124 Number::from(expected_assets),
125 )
126 .as_u128_trunc();
127
128 let refund = shares.saturating_sub(to_burn);
129
130 EscrowSettlement::partial(to_burn, refund)
131}
132
133#[inline]
135#[must_use]
136pub fn can_apply_settlement(entry: &EscrowEntry, settlement: &EscrowSettlement) -> bool {
137 settlement
138 .to_burn
139 .checked_add(settlement.refund)
140 .is_some_and(|total| total <= entry.shares)
141}
142
143#[inline]
145#[must_use]
146pub fn is_stale(entry: &EscrowEntry, now_ns: TimestampNs, max_age_ns: u64) -> bool {
147 now_ns > entry.created_at_ns.saturating_add_u64(max_age_ns)
148}
149
150#[must_use]
152pub fn compute_escrow_stats<'a, I>(entries: I) -> EscrowStats
153where
154 I: IntoIterator<Item = &'a EscrowEntry>,
155{
156 let mut stats = EscrowStats::default();
157
158 for entry in entries {
159 stats.count = stats.count.saturating_add(1);
160 stats.total_shares = stats.total_shares.saturating_add(entry.shares);
161 stats.total_expected_assets = stats
162 .total_expected_assets
163 .saturating_add(entry.expected_assets);
164 }
165
166 stats
167}
168
169#[must_use]
171pub fn find_by_owner<'a, I>(entries: I, owner: &Address) -> Option<&'a EscrowEntry>
172where
173 I: IntoIterator<Item = &'a EscrowEntry>,
174{
175 entries.into_iter().find(|e| &e.owner == owner)
176}
177
178#[must_use]
180pub fn total_burn<'a, I>(settlements: I) -> u128
181where
182 I: IntoIterator<Item = &'a EscrowSettlement>,
183{
184 settlements
185 .into_iter()
186 .map(|s| s.to_burn)
187 .fold(0u128, |acc, x| acc.saturating_add(x))
188}
189
190#[must_use]
192pub fn total_refund<'a, I>(settlements: I) -> u128
193where
194 I: IntoIterator<Item = &'a EscrowSettlement>,
195{
196 settlements
197 .into_iter()
198 .map(|s| s.refund)
199 .fold(0u128, |acc, x| acc.saturating_add(x))
200}
201
202#[cfg(test)]
203mod tests;