1pub mod msgs {
2
3    use cosmwasm_schema::{cw_serde, QueryResponses};
4
5    use cosmwasm_std::{Addr, Decimal, Uint128};
6    use cw20::Cw20ReceiveMsg;
7
8    use crate::common::AssetType;
9
10    use super::definitions::{PoolConfig, PoolData};
11
12    #[cw_serde]
13    pub struct InstantiateMsg {
14        pub config: PoolConfig,
15        pub lp_type: AssetType,
16        pub code_id_cw20: Option<u64>,
17    }
18
19    #[cw_serde]
20    pub enum ExecuteMsg {
21        Deposit(DepositMsg),
22        Receive(Cw20ReceiveMsg),
23        Borrow(BorrowMsg),
24        Repay(RepayMsg),
25        Withdraw(WithdrawMsg),
26        CloseExpiredLoan(CloseExpiredLoanMsg),
27        Swap(SwapMsg),
28        UpdateConfig(Box<UpdateConfigMsg>),
29    }
30
31    #[cw_serde]
32    #[derive(QueryResponses)]
33    pub enum QueryMsg {
34        #[returns(PoolData)]
35        PoolData {},
36        #[returns(PoolConfig)]
37        PoolConfig {},
38        #[returns(LoanInfoResponse)]
39        LoanInfo { id: u64 },
40        #[returns(CurrentInterestsResponse)]
41        CurrentInterests,
42    }
43
44    #[cw_serde]
45    pub struct MigrateMsg {}
46
47    #[cw_serde]
48    pub enum Cw20HookMsg {
49        Deposit(DepositMsg),
50        Borrow(BorrowMsg),
51        Withdraw(WithdrawMsg),
52    }
53
54    #[cw_serde]
55    pub struct DepositMsg {}
56
57    #[cw_serde]
58    pub struct BorrowMsg {
59        pub duration: u64,
60    }
61
62    #[cw_serde]
63    pub struct RepayMsg {
64        pub loan_id: u64,
65    }
66
67    #[cw_serde]
68    pub struct WithdrawMsg {}
69
70    #[cw_serde]
71    pub struct CloseExpiredLoanMsg {
72        pub loan_id: u64,
73    }
74
75    #[cw_serde]
76    pub struct SwapMsg {}
77
78    #[cw_serde]
79    pub struct UpdateConfigMsg {
80        pub config: PoolConfig,
81        pub factory: Option<String>,
82    }
83
84    #[cw_serde]
85    pub struct LoanInfoResponse {
86        pub owner: Addr,
87        pub loan_amount: Uint128,
88        pub collateral_amount: Uint128,
89        pub expiration_timestamp: u64,
90        pub duration: u64,
91    }
92
93    #[cw_serde]
94    pub struct CurrentInterestsResponse {
95        pub borrow: Decimal,
96        pub provide: Decimal,
97    }
98}
99
100pub mod definitions {
101    use cosmwasm_schema::cw_serde;
102    use cosmwasm_std::{Addr, Decimal, StdError, StdResult, Timestamp, Uint128};
103    use cw_asset::AssetInfo;
104    use rhaki_cw_plus::{math::IntoDecimal, serde_value::IntoSerdeJsonString};
105
106    use crate::{
107        common::{PriceType, YEAR_IN_SECONDS},
108        traits::AssertOwner,
109    };
110
111    #[cw_serde]
112    pub struct PoolConfig {
113        pub collateral: AssetInfo,
114        pub core: AssetInfo,
115        pub price_type: PriceType,
116        pub interest_type: InterestType,
117        pub loan_duration_type: LoanDurationType,
118        pub initial_ltv: Decimal,
119        pub allow_early_close_uncollateralized: bool,
120        pub swap_info: SwapInfo,
121        pub lp_info: AssetInfo,
122    }
123
124    impl PoolConfig {
125        pub fn validate(&self) -> StdResult<()> {
126            if self.initial_ltv > Decimal::one() {
127                return Err(StdError::generic_err(
128                    "initial_ltv must be lesser then or equal 1",
129                ));
130            }
131
132            match self.loan_duration_type {
133                LoanDurationType::Fixed(duration) => {
134                    if duration == 0 {
135                        return Err(StdError::generic_err(
136                            "loan duration must be greater than 0",
137                        ));
138                    }
139                }
140                LoanDurationType::Variable { min, max, .. } => {
141                    if min > max || max == 0 {
142                        return Err(StdError::generic_err(
143                            "variable loan min must be greater than max",
144                        ));
145                    }
146                }
147            }
148
149            self.swap_info.validate()?;
150
151            Ok(())
152        }
153    }
154
155    #[cw_serde]
156    pub enum InterestType {
157        Quadratic { a: Decimal, c: Decimal },
158    }
159
160    impl InterestType {
161        pub fn calculate_interest(&self, utilization_ratio: Decimal) -> Decimal {
163            match self {
164                Self::Quadratic { a, c } => {
165                    (utilization_ratio.pow(2) * a + c) / 100_u128.into_decimal()
166                }
167            }
168        }
169    }
170
171    impl IntoSerdeJsonString for InterestType {}
172
173    #[cw_serde]
174    pub enum LoanDurationType {
175        Fixed(u64),
176        Variable {
177            min: u64,
178            max: u64,
179            interest_multiplier_at_max: Decimal,
180        },
181    }
182
183    impl IntoSerdeJsonString for LoanDurationType {}
184
185    impl LoanDurationType {
186        pub fn assert_duration(&self, duration: u64) -> StdResult<()> {
187            match self {
188                LoanDurationType::Fixed(fix) => {
189                    if *fix != duration {
190                        return Err(StdError::generic_err("InvalidDuration"));
191                    }
192                }
193                LoanDurationType::Variable { min, max, .. } => {
194                    if duration < *min || duration > *max {
195                        return Err(StdError::generic_err("InvalidDuration"));
196                    }
197                }
198            }
199
200            Ok(())
201        }
202
203        pub fn get_multiplier(&self, duration: u64) -> Decimal {
204            match self {
205                LoanDurationType::Fixed(..) => Decimal::one(),
206                LoanDurationType::Variable {
207                    min,
208                    max,
209                    interest_multiplier_at_max,
210                } => {
211                    let ratio = Decimal::from_ratio(duration - min, max - min);
212                    Decimal::one() + (interest_multiplier_at_max - Decimal::one()) * ratio
213                }
214            }
215        }
216    }
217
218    #[cw_serde]
219    pub struct PoolData {
220        pub core_avaiable: Uint128,
221        pub core_locked: Uint128,
222        pub collateral_avaiable: Uint128,
223        pub collateral_locked: Uint128,
224        pub global_index: Decimal,
225        pub last_update: u64,
226        pub factory: Addr,
227        pub loan_counter: u64,
228    }
229
230    impl PoolData {
231        pub fn new(current_timestamp: Timestamp, factory: Addr) -> PoolData {
232            PoolData {
233                last_update: current_timestamp.seconds(),
234                core_avaiable: Uint128::zero(),
235                core_locked: Uint128::zero(),
236                collateral_avaiable: Uint128::zero(),
237                collateral_locked: Uint128::zero(),
238                global_index: Decimal::zero(),
239                factory,
240                loan_counter: 0,
241            }
242        }
243
244        pub fn total_value_in_core(&self, price_collateral_in_core: Decimal) -> Uint128 {
245            self.core_avaiable
246                + self.collateral_avaiable * price_collateral_in_core
247                + self.core_locked
248            }
254
255        pub fn utilization_ratio(&self) -> Decimal {
256            if self.core_locked + self.core_avaiable > Uint128::zero() {
257                Decimal::from_ratio(self.core_locked, self.core_locked + self.core_avaiable)
258            } else {
259                Decimal::zero()
260            }
261        }
262
263        pub fn update_global_index(&mut self, current_timestamp: &Timestamp, config: &PoolConfig) {
264            let interest = self.current_interest(config);
265
266            let year_passed = Decimal::from_ratio(
267                current_timestamp.seconds() - self.last_update,
268                YEAR_IN_SECONDS,
269            );
270
271            self.global_index += interest * year_passed;
272            self.last_update = current_timestamp.seconds()
273        }
274
275        pub fn current_interest(&self, config: &PoolConfig) -> Decimal {
276            config
277                .interest_type
278                .calculate_interest(self.utilization_ratio())
279        }
280    }
281
282    impl AssertOwner for PoolData {
283        fn get_admin(&self) -> Addr {
284            self.factory.clone()
285        }
286    }
287
288    #[cw_serde]
289    pub enum SwapInfo {
290        Disabled,
291        OnlyFromcore { fee: Decimal },
292    }
293
294    impl IntoSerdeJsonString for SwapInfo {}
295
296    impl SwapInfo {
297        pub fn assert_input(&self, input: AssetInfo, config: &PoolConfig) -> StdResult<()> {
298            match self {
299                SwapInfo::Disabled => return Err(StdError::generic_err("Swap not enable")),
300                SwapInfo::OnlyFromcore { .. } => {
301                    if input != config.core {
302                        return Err(StdError::generic_err(format!(
303                            "Wrong asset, expected: {}, received: {}",
304                            config.core, input
305                        )));
306                    }
307                }
308            }
309
310            Ok(())
311        }
312
313        pub fn validate(&self) -> StdResult<()> {
314            match self {
315                SwapInfo::Disabled => Ok(()),
316                SwapInfo::OnlyFromcore { fee } => {
317                    if *fee > Decimal::one() {
318                        return Err(StdError::generic_err(
319                            "close_position_fee must be lesser then or equal 1",
320                        ));
321                    }
322                    Ok(())
323                }
324            }
325        }
326
327        pub fn get_fee(&self) -> StdResult<Decimal> {
328            match self {
329                SwapInfo::Disabled => Err(StdError::generic_err("Swap not enable")),
330                SwapInfo::OnlyFromcore { fee } => Ok(*fee),
331            }
332        }
333    }
334}