templar_curator_primitives/policy/cap_group/
mod.rs1use alloc::string::String;
4#[cfg(feature = "borsh-schema")]
5use alloc::string::ToString;
6use core::num::NonZeroU128;
7#[cfg(not(target_arch = "wasm32"))]
8use derive_more::Display;
9use derive_more::{From, Into};
10use templar_vault_kernel::Wad;
11use typed_builder::TypedBuilder;
12
13#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, 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, From, Into)]
17pub struct CapGroupId(pub String);
18
19impl From<&str> for CapGroupId {
20 fn from(value: &str) -> Self {
21 Self(String::from(value))
22 }
23}
24
25#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
30#[derive(Clone, PartialEq, Eq, Default, TypedBuilder)]
31pub struct CapGroup {
32 #[builder(default, setter(transform = |cap: u128| NonZeroU128::new(cap)))]
35 pub absolute_cap: Option<NonZeroU128>,
36 #[builder(default, setter(transform = |cap: Wad| if cap.is_zero() { None } else { Some(cap) }))]
39 pub relative_cap: Option<Wad>,
40}
41
42impl CapGroup {
43 #[must_use]
44 pub fn is_unlimited(&self) -> bool {
45 self.absolute_cap.is_none() && self.relative_cap.is_none()
46 }
47
48 #[must_use]
52 pub fn effective_cap(&self, total_assets: u128) -> u128 {
53 if self.is_unlimited() {
54 return u128::MAX;
55 }
56
57 let absolute = self.absolute_cap.map(|c| c.get()).unwrap_or(u128::MAX);
58
59 let relative = self
60 .relative_cap
61 .map(|cap| {
62 cap.apply_floored(templar_vault_kernel::Number::from(total_assets))
63 .as_u128_saturating()
64 })
65 .unwrap_or(u128::MAX);
66
67 absolute.min(relative)
68 }
69
70 #[must_use]
72 pub fn can_allocate(&self, current_principal: u128, amount: u128, total_assets: u128) -> bool {
73 if self.is_unlimited() {
74 return true;
75 }
76
77 let Some(new_principal) = current_principal.checked_add(amount) else {
78 return false;
79 };
80 new_principal <= self.effective_cap(total_assets)
81 }
82
83 pub fn enforce(
85 &self,
86 current_principal: u128,
87 amount: u128,
88 total_assets: u128,
89 ) -> Result<(), CapGroupError> {
90 if self.is_unlimited() {
91 return Ok(());
92 }
93
94 let Some(new_principal) = current_principal.checked_add(amount) else {
95 return Err(CapGroupError::Overflow {
96 current_principal,
97 requested: amount,
98 });
99 };
100
101 if let Some(abs_cap) = self.absolute_cap {
102 if new_principal > abs_cap.get() {
103 return Err(CapGroupError::ExceedsAbsoluteCap {
104 requested: amount,
105 current_principal,
106 absolute_cap: abs_cap.get(),
107 });
108 }
109 }
110
111 if let Some(ref rel_cap) = self.relative_cap {
112 let effective_cap = rel_cap
113 .apply_floored(templar_vault_kernel::Number::from(total_assets))
114 .as_u128_saturating();
115
116 if new_principal > effective_cap {
117 return Err(CapGroupError::ExceedsRelativeCap {
118 requested: amount,
119 current_principal,
120 effective_cap,
121 total_assets,
122 });
123 }
124 }
125
126 Ok(())
127 }
128
129 #[must_use]
131 pub fn available_capacity(&self, current_principal: u128, total_assets: u128) -> u128 {
132 if self.is_unlimited() {
133 return u128::MAX;
134 }
135
136 let effective_cap = self.effective_cap(total_assets);
137 effective_cap.saturating_sub(current_principal)
138 }
139}
140
141#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
143#[derive(Clone, Default)]
144pub struct CapGroupRecord {
145 pub cap: CapGroup,
147 pub principal: u128,
149}
150
151impl CapGroupRecord {
152 #[must_use]
154 pub fn apply_allocation(&self, amount: u128) -> Self {
155 Self {
156 cap: self.cap.clone(),
157 principal: self.principal.checked_add(amount).unwrap(),
158 }
159 }
160
161 #[must_use]
163 pub fn remove_allocation(&self, amount: u128) -> Self {
164 Self {
165 cap: self.cap.clone(),
166 principal: self.principal.checked_sub(amount).unwrap(),
167 }
168 }
169
170 #[must_use]
172 pub fn can_allocate(&self, amount: u128, total_assets: u128) -> bool {
173 self.cap.can_allocate(self.principal, amount, total_assets)
174 }
175
176 pub fn enforce(&self, amount: u128, total_assets: u128) -> Result<(), CapGroupError> {
178 self.cap.enforce(self.principal, amount, total_assets)
179 }
180
181 #[must_use]
183 pub fn available_capacity(&self, total_assets: u128) -> u128 {
184 self.cap.available_capacity(self.principal, total_assets)
185 }
186}
187
188impl From<CapGroup> for CapGroupRecord {
189 fn from(cap: CapGroup) -> Self {
190 Self { cap, principal: 0 }
191 }
192}
193
194#[templar_vault_macros::vault_derive]
196#[derive(Clone, PartialEq, Eq)]
197pub enum CapGroupError {
198 ExceedsAbsoluteCap {
200 requested: u128,
201 current_principal: u128,
202 absolute_cap: u128,
203 },
204 ExceedsRelativeCap {
206 requested: u128,
207 current_principal: u128,
208 effective_cap: u128,
209 total_assets: u128,
210 },
211 NotFound { id: CapGroupId },
213 Overflow {
215 current_principal: u128,
216 requested: u128,
217 },
218}
219
220#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
222#[derive(Clone, PartialEq, Eq)]
223pub enum CapGroupUpdate {
224 SetCap {
225 cap_group_id: CapGroupId,
226 new_cap: u128,
227 },
228 SetRelativeCap {
229 cap_group_id: CapGroupId,
230 new_relative_cap_wad: u128,
231 },
232 SetMembership {
233 market_id: templar_vault_kernel::TargetId,
234 cap_group_id: Option<CapGroupId>,
235 },
236}
237
238#[templar_vault_macros::vault_derive(borsh, borsh_schema, postcard, schemars, serde)]
240#[derive(Clone, PartialEq, Eq)]
241pub enum CapGroupUpdateKey {
242 SetCap {
243 cap_group_id: CapGroupId,
244 },
245 SetRelativeCap {
246 cap_group_id: CapGroupId,
247 },
248 SetMembership {
249 market_id: templar_vault_kernel::TargetId,
250 },
251}
252
253impl CapGroupUpdate {
254 #[must_use]
256 pub fn key(&self) -> CapGroupUpdateKey {
257 self.into()
258 }
259}
260
261impl From<&CapGroupUpdate> for CapGroupUpdateKey {
262 fn from(value: &CapGroupUpdate) -> Self {
263 match value {
264 CapGroupUpdate::SetCap { cap_group_id, .. } => Self::SetCap {
265 cap_group_id: cap_group_id.clone(),
266 },
267 CapGroupUpdate::SetRelativeCap { cap_group_id, .. } => Self::SetRelativeCap {
268 cap_group_id: cap_group_id.clone(),
269 },
270 CapGroupUpdate::SetMembership { market_id, .. } => Self::SetMembership {
271 market_id: *market_id,
272 },
273 }
274 }
275}
276
277pub fn validate_allocations(
290 allocations: &[(&CapGroupId, &CapGroupRecord, u128)],
291 total_assets: u128,
292) -> Result<(), CapGroupError> {
293 use alloc::collections::BTreeMap;
294
295 let mut cumulative: BTreeMap<&CapGroupId, u128> = BTreeMap::new();
297
298 for (group_id, record, amount) in allocations {
299 let prior_cumulative = cumulative.get(group_id).copied().unwrap_or(0);
300
301 let effective_principal =
303 record
304 .principal
305 .checked_add(prior_cumulative)
306 .ok_or(CapGroupError::Overflow {
307 current_principal: record.principal,
308 requested: prior_cumulative,
309 })?;
310
311 record
313 .cap
314 .enforce(effective_principal, *amount, total_assets)?;
315
316 let new_cumulative =
318 prior_cumulative
319 .checked_add(*amount)
320 .ok_or(CapGroupError::Overflow {
321 current_principal: prior_cumulative,
322 requested: *amount,
323 })?;
324 cumulative.insert(group_id, new_cumulative);
325 }
326 Ok(())
327}