Skip to main content

spl_token_lending/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        let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?;
124        liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct)
125    }
126
127    /// Find collateral by deposit reserve
128    pub fn find_collateral_in_deposits(
129        &self,
130        deposit_reserve: Pubkey,
131    ) -> Result<(&ObligationCollateral, usize), ProgramError> {
132        if self.deposits.is_empty() {
133            msg!("Obligation has no deposits");
134            return Err(LendingError::ObligationDepositsEmpty.into());
135        }
136        let collateral_index = self
137            ._find_collateral_index_in_deposits(deposit_reserve)
138            .ok_or(LendingError::InvalidObligationCollateral)?;
139        Ok((&self.deposits[collateral_index], collateral_index))
140    }
141
142    /// Find or add collateral by deposit reserve
143    pub fn find_or_add_collateral_to_deposits(
144        &mut self,
145        deposit_reserve: Pubkey,
146    ) -> Result<&mut ObligationCollateral, ProgramError> {
147        if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) {
148            return Ok(&mut self.deposits[collateral_index]);
149        }
150        if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
151            msg!(
152                "Obligation cannot have more than {} deposits and borrows combined",
153                MAX_OBLIGATION_RESERVES
154            );
155            return Err(LendingError::ObligationReserveLimit.into());
156        }
157        let collateral = ObligationCollateral::new(deposit_reserve);
158        self.deposits.push(collateral);
159        Ok(self.deposits.last_mut().unwrap())
160    }
161
162    fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option<usize> {
163        self.deposits
164            .iter()
165            .position(|collateral| collateral.deposit_reserve == deposit_reserve)
166    }
167
168    /// Find liquidity by borrow reserve
169    pub fn find_liquidity_in_borrows(
170        &self,
171        borrow_reserve: Pubkey,
172    ) -> Result<(&ObligationLiquidity, usize), ProgramError> {
173        if self.borrows.is_empty() {
174            msg!("Obligation has no borrows");
175            return Err(LendingError::ObligationBorrowsEmpty.into());
176        }
177        let liquidity_index = self
178            ._find_liquidity_index_in_borrows(borrow_reserve)
179            .ok_or(LendingError::InvalidObligationLiquidity)?;
180        Ok((&self.borrows[liquidity_index], liquidity_index))
181    }
182
183    /// Find or add liquidity by borrow reserve
184    pub fn find_or_add_liquidity_to_borrows(
185        &mut self,
186        borrow_reserve: Pubkey,
187    ) -> Result<&mut ObligationLiquidity, ProgramError> {
188        if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) {
189            return Ok(&mut self.borrows[liquidity_index]);
190        }
191        if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
192            msg!(
193                "Obligation cannot have more than {} deposits and borrows combined",
194                MAX_OBLIGATION_RESERVES
195            );
196            return Err(LendingError::ObligationReserveLimit.into());
197        }
198        let liquidity = ObligationLiquidity::new(borrow_reserve);
199        self.borrows.push(liquidity);
200        Ok(self.borrows.last_mut().unwrap())
201    }
202
203    fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option<usize> {
204        self.borrows
205            .iter()
206            .position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
207    }
208}
209
210/// Initialize an obligation
211pub struct InitObligationParams {
212    /// Last update to collateral, liquidity, or their market values
213    pub current_slot: Slot,
214    /// Lending market address
215    pub lending_market: Pubkey,
216    /// Owner authority which can borrow liquidity
217    pub owner: Pubkey,
218    /// Deposited collateral for the obligation, unique by deposit reserve address
219    pub deposits: Vec<ObligationCollateral>,
220    /// Borrowed liquidity for the obligation, unique by borrow reserve address
221    pub borrows: Vec<ObligationLiquidity>,
222}
223
224impl Sealed for Obligation {}
225impl IsInitialized for Obligation {
226    fn is_initialized(&self) -> bool {
227        self.version != UNINITIALIZED_VERSION
228    }
229}
230
231/// Obligation collateral state
232#[derive(Clone, Debug, Default, PartialEq)]
233pub struct ObligationCollateral {
234    /// Reserve collateral is deposited to
235    pub deposit_reserve: Pubkey,
236    /// Amount of collateral deposited
237    pub deposited_amount: u64,
238    /// Collateral market value in quote currency
239    pub market_value: Decimal,
240}
241
242impl ObligationCollateral {
243    /// Create new obligation collateral
244    pub fn new(deposit_reserve: Pubkey) -> Self {
245        Self {
246            deposit_reserve,
247            deposited_amount: 0,
248            market_value: Decimal::zero(),
249        }
250    }
251
252    /// Increase deposited collateral
253    pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult {
254        self.deposited_amount = self
255            .deposited_amount
256            .checked_add(collateral_amount)
257            .ok_or(LendingError::MathOverflow)?;
258        Ok(())
259    }
260
261    /// Decrease deposited collateral
262    pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult {
263        self.deposited_amount = self
264            .deposited_amount
265            .checked_sub(collateral_amount)
266            .ok_or(LendingError::MathOverflow)?;
267        Ok(())
268    }
269}
270
271/// Obligation liquidity state
272#[derive(Clone, Debug, Default, PartialEq)]
273pub struct ObligationLiquidity {
274    /// Reserve liquidity is borrowed from
275    pub borrow_reserve: Pubkey,
276    /// Borrow rate used for calculating interest
277    pub cumulative_borrow_rate_wads: Decimal,
278    /// Amount of liquidity borrowed plus interest
279    pub borrowed_amount_wads: Decimal,
280    /// Liquidity market value in quote currency
281    pub market_value: Decimal,
282}
283
284impl ObligationLiquidity {
285    /// Create new obligation liquidity
286    pub fn new(borrow_reserve: Pubkey) -> Self {
287        Self {
288            borrow_reserve,
289            cumulative_borrow_rate_wads: Decimal::one(),
290            borrowed_amount_wads: Decimal::zero(),
291            market_value: Decimal::zero(),
292        }
293    }
294
295    /// Decrease borrowed liquidity
296    pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult {
297        self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
298        Ok(())
299    }
300
301    /// Increase borrowed liquidity
302    pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult {
303        self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?;
304        Ok(())
305    }
306
307    /// Accrue interest
308    pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult {
309        match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) {
310            Ordering::Less => {
311                msg!("Interest rate cannot be negative");
312                return Err(LendingError::NegativeInterestRate.into());
313            }
314            Ordering::Equal => {}
315            Ordering::Greater => {
316                let compounded_interest_rate: Rate = cumulative_borrow_rate_wads
317                    .try_div(self.cumulative_borrow_rate_wads)?
318                    .try_into()?;
319
320                self.borrowed_amount_wads = self
321                    .borrowed_amount_wads
322                    .try_mul(compounded_interest_rate)?;
323                self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads;
324            }
325        }
326
327        Ok(())
328    }
329}
330
331const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16
332const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16
333const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9)
334                                   // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca
335impl Pack for Obligation {
336    const LEN: usize = OBLIGATION_LEN;
337
338    fn pack_into_slice(&self, dst: &mut [u8]) {
339        let output = array_mut_ref![dst, 0, OBLIGATION_LEN];
340        #[allow(clippy::ptr_offset_with_cast)]
341        let (
342            version,
343            last_update_slot,
344            last_update_stale,
345            lending_market,
346            owner,
347            deposited_value,
348            borrowed_value,
349            allowed_borrow_value,
350            unhealthy_borrow_value,
351            deposits_len,
352            borrows_len,
353            data_flat,
354        ) = mut_array_refs![
355            output,
356            1,
357            8,
358            1,
359            PUBKEY_BYTES,
360            PUBKEY_BYTES,
361            16,
362            16,
363            16,
364            16,
365            1,
366            1,
367            OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
368        ];
369
370        // obligation
371        *version = self.version.to_le_bytes();
372        *last_update_slot = self.last_update.slot.to_le_bytes();
373        pack_bool(self.last_update.stale, last_update_stale);
374        lending_market.copy_from_slice(self.lending_market.as_ref());
375        owner.copy_from_slice(self.owner.as_ref());
376        pack_decimal(self.deposited_value, deposited_value);
377        pack_decimal(self.borrowed_value, borrowed_value);
378        pack_decimal(self.allowed_borrow_value, allowed_borrow_value);
379        pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value);
380        *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes();
381        *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes();
382
383        let mut offset = 0;
384
385        // deposits
386        for collateral in &self.deposits {
387            let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
388            #[allow(clippy::ptr_offset_with_cast)]
389            let (deposit_reserve, deposited_amount, market_value) =
390                mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
391            deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref());
392            *deposited_amount = collateral.deposited_amount.to_le_bytes();
393            pack_decimal(collateral.market_value, market_value);
394            offset += OBLIGATION_COLLATERAL_LEN;
395        }
396
397        // borrows
398        for liquidity in &self.borrows {
399            let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
400            #[allow(clippy::ptr_offset_with_cast)]
401            let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
402                mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
403            borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref());
404            pack_decimal(
405                liquidity.cumulative_borrow_rate_wads,
406                cumulative_borrow_rate_wads,
407            );
408            pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads);
409            pack_decimal(liquidity.market_value, market_value);
410            offset += OBLIGATION_LIQUIDITY_LEN;
411        }
412    }
413
414    /// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html).
415    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
416        let input = array_ref![src, 0, OBLIGATION_LEN];
417        #[allow(clippy::ptr_offset_with_cast)]
418        let (
419            version,
420            last_update_slot,
421            last_update_stale,
422            lending_market,
423            owner,
424            deposited_value,
425            borrowed_value,
426            allowed_borrow_value,
427            unhealthy_borrow_value,
428            deposits_len,
429            borrows_len,
430            data_flat,
431        ) = array_refs![
432            input,
433            1,
434            8,
435            1,
436            PUBKEY_BYTES,
437            PUBKEY_BYTES,
438            16,
439            16,
440            16,
441            16,
442            1,
443            1,
444            OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
445        ];
446
447        let version = u8::from_le_bytes(*version);
448        if version > PROGRAM_VERSION {
449            msg!("Obligation version does not match lending program version");
450            return Err(ProgramError::InvalidAccountData);
451        }
452
453        let deposits_len = u8::from_le_bytes(*deposits_len);
454        let borrows_len = u8::from_le_bytes(*borrows_len);
455        let mut deposits = Vec::with_capacity(deposits_len as usize + 1);
456        let mut borrows = Vec::with_capacity(borrows_len as usize + 1);
457
458        let mut offset = 0;
459        for _ in 0..deposits_len {
460            let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
461            #[allow(clippy::ptr_offset_with_cast)]
462            let (deposit_reserve, deposited_amount, market_value) =
463                array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
464            deposits.push(ObligationCollateral {
465                deposit_reserve: Pubkey::new(deposit_reserve),
466                deposited_amount: u64::from_le_bytes(*deposited_amount),
467                market_value: unpack_decimal(market_value),
468            });
469            offset += OBLIGATION_COLLATERAL_LEN;
470        }
471        for _ in 0..borrows_len {
472            let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
473            #[allow(clippy::ptr_offset_with_cast)]
474            let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
475                array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
476            borrows.push(ObligationLiquidity {
477                borrow_reserve: Pubkey::new(borrow_reserve),
478                cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads),
479                borrowed_amount_wads: unpack_decimal(borrowed_amount_wads),
480                market_value: unpack_decimal(market_value),
481            });
482            offset += OBLIGATION_LIQUIDITY_LEN;
483        }
484
485        Ok(Self {
486            version,
487            last_update: LastUpdate {
488                slot: u64::from_le_bytes(*last_update_slot),
489                stale: unpack_bool(last_update_stale)?,
490            },
491            lending_market: Pubkey::new_from_array(*lending_market),
492            owner: Pubkey::new_from_array(*owner),
493            deposits,
494            borrows,
495            deposited_value: unpack_decimal(deposited_value),
496            borrowed_value: unpack_decimal(borrowed_value),
497            allowed_borrow_value: unpack_decimal(allowed_borrow_value),
498            unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value),
499        })
500    }
501}
502
503#[cfg(test)]
504mod test {
505    use super::*;
506    use crate::math::TryAdd;
507    use proptest::prelude::*;
508
509    const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000%
510
511    #[test]
512    fn obligation_accrue_interest_failure() {
513        assert_eq!(
514            ObligationLiquidity {
515                cumulative_borrow_rate_wads: Decimal::zero(),
516                ..ObligationLiquidity::default()
517            }
518            .accrue_interest(Decimal::one()),
519            Err(LendingError::MathOverflow.into())
520        );
521
522        assert_eq!(
523            ObligationLiquidity {
524                cumulative_borrow_rate_wads: Decimal::from(2u64),
525                ..ObligationLiquidity::default()
526            }
527            .accrue_interest(Decimal::one()),
528            Err(LendingError::NegativeInterestRate.into())
529        );
530
531        assert_eq!(
532            ObligationLiquidity {
533                cumulative_borrow_rate_wads: Decimal::one(),
534                borrowed_amount_wads: Decimal::from(u64::MAX),
535                ..ObligationLiquidity::default()
536            }
537            .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)),
538            Err(LendingError::MathOverflow.into())
539        );
540    }
541
542    // Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1
543    prop_compose! {
544        fn cumulative_rates()(rate in 1..=u128::MAX)(
545            current_rate in Just(rate),
546            max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128),
547        ) -> (u128, u128) {
548            (current_rate, max_new_rate)
549        }
550    }
551
552    const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128;
553
554    // Creates liquidity amounts (repay, borrow) where repay < borrow
555    prop_compose! {
556        fn repay_partial_amounts()(amount in 1..=u64::MAX)(
557            repay_amount in Just(WAD as u128 * amount as u128),
558            borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
559        ) -> (u128, u128) {
560            (repay_amount, borrowed_amount)
561        }
562    }
563
564    // Creates liquidity amounts (repay, borrow) where repay >= borrow
565    prop_compose! {
566        fn repay_full_amounts()(amount in 1..=u64::MAX)(
567            repay_amount in Just(WAD as u128 * amount as u128),
568        ) -> (u128, u128) {
569            (repay_amount, repay_amount)
570        }
571    }
572
573    proptest! {
574        #[test]
575        fn repay_partial(
576            (repay_amount, borrowed_amount) in repay_partial_amounts(),
577        ) {
578            let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
579            let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
580            let mut obligation = Obligation {
581                borrows: vec![ObligationLiquidity {
582                    borrowed_amount_wads,
583                    ..ObligationLiquidity::default()
584                }],
585                ..Obligation::default()
586            };
587
588            obligation.repay(repay_amount_wads, 0)?;
589            assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
590            assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
591        }
592
593        #[test]
594        fn repay_full(
595            (repay_amount, borrowed_amount) in repay_full_amounts(),
596        ) {
597            let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
598            let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
599            let mut obligation = Obligation {
600                borrows: vec![ObligationLiquidity {
601                    borrowed_amount_wads,
602                    ..ObligationLiquidity::default()
603                }],
604                ..Obligation::default()
605            };
606
607            obligation.repay(repay_amount_wads, 0)?;
608            assert_eq!(obligation.borrows.len(), 0);
609        }
610
611        #[test]
612        fn accrue_interest(
613            (current_borrow_rate, new_borrow_rate) in cumulative_rates(),
614            borrowed_amount in 0..=u64::MAX,
615        ) {
616            let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?;
617            let borrowed_amount_wads = Decimal::from(borrowed_amount);
618            let mut liquidity = ObligationLiquidity {
619                cumulative_borrow_rate_wads,
620                borrowed_amount_wads,
621                ..ObligationLiquidity::default()
622            };
623
624            let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?;
625            liquidity.accrue_interest(next_cumulative_borrow_rate)?;
626
627            if next_cumulative_borrow_rate > cumulative_borrow_rate_wads {
628                assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads);
629            } else {
630                assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads);
631            }
632        }
633    }
634}