1use alloc::vec::Vec;
9
10use templar_vault_kernel::math::wad::{Wad, MAX_MANAGEMENT_FEE_WAD, MAX_PERFORMANCE_FEE_WAD};
11use templar_vault_kernel::types::{DurationNs, TimestampNs};
12use templar_vault_kernel::TimeGate;
13
14#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
16#[derive(Clone, PartialEq, Eq)]
17pub struct PendingValue<T> {
18 pub value: T,
19 pub ready_at_ns: TimestampNs,
20}
21
22impl<T> PendingValue<T> {
23 #[must_use]
25 pub fn is_mature(&self, now_ns: TimestampNs) -> bool {
26 TimeGate::from_ready_at(self.ready_at_ns).is_ready(now_ns)
27 }
28}
29
30#[templar_vault_macros::vault_derive]
31#[derive(Clone, PartialEq, Eq)]
32pub enum TakePending<T> {
33 Missing,
34 Pending { ready_at_ns: TimestampNs },
35 Ready(T),
36}
37
38pub struct ScheduledPending<T> {
39 pub ready_at_ns: TimestampNs,
40 pub replaced: Vec<T>,
41}
42
43#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
44#[derive(Clone, PartialEq, Eq)]
45pub struct PendingActions<T> {
46 entries: Vec<PendingValue<T>>,
47}
48
49impl<T> Default for PendingActions<T> {
50 fn default() -> Self {
51 Self {
52 entries: Vec::new(),
53 }
54 }
55}
56
57impl<T> PendingActions<T> {
58 #[must_use]
59 pub fn len(&self) -> usize {
60 self.entries.len()
61 }
62
63 #[must_use]
64 pub fn is_empty(&self) -> bool {
65 self.entries.is_empty()
66 }
67
68 pub fn iter(&self) -> impl Iterator<Item = &PendingValue<T>> {
69 self.entries.iter()
70 }
71
72 #[must_use]
73 pub fn back(&self) -> Option<&PendingValue<T>> {
74 self.entries.last()
75 }
76
77 pub fn schedule(
78 &mut self,
79 value: T,
80 now_ns: TimestampNs,
81 timelock_ns: DurationNs,
82 ) -> TimestampNs {
83 let ready_at_ns = TimeGate::schedule_from(now_ns, timelock_ns)
84 .ready_at_ns()
85 .expect("TimeGate::schedule_from always yields a ready timestamp");
86 self.entries.push(PendingValue { value, ready_at_ns });
87 ready_at_ns
88 }
89
90 #[must_use]
91 pub fn has_pending_key<K: PartialEq>(&self, key: &K, mut key_of: impl FnMut(&T) -> K) -> bool {
92 self.entries
93 .iter()
94 .any(|entry| key_of(&entry.value) == *key)
95 }
96
97 pub fn take_by_key<K: PartialEq>(
98 &mut self,
99 now_ns: TimestampNs,
100 key: &K,
101 mut key_of: impl FnMut(&T) -> K,
102 ) -> TakePending<T> {
103 let mut mature_index = None;
104 let mut next_ready_at: Option<TimestampNs> = None;
105
106 for (index, entry) in self.entries.iter().enumerate() {
107 if key_of(&entry.value) != *key {
108 continue;
109 }
110
111 if entry.is_mature(now_ns) {
112 mature_index = Some(index);
113 break;
114 }
115
116 next_ready_at = Some(match next_ready_at {
117 Some(current) => current.min(entry.ready_at_ns),
118 None => entry.ready_at_ns,
119 });
120 }
121
122 if let Some(index) = mature_index {
123 let pending = self.entries.remove(index);
124 return TakePending::Ready(pending.value);
125 }
126
127 match next_ready_at {
128 Some(ready_at_ns) => TakePending::Pending { ready_at_ns },
129 None => TakePending::Missing,
130 }
131 }
132
133 #[must_use]
134 pub fn revoke_by_key<K: PartialEq>(
135 &mut self,
136 key: &K,
137 mut key_of: impl FnMut(&T) -> K,
138 ) -> Vec<T> {
139 let mut retained = Vec::with_capacity(self.entries.len());
140 let mut removed = Vec::new();
141
142 for entry in self.entries.drain(..) {
143 if key_of(&entry.value) == *key {
144 removed.push(entry.value);
145 } else {
146 retained.push(entry);
147 }
148 }
149
150 self.entries = retained;
151 removed
152 }
153
154 pub fn schedule_replacing<K: PartialEq>(
155 &mut self,
156 key: &K,
157 key_of: impl FnMut(&T) -> K,
158 value: T,
159 now_ns: TimestampNs,
160 timelock_ns: DurationNs,
161 ) -> ScheduledPending<T> {
162 let replaced = self.revoke_by_key(key, key_of);
163 let ready_at_ns = self.schedule(value, now_ns, timelock_ns);
164 ScheduledPending {
165 ready_at_ns,
166 replaced,
167 }
168 }
169
170 #[must_use]
171 pub fn from_restored_entries(entries: Vec<PendingValue<T>>) -> Self {
172 Self { entries }
173 }
174
175 #[must_use]
176 pub fn from_entries<I>(entries: I) -> Self
177 where
178 I: IntoIterator<Item = PendingValue<T>>,
179 {
180 Self {
181 entries: entries.into_iter().collect(),
182 }
183 }
184
185 #[must_use]
186 pub fn into_entries(self) -> Vec<PendingValue<T>> {
187 self.entries
188 }
189}
190
191impl<T> IntoIterator for PendingActions<T> {
192 type Item = PendingValue<T>;
193 type IntoIter = alloc::vec::IntoIter<PendingValue<T>>;
194
195 fn into_iter(self) -> Self::IntoIter {
196 self.entries.into_iter()
197 }
198}
199
200impl<'a, T> IntoIterator for &'a PendingActions<T> {
201 type Item = &'a PendingValue<T>;
202 type IntoIter = core::slice::Iter<'a, PendingValue<T>>;
203
204 fn into_iter(self) -> Self::IntoIter {
205 self.entries.iter()
206 }
207}
208
209#[templar_vault_macros::vault_derive(borsh, serde)]
211#[derive(Clone, Copy, PartialEq, Eq)]
212pub enum TimelockDecision {
213 Immediate,
214 Timelocked,
215}
216
217impl TimelockDecision {
218 #[must_use]
219 pub fn requires_timelock(self) -> bool {
220 matches!(self, TimelockDecision::Timelocked)
221 }
222
223 #[must_use]
224 pub fn from_requires_timelock(requires_timelock: bool) -> Self {
225 if requires_timelock {
226 TimelockDecision::Timelocked
227 } else {
228 TimelockDecision::Immediate
229 }
230 }
231
232 #[must_use]
233 pub fn is_immediate(self) -> bool {
234 matches!(self, TimelockDecision::Immediate)
235 }
236}
237
238#[templar_vault_macros::vault_derive]
240#[derive(Clone, PartialEq, Eq)]
241pub enum Restrictions<T> {
242 Paused,
243 Blacklist(Vec<T>),
244 Whitelist(Vec<T>),
245}
246
247fn slice_contains<T: PartialEq>(items: &[T], target: &T) -> bool {
248 items.iter().any(|item| item == target)
249}
250
251fn any_missing_from<T: PartialEq>(source: &[T], candidate_superset: &[T]) -> bool {
252 source
253 .iter()
254 .any(|item| !slice_contains(candidate_superset, item))
255}
256
257fn any_overlap<T: PartialEq>(left: &[T], right: &[T]) -> bool {
258 left.iter().any(|item| slice_contains(right, item))
259}
260
261impl<T: PartialEq> Restrictions<T> {
262 #[must_use]
263 pub fn blacklist(members: Vec<T>) -> Self {
264 Self::Blacklist(normalize_members(members))
265 }
266
267 #[must_use]
268 pub fn whitelist(members: Vec<T>) -> Self {
269 Self::Whitelist(normalize_members(members))
270 }
271
272 #[must_use]
273 pub fn normalized(self) -> Self {
274 match self {
275 Self::Paused => Self::Paused,
276 Self::Blacklist(members) => Self::Blacklist(normalize_members(members)),
277 Self::Whitelist(members) => Self::Whitelist(normalize_members(members)),
278 }
279 }
280
281 #[must_use]
282 pub fn members(&self) -> Option<&[T]> {
283 match self {
284 Self::Paused => None,
285 Self::Blacklist(members) | Self::Whitelist(members) => Some(members),
286 }
287 }
288
289 #[must_use]
291 pub fn determine_relaxed(current: &Option<Self>, next: &Option<Self>) -> bool {
292 match (current, next) {
293 (None, None) => false,
294 (None, Some(_)) => false,
295 (Some(_), None) => true,
296 (Some(Self::Paused), Some(Self::Paused)) => false,
297 (Some(Self::Paused), Some(Self::Whitelist(new))) => !new.is_empty(),
298 (Some(Self::Paused), Some(_)) => true,
299 (Some(Self::Blacklist(old)), Some(Self::Blacklist(new))) => any_missing_from(old, new),
300 (Some(Self::Whitelist(old)), Some(Self::Whitelist(new))) => any_missing_from(new, old),
301 (Some(Self::Blacklist(old)), Some(Self::Whitelist(new))) => any_overlap(old, new),
302 (Some(Self::Whitelist(_)), Some(Self::Paused))
303 | (Some(Self::Blacklist(_)), Some(Self::Paused)) => false,
304 (Some(Self::Whitelist(_)), Some(Self::Blacklist(_))) => true,
305 }
306 }
307}
308
309fn normalize_members<T: PartialEq>(members: Vec<T>) -> Vec<T> {
310 let mut normalized = Vec::with_capacity(members.len());
311 for member in members {
312 if !normalized.iter().any(|existing| existing == &member) {
313 normalized.push(member);
314 }
315 }
316 normalized
317}
318
319pub struct FeeConfig<'a, R> {
321 pub performance_fee: Wad,
322 pub management_fee: Wad,
323 pub performance_recipient: &'a R,
324 pub management_recipient: &'a R,
325 pub max_rate: Option<Wad>,
326}
327
328impl<R: PartialEq> FeeConfig<'_, R> {
329 pub fn evaluate_change(
330 current: &Self,
331 proposed: &Self,
332 ) -> Result<FeeChangeDecision, FeeChangeError> {
333 if proposed.performance_fee > Wad::from(MAX_PERFORMANCE_FEE_WAD) {
334 return Err(FeeChangeError::PerformanceFeeTooHigh);
335 }
336 if proposed.management_fee > Wad::from(MAX_MANAGEMENT_FEE_WAD) {
337 return Err(FeeChangeError::ManagementFeeTooHigh);
338 }
339
340 let performance_fee_changed = proposed.performance_fee != current.performance_fee;
341 let management_fee_changed = proposed.management_fee != current.management_fee;
342 let performance_recipient_changed =
343 proposed.performance_recipient != current.performance_recipient;
344 let management_recipient_changed =
345 proposed.management_recipient != current.management_recipient;
346 let max_rate_changed = proposed.max_rate != current.max_rate;
347
348 if !(performance_fee_changed
349 || management_fee_changed
350 || performance_recipient_changed
351 || management_recipient_changed
352 || max_rate_changed)
353 {
354 return Err(FeeChangeError::NoChange);
355 }
356
357 let fee_increase = proposed.performance_fee > current.performance_fee
358 || proposed.management_fee > current.management_fee;
359 let recipient_changed = performance_recipient_changed || management_recipient_changed;
360
361 let max_rate_relaxed = match (current.max_rate, proposed.max_rate) {
362 (None, None) => false,
363 (None, Some(_)) => false,
364 (Some(_), None) => true,
365 (Some(old), Some(new)) => new > old,
366 };
367
368 Ok(FeeChangeDecision {
369 timelocked: fee_increase || recipient_changed || max_rate_relaxed,
370 fee_increase,
371 recipient_changed,
372 max_rate_relaxed,
373 })
374 }
375}
376
377#[templar_vault_macros::vault_derive(borsh, serde)]
378#[derive(Clone, Copy, PartialEq, Eq)]
379pub struct FeeChangeDecision {
380 pub timelocked: bool,
381 pub fee_increase: bool,
382 pub recipient_changed: bool,
383 pub max_rate_relaxed: bool,
384}
385
386#[templar_vault_macros::vault_derive(borsh, serde)]
387#[derive(Clone, Copy, PartialEq, Eq)]
388pub enum FeeChangeError {
389 NoChange,
390 PerformanceFeeTooHigh,
391 ManagementFeeTooHigh,
392}
393
394#[templar_vault_macros::vault_derive(borsh, serde)]
395#[derive(Clone, Copy, PartialEq, Eq)]
396pub enum TimelockConfigError {
397 NoChange,
398 OutOfBounds,
399}
400
401pub fn timelock_config_decision(
402 current: DurationNs,
403 proposed: DurationNs,
404 min: DurationNs,
405 max: DurationNs,
406) -> Result<TimelockDecision, TimelockConfigError> {
407 if proposed == current {
408 return Err(TimelockConfigError::NoChange);
409 }
410 if proposed < min || proposed > max {
411 return Err(TimelockConfigError::OutOfBounds);
412 }
413 if proposed < current {
414 Ok(TimelockDecision::Timelocked)
415 } else {
416 Ok(TimelockDecision::Immediate)
417 }
418}
419
420#[templar_vault_macros::vault_derive(borsh, serde)]
421#[derive(Clone, Copy, PartialEq, Eq)]
422pub enum CapChangeError {
423 NoChange,
424}
425
426#[templar_vault_macros::vault_derive(borsh, serde)]
427#[derive(Clone, Copy, PartialEq, Eq)]
428pub enum RelativeCapChangeError {
429 NoChange,
430 RelativeCapTooHigh,
431}
432
433#[templar_vault_macros::vault_derive(borsh, serde)]
434#[derive(Clone, Copy, PartialEq, Eq)]
435pub enum MembershipChangeError {
436 NoChange,
437}
438
439#[templar_vault_macros::vault_derive(borsh, serde)]
440#[derive(Clone, Copy, PartialEq, Eq)]
441pub enum MembershipChangeKind {
442 Added,
443 Removed,
444 Reassigned,
445}
446
447impl TimelockDecision {
448 pub fn from_cap_change(current: Option<u128>, proposed: u128) -> Result<Self, CapChangeError> {
453 match current {
454 Some(existing) if proposed == existing => Err(CapChangeError::NoChange),
455 Some(existing) if proposed > existing => Ok(Self::Timelocked),
456 Some(_) => Ok(Self::Immediate),
457 None => Ok(Self::Timelocked),
458 }
459 }
460
461 pub fn from_cap_group_cap_change(
462 current: Option<u128>,
463 proposed: Option<u128>,
464 ) -> Result<Self, CapChangeError> {
465 match (current, proposed) {
466 (None, None) => Err(CapChangeError::NoChange),
467 (None, Some(_)) => Ok(Self::Immediate),
468 (Some(_), None) => Ok(Self::Timelocked),
469 (Some(existing), Some(next)) if next == existing => Err(CapChangeError::NoChange),
470 (Some(existing), Some(next)) if next > existing => Ok(Self::Timelocked),
471 (Some(_), Some(_)) => Ok(Self::Immediate),
472 }
473 }
474
475 pub fn from_relative_cap_change(
476 current: Option<Wad>,
477 proposed: Option<Wad>,
478 ) -> Result<Self, RelativeCapChangeError> {
479 if let Some(proposed) = proposed {
480 if proposed > Wad::one() {
481 return Err(RelativeCapChangeError::RelativeCapTooHigh);
482 }
483 }
484
485 match (current, proposed) {
486 (None, None) => Err(RelativeCapChangeError::NoChange),
487 (None, Some(_)) => Ok(Self::Immediate),
488 (Some(_), None) => Ok(Self::Timelocked),
489 (Some(existing), Some(next)) if next == existing => {
490 Err(RelativeCapChangeError::NoChange)
491 }
492 (Some(existing), Some(next)) if next > existing => Ok(Self::Timelocked),
493 (Some(_), Some(_)) => Ok(Self::Immediate),
494 }
495 }
496
497 #[must_use]
498 pub fn membership_change_kind<T: PartialEq>(
499 current: Option<&T>,
500 proposed: Option<&T>,
501 ) -> Option<MembershipChangeKind> {
502 match (current, proposed) {
503 (None, None) => None,
504 (None, Some(_)) => Some(MembershipChangeKind::Added),
505 (Some(_), None) => Some(MembershipChangeKind::Removed),
506 (Some(current), Some(proposed)) if current == proposed => None,
507 (Some(_), Some(_)) => Some(MembershipChangeKind::Reassigned),
508 }
509 }
510
511 pub fn from_membership_assignment_change<T: PartialEq>(
512 current: Option<&T>,
513 proposed: Option<&T>,
514 ) -> Result<Self, MembershipChangeError> {
515 match Self::membership_change_kind(current, proposed) {
516 Some(_) => Ok(Self::Timelocked),
517 None => Err(MembershipChangeError::NoChange),
518 }
519 }
520
521 #[must_use]
522 pub fn from_membership_change_kind(_change: MembershipChangeKind) -> Self {
523 Self::Timelocked
524 }
525}