hylo_core/exchange_context/
mod.rs1mod exo;
7mod lst;
8#[cfg(feature = "offchain")]
9pub mod marginal;
10
11use fix::prelude::*;
12
13pub use self::exo::ExoExchangeContext;
14pub use self::lst::LstExchangeContext;
15use crate::collateral_ratio::{CollateralRatio, CR};
16use crate::conversion::SwapConversion;
17use crate::error::CoreError;
18#[cfg(feature = "offchain")]
19use crate::error::CoreError::CollateralRatioOverflow;
20use crate::error::CoreError::{
21 DestinationStablecoin, LevercoinMarketCapArithmetic, LevercoinNav,
22 MaxMintable, MaxSwappable, RebalanceBuySideTarget,
23 RebalanceSellSideLiquidity, RequestedStablecoinOverMaxMintable,
24 VirtualStablecoinOverhang, VirtualStablecoinSurplus,
25};
26use crate::exchange_math::{
27 depeg_stablecoin_nav, max_mintable_stablecoin, max_redeemable_stablecoin,
28 max_swappable_stablecoin, next_levercoin_mint_nav, next_levercoin_redeem_nav,
29 total_value_locked,
30};
31use crate::fees::controller::{FeeExtract, LevercoinFees};
32use crate::fees::curve_controller::cr_from_curve;
33use crate::fees::curves::REDEEM_MAX_CR;
34use crate::pyth::{OraclePrice, PriceRange};
35use crate::rebalance::math::{
36 max_buyable_collateral, max_sellable_collateral, midpoint,
37};
38use crate::rebalance::mode::RebalanceMode;
39use crate::rebalance::pricing::{
40 BuyPriceCurve, RebalanceCurveConfig, RebalancePriceController, SellPriceCurve,
41};
42#[cfg(feature = "offchain")]
43use crate::util::max_scaled_input;
44
45pub struct ProjectedState {
48 pub total_collateral: UFix64<N9>,
49 pub stablecoin_supply: UFix64<N6>,
50 pub collateral_ratio: CR,
51}
52
53pub trait ExchangeContext {
55 fn total_collateral(&self) -> UFix64<N9>;
57
58 fn collateral_usd_price(&self) -> PriceRange<N9>;
60
61 fn collateral_oracle_price(&self) -> OraclePrice;
63
64 fn sell_curve_config(&self) -> &RebalanceCurveConfig;
66
67 fn buy_curve_config(&self) -> &RebalanceCurveConfig;
69
70 fn stablecoin_mint_threshold(&self) -> UFix64<N9>;
72
73 fn stablecoin_mint_enabled(&self) -> bool {
75 self
76 .collateral_ratio()
77 .at_least(self.stablecoin_mint_threshold())
78 }
79
80 fn levercoin_mint_enabled(&self) -> bool {
82 self.rebalance_mode() != RebalanceMode::Depeg
83 }
84
85 fn rebalance_sell_curve(&self) -> Result<SellPriceCurve, CoreError> {
90 SellPriceCurve::new(
91 self.collateral_oracle_price(),
92 self.sell_curve_config(),
93 )
94 }
95
96 fn rebalance_buy_curve(&self) -> Result<BuyPriceCurve, CoreError> {
101 BuyPriceCurve::new(self.collateral_oracle_price(), self.buy_curve_config())
102 }
103
104 fn rebalance_sell_active(&self) -> bool {
106 self
107 .rebalance_sell_curve()
108 .is_ok_and(|c| c.is_active(self.collateral_ratio()))
109 }
110
111 fn rebalance_buy_active(&self) -> bool {
113 self
114 .rebalance_buy_curve()
115 .is_ok_and(|c| c.is_active(self.collateral_ratio()))
116 }
117
118 fn rebalance_sell_liquidity(&self) -> Result<UFix64<N9>, CoreError> {
123 let target_cr = RebalanceMode::Neutral.active_range().start()?;
124 let virtual_stablecoin = self.virtual_stablecoin_supply()?;
125 let collateral_usd_price = self.collateral_oracle_price().spot;
126 let total_collateral = self.total_collateral();
127 max_sellable_collateral(
128 target_cr,
129 virtual_stablecoin,
130 collateral_usd_price,
131 total_collateral,
132 )
133 .ok_or(RebalanceSellSideLiquidity)
134 }
135
136 fn rebalance_buy_target(&self) -> Result<UFix64<N9>, CoreError> {
141 let target_cr = RebalanceMode::BuyZone1.active_range().start()?;
142 let virtual_stablecoin = self.virtual_stablecoin_supply()?;
143 let collateral_usd_price = self.collateral_oracle_price().spot;
144 let total_collateral = self.total_collateral();
145 max_buyable_collateral(
146 target_cr,
147 virtual_stablecoin,
148 collateral_usd_price,
149 total_collateral,
150 )
151 .ok_or(RebalanceBuySideTarget)
152 }
153
154 fn rebalance_buy_target_cr(&self) -> Result<UFix64<N9>, CoreError> {
160 match self.rebalance_mode() {
161 RebalanceMode::BuyZone2 => RebalanceMode::BuyZone1.active_range().end(),
162 RebalanceMode::BuyZone1 => {
163 let spot = self.collateral_oracle_price().spot;
164 let cr_spot = self.rebalance_buy_curve()?.cr_at_price(spot)?;
165 self
166 .collateral_ratio()
167 .as_finite()
168 .and_then(|cr| midpoint(cr, cr_spot))
169 .ok_or(RebalanceBuySideTarget)
170 }
171 _ => Err(RebalanceBuySideTarget),
172 }
173 }
174
175 fn rebalance_buy_amount_at_premium(&self) -> Result<UFix64<N9>, CoreError> {
181 max_buyable_collateral(
182 self.rebalance_buy_target_cr()?,
183 self.virtual_stablecoin_supply()?,
184 self.collateral_oracle_price().spot,
185 self.total_collateral(),
186 )
187 .ok_or(RebalanceBuySideTarget)
188 }
189
190 fn rebalance_sell_target_cr(&self) -> Result<UFix64<N9>, CoreError> {
196 match self.rebalance_mode() {
197 RebalanceMode::SellZone2 => {
198 RebalanceMode::SellZone1.active_range().start()
199 }
200 RebalanceMode::SellZone1 => {
201 let spot = self.collateral_oracle_price().spot;
202 let cr_spot = self.rebalance_sell_curve()?.cr_at_price(spot)?;
203 self
204 .collateral_ratio()
205 .as_finite()
206 .and_then(|cr| midpoint(cr, cr_spot))
207 .ok_or(RebalanceSellSideLiquidity)
208 }
209 _ => Err(RebalanceSellSideLiquidity),
210 }
211 }
212
213 fn rebalance_sell_amount_at_discount(&self) -> Result<UFix64<N9>, CoreError> {
219 max_sellable_collateral(
220 self.rebalance_sell_target_cr()?,
221 self.virtual_stablecoin_supply()?,
222 self.collateral_oracle_price().spot,
223 self.total_collateral(),
224 )
225 .ok_or(RebalanceSellSideLiquidity)
226 }
227
228 fn virtual_stablecoin_supply(&self) -> Result<UFix64<N6>, CoreError>;
230
231 fn levercoin_supply(&self) -> Result<UFix64<N6>, CoreError>;
233
234 fn rebalance_mode(&self) -> RebalanceMode;
236
237 fn collateral_ratio(&self) -> CollateralRatio;
239
240 fn levercoin_fees(&self) -> &LevercoinFees;
242
243 fn total_value_locked(&self) -> Result<UFix64<N9>, CoreError> {
248 total_value_locked(
249 self.total_collateral(),
250 self.collateral_usd_price().lower,
251 )
252 }
253
254 fn levercoin_market_cap(&self) -> Result<UFix64<N9>, CoreError> {
260 if self.levercoin_supply()? == UFix64::zero() {
261 Ok(UFix64::zero())
262 } else {
263 let collateral_value = self
264 .total_collateral()
265 .mul_ceil(self.collateral_usd_price().upper)
266 .ok_or(LevercoinMarketCapArithmetic)?;
267 let stablecoin_nav = self.stablecoin_nav()?;
268 let stablecoin_value = self
269 .virtual_stablecoin_supply()?
270 .checked_convert::<N9>()
271 .and_then(|supply| supply.mul_floor(stablecoin_nav))
272 .ok_or(LevercoinMarketCapArithmetic)?;
273 Ok(collateral_value.saturating_sub(&stablecoin_value))
274 }
275 }
276
277 fn stablecoin_nav(&self) -> Result<UFix64<N9>, CoreError> {
282 match self.rebalance_mode() {
283 RebalanceMode::Depeg => depeg_stablecoin_nav(
284 self.total_collateral(),
285 self.collateral_usd_price().lower,
286 self.virtual_stablecoin_supply()?,
287 ),
288 _ => Ok(UFix64::one()),
289 }
290 }
291
292 fn levercoin_mint_nav(&self) -> Result<UFix64<N9>, CoreError> {
297 next_levercoin_mint_nav(
298 self.total_collateral(),
299 self.collateral_usd_price(),
300 self.virtual_stablecoin_supply()?,
301 self.stablecoin_nav()?,
302 self.levercoin_supply()?,
303 )
304 .ok_or(LevercoinNav)
305 }
306
307 fn levercoin_redeem_nav(&self) -> Result<UFix64<N9>, CoreError> {
312 next_levercoin_redeem_nav(
313 self.total_collateral(),
314 self.collateral_usd_price(),
315 self.virtual_stablecoin_supply()?,
316 self.stablecoin_nav()?,
317 self.levercoin_supply()?,
318 )
319 .ok_or(LevercoinNav)
320 }
321
322 fn virtual_stablecoin_overhang(&self) -> Result<UFix64<N6>, CoreError> {
328 let tvl = self.total_value_locked()?;
329 let virtual_stablecoin = self.virtual_stablecoin_supply()?;
330 tvl
331 .checked_convert::<N6>()
332 .and_then(|tvl| virtual_stablecoin.checked_sub(&tvl))
333 .ok_or(VirtualStablecoinOverhang)
334 }
335
336 fn virtual_stablecoin_surplus(&self) -> Result<UFix64<N6>, CoreError> {
342 let tvl = self.total_value_locked()?;
343 let virtual_stablecoin = self.virtual_stablecoin_supply()?;
344 tvl
345 .checked_convert::<N6>()
346 .and_then(|tvl| tvl.checked_sub(&virtual_stablecoin))
347 .ok_or(VirtualStablecoinSurplus)
348 }
349
350 fn projected_rebalance_mode(
356 &self,
357 new_total: UFix64<N9>,
358 new_stablecoin: UFix64<N6>,
359 ) -> Result<RebalanceMode, CoreError> {
360 let projected_cr = CollateralRatio::new(
361 new_total,
362 self.collateral_usd_price().lower,
363 new_stablecoin,
364 )?;
365 Ok(RebalanceMode::from_cr(projected_cr))
366 }
367
368 fn select_rebalance_mode_for_fees(
371 &self,
372 projected: RebalanceMode,
373 ) -> RebalanceMode {
374 projected.min(self.rebalance_mode())
375 }
376
377 fn swap_conversion(&self) -> Result<SwapConversion, CoreError> {
382 let levercoin_nav =
383 PriceRange::new(self.levercoin_redeem_nav()?, self.levercoin_mint_nav()?);
384 Ok(SwapConversion::new(self.stablecoin_nav()?, levercoin_nav))
385 }
386
387 fn max_mintable_stablecoin(&self) -> Result<UFix64<N6>, CoreError> {
393 let target = self
394 .stablecoin_mint_threshold()
395 .checked_convert()
396 .ok_or(MaxMintable)?;
397 max_mintable_stablecoin(
398 target,
399 self.total_collateral(),
400 self.collateral_usd_price().lower,
401 self.virtual_stablecoin_supply()?,
402 )
403 }
404
405 fn max_redeemable_stablecoin(&self) -> Result<UFix64<N6>, CoreError> {
411 max_redeemable_stablecoin(
412 cr_from_curve(REDEEM_MAX_CR)?,
413 self.total_value_locked()?,
414 self.virtual_stablecoin_supply()?,
415 self.stablecoin_nav()?,
416 )
417 }
418
419 fn max_swappable_stablecoin(&self) -> Result<UFix64<N6>, CoreError> {
425 let target = RebalanceMode::SellZone1
426 .active_range()
427 .end()?
428 .checked_convert()
429 .ok_or(MaxSwappable)?;
430 max_swappable_stablecoin(
431 target,
432 self.total_value_locked()?,
433 self.virtual_stablecoin_supply()?,
434 )
435 }
436
437 #[cfg(feature = "offchain")]
444 fn max_collateral_removal(&self) -> Result<UFix64<N9>, CoreError> {
445 let supply = self.virtual_stablecoin_supply()?;
446 if supply == UFix64::zero() {
447 Ok(self.total_collateral())
448 } else {
449 let atom = UFix64::new(1);
450 let last_depeg_cr = RebalanceMode::SellZone2
451 .active_range()
452 .start()?
453 .checked_sub(&atom)
454 .ok_or(CollateralRatioOverflow)?;
455 let min_collateral = supply
456 .checked_convert::<N9>()
457 .and_then(|supply| {
458 max_scaled_input(
459 last_depeg_cr,
460 self.collateral_usd_price().lower,
461 supply,
462 )
463 })
464 .and_then(|last_depeg_collateral| {
465 last_depeg_collateral.checked_add(&atom)
466 })
467 .ok_or(CollateralRatioOverflow)?;
468 self
469 .total_collateral()
470 .checked_sub(&min_collateral)
471 .ok_or(CoreError::DestinationCollateral)
472 }
473 }
474
475 #[cfg(feature = "offchain")]
481 fn max_stablecoin_removal(&self) -> Result<UFix64<N6>, CoreError> {
482 let min_supply = self
483 .total_collateral()
484 .mul_div_ceil(
485 self.collateral_usd_price().lower,
486 UFix64::<N9>::new(u64::MAX),
487 )
488 .and_then(UFix64::checked_convert_ceil::<N6>)
489 .ok_or(CollateralRatioOverflow)?;
490 Ok(
491 self
492 .virtual_stablecoin_supply()?
493 .checked_sub(&min_supply)
494 .unwrap_or_default(),
495 )
496 }
497
498 fn validate_stablecoin_amount(
503 &self,
504 requested: UFix64<N6>,
505 ) -> Result<UFix64<N6>, CoreError> {
506 let max = self.max_mintable_stablecoin()?;
507 if requested <= max {
508 Ok(requested)
509 } else {
510 Err(RequestedStablecoinOverMaxMintable)
511 }
512 }
513
514 fn validate_stablecoin_pnl_profit(
519 &self,
520 requested: UFix64<N6>,
521 ) -> Result<UFix64<N6>, CoreError> {
522 let target = RebalanceMode::SellZone2
523 .active_range()
524 .end()?
525 .checked_convert()
526 .ok_or(MaxMintable)?;
527 let max = max_swappable_stablecoin(
528 target,
529 self.total_value_locked()?,
530 self.virtual_stablecoin_supply()?,
531 )
532 .unwrap_or_default();
533 Ok(requested.min(max))
534 }
535
536 fn validate_stablecoin_swap_amount(
541 &self,
542 requested: UFix64<N6>,
543 ) -> Result<UFix64<N6>, CoreError> {
544 let max = self.max_swappable_stablecoin()?;
545 if requested <= max {
546 Ok(requested)
547 } else {
548 Err(RequestedStablecoinOverMaxMintable)
549 }
550 }
551
552 fn levercoin_to_stablecoin_fee(
557 &self,
558 amount_stablecoin_out: UFix64<N6>,
559 ) -> Result<FeeExtract<N6>, CoreError> {
560 let rate = self.levercoin_to_stablecoin_fee_rate(amount_stablecoin_out)?;
561 FeeExtract::new(rate, amount_stablecoin_out)
562 }
563
564 fn levercoin_to_stablecoin_fee_rate(
569 &self,
570 amount_stablecoin_out: UFix64<N6>,
571 ) -> Result<UFix64<N4>, CoreError> {
572 let new_stablecoin = self
573 .virtual_stablecoin_supply()?
574 .checked_add(&amount_stablecoin_out)
575 .ok_or(DestinationStablecoin)?;
576 let projected =
577 self.projected_rebalance_mode(self.total_collateral(), new_stablecoin)?;
578 let mode = self.select_rebalance_mode_for_fees(projected);
579 self.levercoin_fees().convert_to_stablecoin_fee(mode)
580 }
581
582 fn stablecoin_to_levercoin_fee(
587 &self,
588 amount_stablecoin_in: UFix64<N6>,
589 ) -> Result<FeeExtract<N6>, CoreError> {
590 let rate = self.stablecoin_to_levercoin_fee_rate(amount_stablecoin_in)?;
591 FeeExtract::new(rate, amount_stablecoin_in)
592 }
593
594 fn stablecoin_to_levercoin_fee_rate(
599 &self,
600 amount_stablecoin_in: UFix64<N6>,
601 ) -> Result<UFix64<N4>, CoreError> {
602 let new_stablecoin = self
603 .virtual_stablecoin_supply()?
604 .checked_sub(&amount_stablecoin_in)
605 .ok_or(DestinationStablecoin)?;
606 let projected =
607 self.projected_rebalance_mode(self.total_collateral(), new_stablecoin)?;
608 let mode = self.select_rebalance_mode_for_fees(projected);
609 self.levercoin_fees().convert_from_stablecoin_fee(mode)
610 }
611}