1use anchor_lang::prelude::{
2 borsh, AnchorDeserialize, AnchorSerialize, InitSpace,
3};
4use fix::prelude::*;
5#[cfg(feature = "offchain")]
6use fix::typenum::Integer;
7use serde::{Deserialize, Serialize};
8
9use crate::error::CoreError;
10use crate::error::CoreError::{
11 FeeExtraction, InvalidFees, NoValidLevercoinMintFee,
12 NoValidLevercoinRedeemFee, NoValidSwapFee,
13};
14use crate::rebalance::mode::RebalanceMode::{
15 self, BuyZone1, BuyZone2, Depeg, Neutral, SellZone1, SellZone2,
16};
17#[cfg(feature = "offchain")]
18use crate::util::max_scaled_input;
19
20const MAX_FEE: UFix64<N4> = UFix64::constant(1000);
21
22#[derive(
26 Clone,
27 Copy,
28 PartialEq,
29 InitSpace,
30 AnchorSerialize,
31 AnchorDeserialize,
32 Serialize,
33 Deserialize,
34)]
35pub struct FeePair {
36 pub mint: UFixValue64,
37 pub redeem: UFixValue64,
38}
39
40impl FeePair {
41 #[must_use]
42 pub fn new(mint: UFixValue64, redeem: UFixValue64) -> FeePair {
43 FeePair { mint, redeem }
44 }
45
46 pub fn mint(&self) -> Result<UFix64<N4>, CoreError> {
47 Ok(self.mint.try_into()?)
48 }
49
50 pub fn redeem(&self) -> Result<UFix64<N4>, CoreError> {
51 Ok(self.redeem.try_into()?)
52 }
53
54 pub fn validate(&self) -> Result<(), CoreError> {
55 (self.mint()? <= MAX_FEE && self.redeem()? <= MAX_FEE)
56 .then_some(())
57 .ok_or(InvalidFees)
58 }
59}
60
61pub trait FeeController: Sized {
63 fn mint_fee(&self, mode: RebalanceMode) -> Result<UFix64<N4>, CoreError>;
64 fn redeem_fee(&self, mode: RebalanceMode) -> Result<UFix64<N4>, CoreError>;
65 fn validate(self) -> Result<Self, CoreError>;
66}
67
68pub struct FeeExtract<Exp> {
71 pub fees_extracted: UFix64<Exp>,
72 pub amount_remaining: UFix64<Exp>,
73}
74
75impl<Exp> FeeExtract<Exp> {
76 pub fn new<FeeExp>(
77 fee: UFix64<FeeExp>,
78 amount_in: UFix64<Exp>,
79 ) -> Result<FeeExtract<Exp>, CoreError>
80 where
81 UFix64<FeeExp>: FixExt,
82 {
83 FeeExtract::split(fee, amount_in).ok_or(FeeExtraction)
84 }
85
86 fn split<FeeExp>(
87 fee: UFix64<FeeExp>,
88 amount_in: UFix64<Exp>,
89 ) -> Option<FeeExtract<Exp>>
90 where
91 UFix64<FeeExp>: FixExt,
92 {
93 let fees_extracted =
94 amount_in.mul_div_ceil(fee, UFix64::<FeeExp>::one())?;
95 let amount_remaining = amount_in.checked_sub(&fees_extracted)?;
96 Some(FeeExtract {
97 fees_extracted,
98 amount_remaining,
99 })
100 }
101
102 #[cfg(feature = "offchain")]
111 pub fn max_input<FeeExp>(
112 fee: UFix64<FeeExp>,
113 cap: UFix64<Exp>,
114 ) -> Result<UFix64<Exp>, CoreError>
115 where
116 UFix64<FeeExp>: FixExt,
117 Exp: Integer,
118 FeeExp: Integer,
119 {
120 let kept = UFix64::one()
121 .checked_sub(&fee)
122 .filter(|kept| *kept != UFix64::zero())
123 .ok_or(FeeExtraction)?;
124 max_scaled_input(cap, kept, UFix64::one()).ok_or(FeeExtraction)
125 }
126}
127
128#[derive(
130 Clone,
131 Copy,
132 InitSpace,
133 AnchorSerialize,
134 AnchorDeserialize,
135 Serialize,
136 Deserialize,
137)]
138pub struct StablecoinFees {
139 pub normal: FeePair,
140 pub mode_1: FeePair,
141}
142
143impl StablecoinFees {
144 #[must_use]
145 pub fn new(normal: FeePair, mode_1: FeePair) -> StablecoinFees {
146 StablecoinFees { normal, mode_1 }
147 }
148}
149
150#[derive(
151 Clone,
152 Copy,
153 PartialEq,
154 InitSpace,
155 AnchorDeserialize,
156 AnchorSerialize,
157 Serialize,
158 Deserialize,
159)]
160pub struct LevercoinFees {
161 pub normal: FeePair,
162 pub sell_zone_1: FeePair,
163 pub sell_zone_2: FeePair,
164}
165
166impl FeeController for LevercoinFees {
167 fn mint_fee(&self, mode: RebalanceMode) -> Result<UFix64<N4>, CoreError> {
169 match mode {
170 Neutral | BuyZone1 | BuyZone2 => self.normal.mint(),
171 SellZone1 => self.sell_zone_1.mint(),
172 SellZone2 => self.sell_zone_2.mint(),
173 Depeg => Err(NoValidLevercoinMintFee),
174 }
175 }
176
177 fn redeem_fee(&self, mode: RebalanceMode) -> Result<UFix64<N4>, CoreError> {
179 match mode {
180 Neutral | BuyZone1 | BuyZone2 => self.normal.redeem(),
181 SellZone1 => self.sell_zone_1.redeem(),
182 SellZone2 => self.sell_zone_2.redeem(),
183 Depeg => Err(NoValidLevercoinRedeemFee),
184 }
185 }
186
187 fn validate(self) -> Result<LevercoinFees, CoreError> {
189 self.normal.validate()?;
190 self.sell_zone_1.validate()?;
191 self.sell_zone_2.validate()?;
192 Ok(self)
193 }
194}
195
196impl LevercoinFees {
197 #[must_use]
198 pub fn new(
199 normal: FeePair,
200 sell_zone_1: FeePair,
201 sell_zone_2: FeePair,
202 ) -> LevercoinFees {
203 LevercoinFees {
204 normal,
205 sell_zone_1,
206 sell_zone_2,
207 }
208 }
209
210 pub fn convert_to_stablecoin_fee(
212 &self,
213 mode: RebalanceMode,
214 ) -> Result<UFix64<N4>, CoreError> {
215 match mode {
216 Neutral | BuyZone1 | BuyZone2 => self.normal.redeem(),
217 SellZone1 => self.sell_zone_1.redeem(),
218 SellZone2 | Depeg => Err(NoValidSwapFee),
219 }
220 }
221
222 pub fn convert_from_stablecoin_fee(
224 &self,
225 mode: RebalanceMode,
226 ) -> Result<UFix64<N4>, CoreError> {
227 match mode {
228 Neutral | BuyZone1 | BuyZone2 => self.normal.mint(),
229 SellZone1 => self.sell_zone_1.mint(),
230 SellZone2 => self.sell_zone_2.mint(),
231 Depeg => Err(NoValidSwapFee),
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn fee_extraction() -> Result<(), CoreError> {
242 let fee = UFix64::<N4>::new(50);
243 let amount = UFix64::<N9>::new(69_618_816_010);
244 let out = FeeExtract::new(fee, amount)?;
245 assert_eq!(out.fees_extracted, UFix64::new(348_094_081));
246 assert_eq!(out.amount_remaining, UFix64::new(69_270_721_929));
247 Ok(())
248 }
249
250 #[test]
251 fn fee_extraction_underflow() {
252 let fee = UFix64::<N4>::new(10001);
253 let amount = UFix64::<N9>::new(69_618_816_010);
254 let out = FeeExtract::new(fee, amount);
255 assert_eq!(out.err(), Some(FeeExtraction));
256 }
257}
258
259#[cfg(kani)]
260mod proofs {
261 use fix::prelude::*;
262
263 use crate::fees::controller::FeeExtract;
264 use crate::kani_generators::{narrow_ufix64, tolerance};
265
266 #[kani::proof]
268 fn fee_extract_conservation() {
269 let fee = tolerance();
270 let amount_in: UFix64<N6> = narrow_ufix64();
271 let extract = FeeExtract::split(fee, amount_in);
272 assert!(extract.is_none_or(|e| {
273 e.fees_extracted.checked_add(&e.amount_remaining) == Some(amount_in)
274 }));
275 }
276}