solend_sdk/state/
obligation.rs

1use super::*;
2use crate::{
3    error::LendingError,
4    math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
5};
6use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
7use solana_program::{
8    clock::Slot,
9    entrypoint::ProgramResult,
10    msg,
11    program_error::ProgramError,
12    program_pack::{IsInitialized, Pack, Sealed},
13    pubkey::{Pubkey, PUBKEY_BYTES},
14};
15use std::{
16    cmp::Ordering,
17    convert::{TryFrom, TryInto},
18};
19
20/// Max number of collateral and liquidity reserve accounts combined for an obligation
21pub const MAX_OBLIGATION_RESERVES: usize = 10;
22
23/// Lending market obligation state
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct Obligation {
26    /// Version of the struct
27    pub version: u8,
28    /// Last update to collateral, liquidity, or their market values
29    pub last_update: LastUpdate,
30    /// Lending market address
31    pub lending_market: Pubkey,
32    /// Owner authority which can borrow liquidity
33    pub owner: Pubkey,
34    /// Deposited collateral for the obligation, unique by deposit reserve address
35    pub deposits: Vec<ObligationCollateral>,
36    /// Borrowed liquidity for the obligation, unique by borrow reserve address
37    pub borrows: Vec<ObligationLiquidity>,
38    /// Market value of deposits
39    pub deposited_value: Decimal,
40    /// Market value of borrows
41    pub borrowed_value: Decimal,
42    /// The maximum borrow value at the weighted average loan to value ratio
43    pub allowed_borrow_value: Decimal,
44    /// The dangerous borrow value at the weighted average liquidation threshold
45    pub unhealthy_borrow_value: Decimal,
46}
47
48impl Obligation {
49    /// Create a new obligation
50    pub fn new(params: InitObligationParams) -> Self {
51        let mut obligation = Self::default();
52        Self::init(&mut obligation, params);
53        obligation
54    }
55
56    /// Initialize an obligation
57    pub fn init(&mut self, params: InitObligationParams) {
58        self.version = PROGRAM_VERSION;
59        self.last_update = LastUpdate::new(params.current_slot);
60        self.lending_market = params.lending_market;
61        self.owner = params.owner;
62        self.deposits = params.deposits;
63        self.borrows = params.borrows;
64    }
65
66    /// Calculate the current ratio of borrowed value to deposited value
67    pub fn loan_to_value(&self) -> Result<Decimal, ProgramError> {
68        self.borrowed_value.try_div(self.deposited_value)
69    }
70
71    /// Repay liquidity and remove it from borrows if zeroed out
72    pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult {
73        let liquidity = &mut self.borrows[liquidity_index];
74        if settle_amount == liquidity.borrowed_amount_wads {
75            self.borrows.remove(liquidity_index);
76        } else {
77            liquidity.repay(settle_amount)?;
78        }
79        Ok(())
80    }
81
82    /// Withdraw collateral and remove it from deposits if zeroed out
83    pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult {
84        let collateral = &mut self.deposits[collateral_index];
85        if withdraw_amount == collateral.deposited_amount {
86            self.deposits.remove(collateral_index);
87        } else {
88            collateral.withdraw(withdraw_amount)?;
89        }
90        Ok(())
91    }
92
93    /// Calculate the maximum collateral value that can be withdrawn
94    pub fn max_withdraw_value(
95        &self,
96        withdraw_collateral_ltv: Rate,
97    ) -> Result<Decimal, ProgramError> {
98        if self.allowed_borrow_value <= self.borrowed_value {
99            return Ok(Decimal::zero());
100        }
101        if withdraw_collateral_ltv == Rate::zero() {
102            return Ok(self.deposited_value);
103        }
104        self.allowed_borrow_value
105            .try_sub(self.borrowed_value)?
106            .try_div(withdraw_collateral_ltv)
107    }
108
109    /// Calculate the maximum liquidity value that can be borrowed
110    pub fn remaining_borrow_value(&self) -> Result<Decimal, ProgramError> {
111        self.allowed_borrow_value.try_sub(self.borrowed_value)
112    }
113
114    /// Calculate the maximum liquidation amount for a given liquidity
115    pub fn max_liquidation_amount(
116        &self,
117        liquidity: &ObligationLiquidity,
118    ) -> Result<Decimal, ProgramError> {
119        let max_liquidation_value = self
120            .borrowed_value
121            .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
122            .min(liquidity.market_value)
123            .min(Decimal::from(MAX_LIQUIDATABLE_VALUE_AT_ONCE));
124
125        let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?;
126        liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct)
127    }
128
129    /// Find collateral by deposit reserve
130    pub fn find_collateral_in_deposits(
131        &self,
132        deposit_reserve: Pubkey,
133    ) -> Result<(&ObligationCollateral, usize), ProgramError> {
134        if self.deposits.is_empty() {
135            msg!("Obligation has no deposits");
136            return Err(LendingError::ObligationDepositsEmpty.into());
137        }
138        let collateral_index = self
139            ._find_collateral_index_in_deposits(deposit_reserve)
140            .ok_or(LendingError::InvalidObligationCollateral)?;
141        Ok((&self.deposits[collateral_index], collateral_index))
142    }
143
144    /// Find or add collateral by deposit reserve
145    pub fn find_or_add_collateral_to_deposits(
146        &mut self,
147        deposit_reserve: Pubkey,
148    ) -> Result<&mut ObligationCollateral, ProgramError> {
149        if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) {
150            return Ok(&mut self.deposits[collateral_index]);
151        }
152        if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
153            msg!(
154                "Obligation cannot have more than {} deposits and borrows combined",
155                MAX_OBLIGATION_RESERVES
156            );
157            return Err(LendingError::ObligationReserveLimit.into());
158        }
159        let collateral = ObligationCollateral::new(deposit_reserve);
160        self.deposits.push(collateral);
161        Ok(self.deposits.last_mut().unwrap())
162    }
163
164    fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option<usize> {
165        self.deposits
166            .iter()
167            .position(|collateral| collateral.deposit_reserve == deposit_reserve)
168    }
169
170    /// Find liquidity by borrow reserve
171    pub fn find_liquidity_in_borrows(
172        &self,
173        borrow_reserve: Pubkey,
174    ) -> Result<(&ObligationLiquidity, usize), ProgramError> {
175        if self.borrows.is_empty() {
176            msg!("Obligation has no borrows");
177            return Err(LendingError::ObligationBorrowsEmpty.into());
178        }
179        let liquidity_index = self
180            ._find_liquidity_index_in_borrows(borrow_reserve)
181            .ok_or(LendingError::InvalidObligationLiquidity)?;
182        Ok((&self.borrows[liquidity_index], liquidity_index))
183    }
184
185    /// Find liquidity by borrow reserve mut
186    pub fn find_liquidity_in_borrows_mut(
187        &mut self,
188        borrow_reserve: Pubkey,
189    ) -> Result<(&mut ObligationLiquidity, usize), ProgramError> {
190        if self.borrows.is_empty() {
191            msg!("Obligation has no borrows");
192            return Err(LendingError::ObligationBorrowsEmpty.into());
193        }
194        let liquidity_index = self
195            ._find_liquidity_index_in_borrows(borrow_reserve)
196            .ok_or(LendingError::InvalidObligationLiquidity)?;
197        Ok((&mut self.borrows[liquidity_index], liquidity_index))
198    }
199
200    /// Find or add liquidity by borrow reserve
201    pub fn find_or_add_liquidity_to_borrows(
202        &mut self,
203        borrow_reserve: Pubkey,
204        cumulative_borrow_rate_wads: Decimal,
205    ) -> Result<&mut ObligationLiquidity, ProgramError> {
206        if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) {
207            return Ok(&mut self.borrows[liquidity_index]);
208        }
209        if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
210            msg!(
211                "Obligation cannot have more than {} deposits and borrows combined",
212                MAX_OBLIGATION_RESERVES
213            );
214            return Err(LendingError::ObligationReserveLimit.into());
215        }
216        let liquidity = ObligationLiquidity::new(borrow_reserve, cumulative_borrow_rate_wads);
217        self.borrows.push(liquidity);
218        Ok(self.borrows.last_mut().unwrap())
219    }
220
221    fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option<usize> {
222        self.borrows
223            .iter()
224            .position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
225    }
226}
227
228/// Initialize an obligation
229pub struct InitObligationParams {
230    /// Last update to collateral, liquidity, or their market values
231    pub current_slot: Slot,
232    /// Lending market address
233    pub lending_market: Pubkey,
234    /// Owner authority which can borrow liquidity
235    pub owner: Pubkey,
236    /// Deposited collateral for the obligation, unique by deposit reserve address
237    pub deposits: Vec<ObligationCollateral>,
238    /// Borrowed liquidity for the obligation, unique by borrow reserve address
239    pub borrows: Vec<ObligationLiquidity>,
240}
241
242impl Sealed for Obligation {}
243impl IsInitialized for Obligation {
244    fn is_initialized(&self) -> bool {
245        self.version != UNINITIALIZED_VERSION
246    }
247}
248
249/// Obligation collateral state
250#[derive(Clone, Debug, Default, PartialEq, Eq)]
251pub struct ObligationCollateral {
252    /// Reserve collateral is deposited to
253    pub deposit_reserve: Pubkey,
254    /// Amount of collateral deposited
255    pub deposited_amount: u64,
256    /// Collateral market value in quote currency
257    pub market_value: Decimal,
258}
259
260impl ObligationCollateral {
261    /// Create new obligation collateral
262    pub fn new(deposit_reserve: Pubkey) -> Self {
263        Self {
264            deposit_reserve,
265            deposited_amount: 0,
266            market_value: Decimal::zero(),
267        }
268    }
269
270    /// Increase deposited collateral
271    pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult {
272        self.deposited_amount = self
273            .deposited_amount
274            .checked_add(collateral_amount)
275            .ok_or(LendingError::MathOverflow)?;
276        Ok(())
277    }
278
279    /// Decrease deposited collateral
280    pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult {
281        self.deposited_amount = self
282            .deposited_amount
283            .checked_sub(collateral_amount)
284            .ok_or(LendingError::MathOverflow)?;
285        Ok(())
286    }
287}
288
289/// Obligation liquidity state
290#[derive(Clone, Debug, Default, PartialEq, Eq)]
291pub struct ObligationLiquidity {
292    /// Reserve liquidity is borrowed from
293    pub borrow_reserve: Pubkey,
294    /// Borrow rate used for calculating interest
295    pub cumulative_borrow_rate_wads: Decimal,
296    /// Amount of liquidity borrowed plus interest
297    pub borrowed_amount_wads: Decimal,
298    /// Liquidity market value in quote currency
299    pub market_value: Decimal,
300}
301
302impl ObligationLiquidity {
303    /// Create new obligation liquidity
304    pub fn new(borrow_reserve: Pubkey, cumulative_borrow_rate_wads: Decimal) -> Self {
305        Self {
306            borrow_reserve,
307            cumulative_borrow_rate_wads,
308            borrowed_amount_wads: Decimal::zero(),
309            market_value: Decimal::zero(),
310        }
311    }
312
313    /// Decrease borrowed liquidity
314    pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult {
315        self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
316        Ok(())
317    }
318
319    /// Increase borrowed liquidity
320    pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult {
321        self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?;
322        Ok(())
323    }
324
325    /// Accrue interest
326    pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult {
327        match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) {
328            Ordering::Less => {
329                msg!("Interest rate cannot be negative");
330                return Err(LendingError::NegativeInterestRate.into());
331            }
332            Ordering::Equal => {}
333            Ordering::Greater => {
334                let compounded_interest_rate: Rate = cumulative_borrow_rate_wads
335                    .try_div(self.cumulative_borrow_rate_wads)?
336                    .try_into()?;
337
338                self.borrowed_amount_wads = self
339                    .borrowed_amount_wads
340                    .try_mul(compounded_interest_rate)?;
341                self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads;
342            }
343        }
344
345        Ok(())
346    }
347}
348
349const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 8 + 16 + 32
350const OBLIGATION_LIQUIDITY_LEN: usize = 112; // 32 + 16 + 16 + 16 + 32
351const OBLIGATION_LEN: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 64 + 1 + 1 + (88 * 1) + (112 * 9)
352                                    // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca
353impl Pack for Obligation {
354    const LEN: usize = OBLIGATION_LEN;
355
356    fn pack_into_slice(&self, dst: &mut [u8]) {
357        let output = array_mut_ref![dst, 0, OBLIGATION_LEN];
358        #[allow(clippy::ptr_offset_with_cast)]
359        let (
360            version,
361            last_update_slot,
362            last_update_stale,
363            lending_market,
364            owner,
365            deposited_value,
366            borrowed_value,
367            allowed_borrow_value,
368            unhealthy_borrow_value,
369            _padding,
370            deposits_len,
371            borrows_len,
372            data_flat,
373        ) = mut_array_refs![
374            output,
375            1,
376            8,
377            1,
378            PUBKEY_BYTES,
379            PUBKEY_BYTES,
380            16,
381            16,
382            16,
383            16,
384            64,
385            1,
386            1,
387            OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
388        ];
389
390        // obligation
391        *version = self.version.to_le_bytes();
392        *last_update_slot = self.last_update.slot.to_le_bytes();
393        pack_bool(self.last_update.stale, last_update_stale);
394        lending_market.copy_from_slice(self.lending_market.as_ref());
395        owner.copy_from_slice(self.owner.as_ref());
396        pack_decimal(self.deposited_value, deposited_value);
397        pack_decimal(self.borrowed_value, borrowed_value);
398        pack_decimal(self.allowed_borrow_value, allowed_borrow_value);
399        pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value);
400        *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes();
401        *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes();
402
403        let mut offset = 0;
404
405        // deposits
406        for collateral in &self.deposits {
407            let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
408            #[allow(clippy::ptr_offset_with_cast)]
409            let (deposit_reserve, deposited_amount, market_value, _padding_deposit) =
410                mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32];
411            deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref());
412            *deposited_amount = collateral.deposited_amount.to_le_bytes();
413            pack_decimal(collateral.market_value, market_value);
414            offset += OBLIGATION_COLLATERAL_LEN;
415        }
416
417        // borrows
418        for liquidity in &self.borrows {
419            let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
420            #[allow(clippy::ptr_offset_with_cast)]
421            let (
422                borrow_reserve,
423                cumulative_borrow_rate_wads,
424                borrowed_amount_wads,
425                market_value,
426                _padding_borrow,
427            ) = mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16, 32];
428            borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref());
429            pack_decimal(
430                liquidity.cumulative_borrow_rate_wads,
431                cumulative_borrow_rate_wads,
432            );
433            pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads);
434            pack_decimal(liquidity.market_value, market_value);
435            offset += OBLIGATION_LIQUIDITY_LEN;
436        }
437    }
438
439    /// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html).
440    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
441        let input = array_ref![src, 0, OBLIGATION_LEN];
442        #[allow(clippy::ptr_offset_with_cast)]
443        let (
444            version,
445            last_update_slot,
446            last_update_stale,
447            lending_market,
448            owner,
449            deposited_value,
450            borrowed_value,
451            allowed_borrow_value,
452            unhealthy_borrow_value,
453            _padding,
454            deposits_len,
455            borrows_len,
456            data_flat,
457        ) = array_refs![
458            input,
459            1,
460            8,
461            1,
462            PUBKEY_BYTES,
463            PUBKEY_BYTES,
464            16,
465            16,
466            16,
467            16,
468            64,
469            1,
470            1,
471            OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
472        ];
473
474        let version = u8::from_le_bytes(*version);
475        if version > PROGRAM_VERSION {
476            msg!("Obligation version does not match lending program version");
477            return Err(ProgramError::InvalidAccountData);
478        }
479
480        let deposits_len = u8::from_le_bytes(*deposits_len);
481        let borrows_len = u8::from_le_bytes(*borrows_len);
482        let mut deposits = Vec::with_capacity(deposits_len as usize + 1);
483        let mut borrows = Vec::with_capacity(borrows_len as usize + 1);
484
485        let mut offset = 0;
486        for _ in 0..deposits_len {
487            let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
488            #[allow(clippy::ptr_offset_with_cast)]
489            let (deposit_reserve, deposited_amount, market_value, _padding_deposit) =
490                array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32];
491            deposits.push(ObligationCollateral {
492                deposit_reserve: Pubkey::new(deposit_reserve),
493                deposited_amount: u64::from_le_bytes(*deposited_amount),
494                market_value: unpack_decimal(market_value),
495            });
496            offset += OBLIGATION_COLLATERAL_LEN;
497        }
498        for _ in 0..borrows_len {
499            let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
500            #[allow(clippy::ptr_offset_with_cast)]
501            let (
502                borrow_reserve,
503                cumulative_borrow_rate_wads,
504                borrowed_amount_wads,
505                market_value,
506                _padding_borrow,
507            ) = array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16, 32];
508            borrows.push(ObligationLiquidity {
509                borrow_reserve: Pubkey::new(borrow_reserve),
510                cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads),
511                borrowed_amount_wads: unpack_decimal(borrowed_amount_wads),
512                market_value: unpack_decimal(market_value),
513            });
514            offset += OBLIGATION_LIQUIDITY_LEN;
515        }
516
517        Ok(Self {
518            version,
519            last_update: LastUpdate {
520                slot: u64::from_le_bytes(*last_update_slot),
521                stale: unpack_bool(last_update_stale)?,
522            },
523            lending_market: Pubkey::new_from_array(*lending_market),
524            owner: Pubkey::new_from_array(*owner),
525            deposits,
526            borrows,
527            deposited_value: unpack_decimal(deposited_value),
528            borrowed_value: unpack_decimal(borrowed_value),
529            allowed_borrow_value: unpack_decimal(allowed_borrow_value),
530            unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value),
531        })
532    }
533}
534
535#[cfg(test)]
536mod test {
537    use super::*;
538    use crate::math::TryAdd;
539    use proptest::prelude::*;
540
541    const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000%
542
543    #[test]
544    fn obligation_accrue_interest_failure() {
545        assert_eq!(
546            ObligationLiquidity {
547                cumulative_borrow_rate_wads: Decimal::zero(),
548                ..ObligationLiquidity::default()
549            }
550            .accrue_interest(Decimal::one()),
551            Err(LendingError::MathOverflow.into())
552        );
553
554        assert_eq!(
555            ObligationLiquidity {
556                cumulative_borrow_rate_wads: Decimal::from(2u64),
557                ..ObligationLiquidity::default()
558            }
559            .accrue_interest(Decimal::one()),
560            Err(LendingError::NegativeInterestRate.into())
561        );
562
563        assert_eq!(
564            ObligationLiquidity {
565                cumulative_borrow_rate_wads: Decimal::one(),
566                borrowed_amount_wads: Decimal::from(u64::MAX),
567                ..ObligationLiquidity::default()
568            }
569            .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)),
570            Err(LendingError::MathOverflow.into())
571        );
572    }
573
574    // Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1
575    prop_compose! {
576        fn cumulative_rates()(rate in 1..=u128::MAX)(
577            current_rate in Just(rate),
578            max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128),
579        ) -> (u128, u128) {
580            (current_rate, max_new_rate)
581        }
582    }
583
584    const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128;
585
586    // Creates liquidity amounts (repay, borrow) where repay < borrow
587    prop_compose! {
588        fn repay_partial_amounts()(amount in 1..=u64::MAX)(
589            repay_amount in Just(WAD as u128 * amount as u128),
590            borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
591        ) -> (u128, u128) {
592            (repay_amount, borrowed_amount)
593        }
594    }
595
596    // Creates liquidity amounts (repay, borrow) where repay >= borrow
597    prop_compose! {
598        fn repay_full_amounts()(amount in 1..=u64::MAX)(
599            repay_amount in Just(WAD as u128 * amount as u128),
600        ) -> (u128, u128) {
601            (repay_amount, repay_amount)
602        }
603    }
604
605    proptest! {
606        #[test]
607        fn repay_partial(
608            (repay_amount, borrowed_amount) in repay_partial_amounts(),
609        ) {
610            let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
611            let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
612            let mut obligation = Obligation {
613                borrows: vec![ObligationLiquidity {
614                    borrowed_amount_wads,
615                    ..ObligationLiquidity::default()
616                }],
617                ..Obligation::default()
618            };
619
620            obligation.repay(repay_amount_wads, 0)?;
621            assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
622            assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
623        }
624
625        #[test]
626        fn repay_full(
627            (repay_amount, borrowed_amount) in repay_full_amounts(),
628        ) {
629            let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
630            let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
631            let mut obligation = Obligation {
632                borrows: vec![ObligationLiquidity {
633                    borrowed_amount_wads,
634                    ..ObligationLiquidity::default()
635                }],
636                ..Obligation::default()
637            };
638
639            obligation.repay(repay_amount_wads, 0)?;
640            assert_eq!(obligation.borrows.len(), 0);
641        }
642
643        #[test]
644        fn accrue_interest(
645            (current_borrow_rate, new_borrow_rate) in cumulative_rates(),
646            borrowed_amount in 0..=u64::MAX,
647        ) {
648            let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?;
649            let borrowed_amount_wads = Decimal::from(borrowed_amount);
650            let mut liquidity = ObligationLiquidity {
651                cumulative_borrow_rate_wads,
652                borrowed_amount_wads,
653                ..ObligationLiquidity::default()
654            };
655
656            let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?;
657            liquidity.accrue_interest(next_cumulative_borrow_rate)?;
658
659            if next_cumulative_borrow_rate > cumulative_borrow_rate_wads {
660                assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads);
661            } else {
662                assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads);
663            }
664        }
665    }
666
667    #[test]
668    fn max_liquidation_amount_normal() {
669        let obligation_liquidity = ObligationLiquidity {
670            borrowed_amount_wads: Decimal::from(50u64),
671            market_value: Decimal::from(100u64),
672            ..ObligationLiquidity::default()
673        };
674
675        let obligation = Obligation {
676            deposited_value: Decimal::from(100u64),
677            borrowed_value: Decimal::from(100u64),
678            borrows: vec![obligation_liquidity.clone()],
679            ..Obligation::default()
680        };
681
682        let expected_collateral = Decimal::from(50u64)
683            .try_mul(Decimal::from(LIQUIDATION_CLOSE_FACTOR as u64))
684            .unwrap()
685            .try_div(100)
686            .unwrap();
687
688        assert_eq!(
689            obligation
690                .max_liquidation_amount(&obligation_liquidity)
691                .unwrap(),
692            expected_collateral
693        );
694    }
695
696    #[test]
697    fn max_liquidation_amount_low_liquidity() {
698        let obligation_liquidity = ObligationLiquidity {
699            borrowed_amount_wads: Decimal::from(100u64),
700            market_value: Decimal::from(1u64),
701            ..ObligationLiquidity::default()
702        };
703
704        let obligation = Obligation {
705            deposited_value: Decimal::from(100u64),
706            borrowed_value: Decimal::from(100u64),
707            borrows: vec![obligation_liquidity.clone()],
708            ..Obligation::default()
709        };
710
711        assert_eq!(
712            obligation
713                .max_liquidation_amount(&obligation_liquidity)
714                .unwrap(),
715            Decimal::from(100u64)
716        );
717    }
718
719    #[test]
720    fn max_liquidation_amount_big_whale() {
721        let obligation_liquidity = ObligationLiquidity {
722            borrowed_amount_wads: Decimal::from(1_000_000_000u64),
723            market_value: Decimal::from(1_000_000_000u64),
724            ..ObligationLiquidity::default()
725        };
726
727        let obligation = Obligation {
728            deposited_value: Decimal::from(1_000_000_000u64),
729            borrowed_value: Decimal::from(1_000_000_000u64),
730            borrows: vec![obligation_liquidity.clone()],
731            ..Obligation::default()
732        };
733
734        assert_eq!(
735            obligation
736                .max_liquidation_amount(&obligation_liquidity)
737                .unwrap(),
738            Decimal::from(MAX_LIQUIDATABLE_VALUE_AT_ONCE)
739        );
740    }
741}