templar_curator_primitives/governance/
mod.rs1use alloc::collections::{BTreeSet, VecDeque};
9use core::cmp::Ordering;
10
11use templar_vault_kernel::math::wad::{Wad, MAX_MANAGEMENT_FEE_WAD, MAX_PERFORMANCE_FEE_WAD};
12use templar_vault_kernel::types::TimestampNs;
13use templar_vault_kernel::TimeGate;
14
15#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
17#[derive(Clone, PartialEq, Eq)]
18pub struct PendingValue<T> {
19 pub value: T,
20 pub valid_at_ns: TimestampNs,
21}
22
23impl<T> PendingValue<T> {
24 #[must_use]
26 pub fn is_mature(&self, now_ns: TimestampNs) -> bool {
27 TimeGate::from_ready_at(self.valid_at_ns).is_ready(now_ns)
28 }
29}
30
31#[templar_vault_macros::vault_derive]
32#[derive(Clone, Copy, PartialEq, Eq)]
33pub enum PendingQueueError {
34 NotMature,
35}
36
37#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
39#[derive(Clone, PartialEq, Eq)]
40pub struct PendingQueue<T> {
41 entries: VecDeque<PendingValue<T>>,
42}
43
44impl<T> Default for PendingQueue<T> {
45 fn default() -> Self {
46 Self {
47 entries: VecDeque::new(),
48 }
49 }
50}
51
52impl<T> PendingQueue<T> {
53 #[must_use]
54 pub fn len(&self) -> usize {
55 self.entries.len()
56 }
57
58 #[must_use]
59 pub fn is_empty(&self) -> bool {
60 self.entries.is_empty()
61 }
62
63 pub fn iter(&self) -> impl Iterator<Item = &PendingValue<T>> {
64 self.entries.iter()
65 }
66
67 #[must_use]
68 pub fn back(&self) -> Option<&PendingValue<T>> {
69 self.entries.back()
70 }
71
72 pub fn push_pending(&mut self, pending: PendingValue<T>) {
73 self.entries.push_back(pending);
74 }
75
76 pub fn schedule(&mut self, value: T, now_ns: TimestampNs, timelock_ns: TimestampNs) {
78 let valid_at_ns = TimeGate::schedule_from(now_ns, timelock_ns)
79 .ready_at_ns()
80 .unwrap_or(now_ns);
81 self.entries.push_back(PendingValue { value, valid_at_ns });
82 }
83
84 #[must_use]
85 pub fn has_pending(&self, mut pred: impl FnMut(&T) -> bool) -> bool {
86 self.entries.iter().any(|entry| pred(&entry.value))
87 }
88
89 pub fn take_mature(
90 &mut self,
91 now_ns: TimestampNs,
92 mut pred: impl FnMut(&T) -> bool,
93 ) -> Result<Option<T>, PendingQueueError> {
94 let mature_index = self
97 .entries
98 .iter()
99 .position(|entry| pred(&entry.value) && entry.is_mature(now_ns));
100
101 if let Some(index) = mature_index {
102 let Some(pending) = self.entries.remove(index) else {
103 return Ok(None);
104 };
105 return Ok(Some(pending.value));
106 }
107
108 let has_immature_match = self.entries.iter().any(|entry| pred(&entry.value));
110 if has_immature_match {
111 return Err(PendingQueueError::NotMature);
112 }
113
114 Ok(None)
115 }
116
117 #[must_use]
118 pub fn revoke_pending(&mut self, mut pred: impl FnMut(&T) -> bool) -> bool {
119 let mut removed_any = false;
120 self.entries.retain(|entry| {
121 let keep = !pred(&entry.value);
122 if !keep {
123 removed_any = true;
124 }
125 keep
126 });
127 removed_any
128 }
129}
130
131impl<T> From<VecDeque<PendingValue<T>>> for PendingQueue<T> {
132 fn from(entries: VecDeque<PendingValue<T>>) -> Self {
133 Self { entries }
134 }
135}
136
137impl<T> From<PendingQueue<T>> for VecDeque<PendingValue<T>> {
138 fn from(queue: PendingQueue<T>) -> Self {
139 queue.entries
140 }
141}
142
143pub fn submission_requires_timelock<E>(decision: Result<TimelockDecision, E>) -> Result<bool, E> {
144 decision.map(TimelockDecision::requires_timelock)
145}
146
147#[templar_vault_macros::vault_derive(borsh, serde)]
149#[derive(Clone, Copy, PartialEq, Eq)]
150pub enum TimelockDecision {
151 Immediate,
152 Timelocked,
153}
154
155impl TimelockDecision {
156 #[must_use]
157 pub fn requires_timelock(self) -> bool {
158 matches!(self, TimelockDecision::Timelocked)
159 }
160
161 #[must_use]
162 pub fn from_requires_timelock(requires_timelock: bool) -> Self {
163 if requires_timelock {
164 TimelockDecision::Timelocked
165 } else {
166 TimelockDecision::Immediate
167 }
168 }
169
170 #[must_use]
171 pub fn is_immediate(self) -> bool {
172 matches!(self, TimelockDecision::Immediate)
173 }
174}
175
176impl TryFrom<Ordering> for TimelockDecision {
177 type Error = ();
178
179 fn try_from(ordering: Ordering) -> Result<Self, Self::Error> {
180 match ordering {
181 Ordering::Equal => Err(()),
182 Ordering::Greater => Ok(TimelockDecision::Timelocked),
183 Ordering::Less => Ok(TimelockDecision::Immediate),
184 }
185 }
186}
187
188#[templar_vault_macros::vault_derive]
190#[derive(Clone, PartialEq, Eq)]
191pub enum Restrictions<T> {
192 Paused,
193 Blacklist(BTreeSet<T>),
194 Whitelist(BTreeSet<T>),
195}
196
197impl<T: Ord> Restrictions<T> {
198 #[must_use]
200 pub fn determine_relaxed(current: &Option<Self>, next: &Option<Self>) -> bool {
201 match (current, next) {
202 (None, None) => false,
203 (None, Some(_)) => false,
204 (Some(_), None) => true,
205 (Some(Self::Paused), Some(Self::Paused)) => false,
206 (Some(Self::Paused), Some(Self::Whitelist(new))) => !new.is_empty(),
207 (Some(Self::Paused), Some(_)) => true,
208 (Some(Self::Blacklist(old)), Some(Self::Blacklist(new))) => {
209 old.difference(new).next().is_some()
210 }
211 (Some(Self::Whitelist(old)), Some(Self::Whitelist(new))) => {
212 new.difference(old).next().is_some()
213 }
214 (Some(Self::Blacklist(old)), Some(Self::Whitelist(new))) => {
215 old.intersection(new).next().is_some()
216 }
217 (Some(Self::Whitelist(_)), Some(Self::Paused))
218 | (Some(Self::Blacklist(_)), Some(Self::Paused)) => false,
219 (Some(Self::Whitelist(_)), Some(Self::Blacklist(_))) => true,
220 }
221 }
222}
223
224pub struct FeeConfig<'a, R> {
226 pub performance_fee: Wad,
227 pub management_fee: Wad,
228 pub performance_recipient: &'a R,
229 pub management_recipient: &'a R,
230 pub max_rate: Option<Wad>,
231}
232
233impl<R: PartialEq> FeeConfig<'_, R> {
234 pub fn evaluate_change(
235 current: &Self,
236 proposed: &Self,
237 ) -> Result<FeeChangeDecision, FeeChangeError> {
238 if proposed.performance_fee > Wad::from(MAX_PERFORMANCE_FEE_WAD) {
239 return Err(FeeChangeError::PerformanceFeeTooHigh);
240 }
241 if proposed.management_fee > Wad::from(MAX_MANAGEMENT_FEE_WAD) {
242 return Err(FeeChangeError::ManagementFeeTooHigh);
243 }
244
245 let performance_fee_changed = proposed.performance_fee != current.performance_fee;
246 let management_fee_changed = proposed.management_fee != current.management_fee;
247 let performance_recipient_changed =
248 proposed.performance_recipient != current.performance_recipient;
249 let management_recipient_changed =
250 proposed.management_recipient != current.management_recipient;
251 let max_rate_changed = proposed.max_rate != current.max_rate;
252
253 if !(performance_fee_changed
254 || management_fee_changed
255 || performance_recipient_changed
256 || management_recipient_changed
257 || max_rate_changed)
258 {
259 return Err(FeeChangeError::NoChange);
260 }
261
262 let fee_increase = proposed.performance_fee > current.performance_fee
263 || proposed.management_fee > current.management_fee;
264 let recipient_changed = performance_recipient_changed || management_recipient_changed;
265
266 let max_rate_relaxed = match (current.max_rate, proposed.max_rate) {
267 (None, None) => false,
268 (None, Some(_)) => false,
269 (Some(_), None) => true,
270 (Some(old), Some(new)) => new > old,
271 };
272
273 Ok(FeeChangeDecision {
274 timelocked: fee_increase || recipient_changed || max_rate_relaxed,
275 fee_increase,
276 recipient_changed,
277 max_rate_relaxed,
278 })
279 }
280}
281
282#[templar_vault_macros::vault_derive(borsh, serde)]
283#[derive(Clone, Copy, PartialEq, Eq)]
284pub struct FeeChangeDecision {
285 pub timelocked: bool,
286 pub fee_increase: bool,
287 pub recipient_changed: bool,
288 pub max_rate_relaxed: bool,
289}
290
291#[templar_vault_macros::vault_derive(borsh, serde)]
292#[derive(Clone, Copy, PartialEq, Eq)]
293pub enum FeeChangeError {
294 NoChange,
295 PerformanceFeeTooHigh,
296 ManagementFeeTooHigh,
297}
298
299#[templar_vault_macros::vault_derive(borsh, serde)]
300#[derive(Clone, Copy, PartialEq, Eq)]
301pub enum TimelockConfigError {
302 NoChange,
303 OutOfBounds,
304}
305
306pub fn timelock_config_decision(
307 current: TimestampNs,
308 proposed: TimestampNs,
309 min: TimestampNs,
310 max: TimestampNs,
311) -> Result<TimelockDecision, TimelockConfigError> {
312 if proposed == current {
313 return Err(TimelockConfigError::NoChange);
314 }
315 if proposed < min || proposed > max {
316 return Err(TimelockConfigError::OutOfBounds);
317 }
318 if proposed < current {
319 Ok(TimelockDecision::Timelocked)
320 } else {
321 Ok(TimelockDecision::Immediate)
322 }
323}
324
325#[templar_vault_macros::vault_derive(borsh, serde)]
326#[derive(Clone, Copy, PartialEq, Eq)]
327pub enum CapChangeError {
328 NoChange,
329}
330
331#[templar_vault_macros::vault_derive(borsh, serde)]
332#[derive(Clone, Copy, PartialEq, Eq)]
333pub enum RelativeCapChangeError {
334 NoChange,
335 RelativeCapTooHigh,
336}
337
338#[templar_vault_macros::vault_derive(borsh, serde)]
339#[derive(Clone, Copy, PartialEq, Eq)]
340pub enum MembershipChangeError {
341 NoChange,
342}
343
344impl TimelockDecision {
345 pub fn from_cap_change(current: Option<u128>, proposed: u128) -> Result<Self, CapChangeError> {
350 match current {
351 Some(existing) => {
352 Self::try_from(proposed.cmp(&existing)).map_err(|_| CapChangeError::NoChange)
353 }
354 None => Ok(Self::Timelocked),
355 }
356 }
357
358 pub fn from_cap_group_cap_change(
364 current: Option<u128>,
365 proposed: u128,
366 ) -> Result<Self, CapChangeError> {
367 let normalize = |cap: Option<u128>| cap.and_then(core::num::NonZeroU128::new);
368 let current_cap = normalize(current);
369 let proposed_cap = core::num::NonZeroU128::new(proposed);
370
371 match (current_cap, proposed_cap) {
372 (None, None) => Err(CapChangeError::NoChange),
373 (None, Some(_)) => Ok(Self::Immediate),
374 (Some(_), None) => Ok(Self::Timelocked),
375 (Some(existing), Some(next)) => Self::try_from(next.get().cmp(&existing.get()))
376 .map_err(|_| CapChangeError::NoChange),
377 }
378 }
379
380 pub fn from_relative_cap_change(
381 current: Option<Wad>,
382 proposed: Wad,
383 ) -> Result<Self, RelativeCapChangeError> {
384 if proposed > Wad::one() {
385 return Err(RelativeCapChangeError::RelativeCapTooHigh);
386 }
387
388 match current {
389 Some(existing) => Self::try_from(proposed.cmp(&existing))
390 .map_err(|_| RelativeCapChangeError::NoChange),
391 None => Ok(Self::Timelocked),
392 }
393 }
394
395 pub fn from_membership_change(changed: bool) -> Result<Self, MembershipChangeError> {
396 if changed {
397 Ok(Self::Timelocked)
398 } else {
399 Err(MembershipChangeError::NoChange)
400 }
401 }
402}