1use alloc::string::String;
4#[cfg(feature = "borsh-schema")]
5use alloc::string::ToString;
6use alloc::vec::Vec;
7use core::str::FromStr;
8#[cfg(not(target_arch = "wasm32"))]
9use derive_more::Display;
10use templar_vault_kernel::Wad;
11use typed_builder::TypedBuilder;
12
13#[templar_vault_macros::vault_derive(borsh, borsh_schema, schemars, serde)]
14#[cfg_attr(not(target_arch = "wasm32"), derive(Display))]
15#[cfg_attr(not(target_arch = "wasm32"), display("{_0}"))]
16#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct CapGroupId(String);
18
19impl CapGroupId {
20 const POLICY_STATE_SENTINEL: &'static str = "policy-state";
21
22 #[must_use]
23 pub fn as_str(&self) -> &str {
24 &self.0
25 }
26
27 #[must_use]
28 pub(crate) fn policy_state_sentinel() -> Self {
29 Self(String::from(Self::POLICY_STATE_SENTINEL))
30 }
31
32 fn validate(value: &str) -> Result<(), CapGroupIdError> {
33 const MAX_LEN: usize = 64;
34
35 if value.is_empty() {
36 return Err(CapGroupIdError::Empty);
37 }
38
39 if value.len() > MAX_LEN {
40 return Err(CapGroupIdError::TooLong { max_len: MAX_LEN });
41 }
42
43 if !value.bytes().all(|byte| {
44 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
45 }) {
46 return Err(CapGroupIdError::InvalidCharacter);
47 }
48
49 Ok(())
50 }
51}
52
53impl TryFrom<String> for CapGroupId {
54 type Error = CapGroupIdError;
55
56 fn try_from(value: String) -> Result<Self, Self::Error> {
57 Self::validate(&value)?;
58 Ok(Self(value))
59 }
60}
61
62impl TryFrom<&str> for CapGroupId {
63 type Error = CapGroupIdError;
64
65 fn try_from(value: &str) -> Result<Self, Self::Error> {
66 Self::validate(value)?;
67 Ok(Self(String::from(value)))
68 }
69}
70
71impl FromStr for CapGroupId {
72 type Err = CapGroupIdError;
73
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
75 Self::try_from(s)
76 }
77}
78
79impl From<CapGroupId> for String {
80 fn from(value: CapGroupId) -> Self {
81 value.0
82 }
83}
84
85#[templar_vault_macros::vault_derive]
86#[derive(Clone, PartialEq, Eq)]
87pub enum CapGroupIdError {
88 Empty,
89 TooLong { max_len: usize },
90 InvalidCharacter,
91}
92
93#[templar_vault_macros::vault_derive(borsh, borsh_schema, schemars, serde)]
98#[derive(Clone, PartialEq, Eq, Default, TypedBuilder)]
99pub struct CapGroup {
100 #[builder(default, setter(transform = |cap: u128| Some(cap)))]
103 absolute_cap: Option<u128>,
104 #[builder(default, setter(transform = |cap: Wad| Some(cap)))]
107 relative_cap: Option<Wad>,
108}
109
110impl CapGroup {
111 #[must_use]
112 pub fn absolute_cap(&self) -> Option<u128> {
113 self.absolute_cap
114 }
115
116 #[must_use]
117 pub fn relative_cap(&self) -> Option<Wad> {
118 self.relative_cap
119 }
120
121 pub fn set_absolute_cap(&mut self, absolute_cap: Option<u128>) {
122 self.absolute_cap = absolute_cap;
123 }
124
125 pub fn set_relative_cap(&mut self, relative_cap: Option<Wad>) {
126 self.relative_cap = relative_cap;
127 }
128
129 #[must_use]
130 pub fn is_unlimited(&self) -> bool {
131 self.absolute_cap.is_none() && self.relative_cap.is_none()
132 }
133
134 #[must_use]
138 pub fn effective_cap(&self, total_assets: u128) -> u128 {
139 if self.is_unlimited() {
140 return u128::MAX;
141 }
142
143 let absolute = self.absolute_cap.unwrap_or(u128::MAX);
144
145 let relative = self
146 .relative_cap
147 .map(|cap| {
148 cap.apply_floored(templar_vault_kernel::Number::from(total_assets))
149 .as_u128_saturating()
150 })
151 .unwrap_or(u128::MAX);
152
153 absolute.min(relative)
154 }
155
156 #[must_use]
158 pub fn can_allocate(&self, current_principal: u128, amount: u128, total_assets: u128) -> bool {
159 let Some(new_principal) = current_principal.checked_add(amount) else {
160 return false;
161 };
162 new_principal <= self.effective_cap(total_assets)
163 }
164
165 pub fn enforce(
167 &self,
168 current_principal: u128,
169 amount: u128,
170 total_assets: u128,
171 ) -> Result<(), CapGroupError> {
172 let Some(new_principal) = current_principal.checked_add(amount) else {
173 return Err(CapGroupError::Overflow {
174 current_principal,
175 requested: amount,
176 });
177 };
178
179 if let Some(abs_cap) = self.absolute_cap {
180 if new_principal > abs_cap {
181 return Err(CapGroupError::ExceedsAbsoluteCap {
182 cap_group_id: None,
183 requested: amount,
184 current_principal,
185 absolute_cap: abs_cap,
186 });
187 }
188 }
189
190 if let Some(ref rel_cap) = self.relative_cap {
191 let effective_cap = rel_cap
192 .apply_floored(templar_vault_kernel::Number::from(total_assets))
193 .as_u128_saturating();
194
195 if new_principal > effective_cap {
196 return Err(CapGroupError::ExceedsRelativeCap {
197 cap_group_id: None,
198 requested: amount,
199 current_principal,
200 effective_cap,
201 total_assets,
202 });
203 }
204 }
205
206 Ok(())
207 }
208
209 #[must_use]
211 pub fn available_capacity(&self, current_principal: u128, total_assets: u128) -> u128 {
212 self.effective_cap(total_assets)
213 .saturating_sub(current_principal)
214 }
215}
216
217#[templar_vault_macros::vault_derive(borsh, borsh_schema, schemars, serde)]
219#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Eq))]
220#[derive(Clone, Default)]
221pub struct CapGroupRecord {
222 pub cap: CapGroup,
224 pub principal: u128,
226}
227
228impl CapGroupRecord {
229 pub fn apply_allocation(&self, amount: u128) -> Result<Self, CapGroupError> {
231 let principal = self
232 .principal
233 .checked_add(amount)
234 .ok_or(CapGroupError::Overflow {
235 current_principal: self.principal,
236 requested: amount,
237 })?;
238
239 Ok(Self {
240 cap: self.cap.clone(),
241 principal,
242 })
243 }
244
245 pub fn remove_allocation(&self, amount: u128) -> Result<Self, CapGroupError> {
247 let principal = self
248 .principal
249 .checked_sub(amount)
250 .ok_or(CapGroupError::Underflow {
251 current_principal: self.principal,
252 requested: amount,
253 })?;
254
255 Ok(Self {
256 cap: self.cap.clone(),
257 principal,
258 })
259 }
260
261 #[must_use]
263 pub fn can_allocate(&self, amount: u128, total_assets: u128) -> bool {
264 self.cap.can_allocate(self.principal, amount, total_assets)
265 }
266
267 pub fn enforce(&self, amount: u128, total_assets: u128) -> Result<(), CapGroupError> {
269 self.cap.enforce(self.principal, amount, total_assets)
270 }
271
272 #[must_use]
274 pub fn available_capacity(&self, total_assets: u128) -> u128 {
275 self.cap.available_capacity(self.principal, total_assets)
276 }
277}
278
279impl From<CapGroup> for CapGroupRecord {
280 fn from(cap: CapGroup) -> Self {
281 Self { cap, principal: 0 }
282 }
283}
284
285#[templar_vault_macros::vault_derive]
287#[derive(Clone, PartialEq, Eq)]
288pub enum CapGroupError {
289 ExceedsAbsoluteCap {
291 cap_group_id: Option<CapGroupId>,
292 requested: u128,
293 current_principal: u128,
294 absolute_cap: u128,
295 },
296 ExceedsRelativeCap {
298 cap_group_id: Option<CapGroupId>,
299 requested: u128,
300 current_principal: u128,
301 effective_cap: u128,
302 total_assets: u128,
303 },
304 NotFound {
306 id: CapGroupId,
307 },
308 Overflow {
310 current_principal: u128,
311 requested: u128,
312 },
313 Underflow {
314 current_principal: u128,
315 requested: u128,
316 },
317 InconsistentRecord {
318 id: CapGroupId,
319 },
320}
321
322#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
324#[derive(Clone, PartialEq, Eq)]
325pub enum CapGroupUpdate {
326 SetCap {
327 cap_group_id: CapGroupId,
328 new_cap: Option<u128>,
329 },
330 SetRelativeCap {
331 cap_group_id: CapGroupId,
332 new_relative_cap: Option<Wad>,
333 },
334 SetMembership {
335 market_id: templar_vault_kernel::TargetId,
336 cap_group_id: Option<CapGroupId>,
337 },
338}
339
340#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
342#[derive(Clone, PartialEq, Eq)]
343pub enum CapGroupUpdateKey {
344 SetCap {
345 cap_group_id: CapGroupId,
346 },
347 SetRelativeCap {
348 cap_group_id: CapGroupId,
349 },
350 SetMembership {
351 market_id: templar_vault_kernel::TargetId,
352 },
353}
354
355impl CapGroupUpdate {
356 #[must_use]
358 pub fn key(&self) -> CapGroupUpdateKey {
359 self.into()
360 }
361}
362
363impl From<&CapGroupUpdate> for CapGroupUpdateKey {
364 fn from(value: &CapGroupUpdate) -> Self {
365 match value {
366 CapGroupUpdate::SetCap { cap_group_id, .. } => Self::SetCap {
367 cap_group_id: cap_group_id.clone(),
368 },
369 CapGroupUpdate::SetRelativeCap { cap_group_id, .. } => Self::SetRelativeCap {
370 cap_group_id: cap_group_id.clone(),
371 },
372 CapGroupUpdate::SetMembership { market_id, .. } => Self::SetMembership {
373 market_id: *market_id,
374 },
375 }
376 }
377}
378
379pub fn validate_allocations(
392 allocations: &[(&CapGroupId, &CapGroupRecord, u128)],
393 total_assets: u128,
394) -> Result<(), CapGroupError> {
395 let mut cumulative: Vec<(&CapGroupId, CapGroupRecord, u128)> = Vec::new();
396
397 for (group_id, record, amount) in allocations {
398 let existing = cumulative
399 .iter_mut()
400 .find(|(existing_group_id, _, _)| *existing_group_id == *group_id);
401
402 let (_, canonical_record, prior_cumulative) = match existing {
403 Some(existing) => existing,
404 None => {
405 let index = cumulative.len();
406 cumulative.push((group_id, (*record).clone(), 0));
407 &mut cumulative[index]
408 }
409 };
410
411 if canonical_record.principal != record.principal || canonical_record.cap != record.cap {
412 return Err(CapGroupError::InconsistentRecord {
413 id: (*group_id).clone(),
414 });
415 }
416
417 let effective_principal = canonical_record
418 .principal
419 .checked_add(*prior_cumulative)
420 .ok_or(CapGroupError::Overflow {
421 current_principal: canonical_record.principal,
422 requested: *prior_cumulative,
423 })?;
424
425 canonical_record
426 .cap
427 .enforce(effective_principal, *amount, total_assets)
428 .map_err(|error| match error {
429 CapGroupError::ExceedsAbsoluteCap {
430 requested,
431 current_principal,
432 absolute_cap,
433 ..
434 } => CapGroupError::ExceedsAbsoluteCap {
435 cap_group_id: Some((*group_id).clone()),
436 requested,
437 current_principal,
438 absolute_cap,
439 },
440 CapGroupError::ExceedsRelativeCap {
441 requested,
442 current_principal,
443 effective_cap,
444 total_assets,
445 ..
446 } => CapGroupError::ExceedsRelativeCap {
447 cap_group_id: Some((*group_id).clone()),
448 requested,
449 current_principal,
450 effective_cap,
451 total_assets,
452 },
453 other => other,
454 })?;
455
456 *prior_cumulative =
457 prior_cumulative
458 .checked_add(*amount)
459 .ok_or(CapGroupError::Overflow {
460 current_principal: *prior_cumulative,
461 requested: *amount,
462 })?;
463 }
464 Ok(())
465}