Skip to main content

spl_token_lending/state/
reserve.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/// Percentage of an obligation that can be repaid during each liquidation call
21pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50;
22
23/// Obligation borrow amount that is small enough to close out
24pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2;
25
26/// Lending market reserve state
27#[derive(Clone, Debug, Default, PartialEq)]
28pub struct Reserve {
29    /// Version of the struct
30    pub version: u8,
31    /// Last slot when supply and rates updated
32    pub last_update: LastUpdate,
33    /// Lending market address
34    pub lending_market: Pubkey,
35    /// Reserve liquidity
36    pub liquidity: ReserveLiquidity,
37    /// Reserve collateral
38    pub collateral: ReserveCollateral,
39    /// Reserve configuration values
40    pub config: ReserveConfig,
41}
42
43impl Reserve {
44    /// Create a new reserve
45    pub fn new(params: InitReserveParams) -> Self {
46        let mut reserve = Self::default();
47        Self::init(&mut reserve, params);
48        reserve
49    }
50
51    /// Initialize a reserve
52    pub fn init(&mut self, params: InitReserveParams) {
53        self.version = PROGRAM_VERSION;
54        self.last_update = LastUpdate::new(params.current_slot);
55        self.lending_market = params.lending_market;
56        self.liquidity = params.liquidity;
57        self.collateral = params.collateral;
58        self.config = params.config;
59    }
60
61    /// Record deposited liquidity and return amount of collateral tokens to mint
62    pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result<u64, ProgramError> {
63        let collateral_amount = self
64            .collateral_exchange_rate()?
65            .liquidity_to_collateral(liquidity_amount)?;
66
67        self.liquidity.deposit(liquidity_amount)?;
68        self.collateral.mint(collateral_amount)?;
69
70        Ok(collateral_amount)
71    }
72
73    /// Record redeemed collateral and return amount of liquidity to withdraw
74    pub fn redeem_collateral(&mut self, collateral_amount: u64) -> Result<u64, ProgramError> {
75        let collateral_exchange_rate = self.collateral_exchange_rate()?;
76        let liquidity_amount =
77            collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?;
78
79        self.collateral.burn(collateral_amount)?;
80        self.liquidity.withdraw(liquidity_amount)?;
81
82        Ok(liquidity_amount)
83    }
84
85    /// Calculate the current borrow rate
86    pub fn current_borrow_rate(&self) -> Result<Rate, ProgramError> {
87        let utilization_rate = self.liquidity.utilization_rate()?;
88        let optimal_utilization_rate = Rate::from_percent(self.config.optimal_utilization_rate);
89        let low_utilization = utilization_rate < optimal_utilization_rate;
90        if low_utilization || self.config.optimal_utilization_rate == 100 {
91            let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?;
92            let min_rate = Rate::from_percent(self.config.min_borrow_rate);
93            let rate_range = Rate::from_percent(
94                self.config
95                    .optimal_borrow_rate
96                    .checked_sub(self.config.min_borrow_rate)
97                    .ok_or(LendingError::MathOverflow)?,
98            );
99
100            Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
101        } else {
102            let normalized_rate = utilization_rate
103                .try_sub(optimal_utilization_rate)?
104                .try_div(Rate::from_percent(
105                    100u8
106                        .checked_sub(self.config.optimal_utilization_rate)
107                        .ok_or(LendingError::MathOverflow)?,
108                ))?;
109            let min_rate = Rate::from_percent(self.config.optimal_borrow_rate);
110            let rate_range = Rate::from_percent(
111                self.config
112                    .max_borrow_rate
113                    .checked_sub(self.config.optimal_borrow_rate)
114                    .ok_or(LendingError::MathOverflow)?,
115            );
116
117            Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
118        }
119    }
120
121    /// Collateral exchange rate
122    pub fn collateral_exchange_rate(&self) -> Result<CollateralExchangeRate, ProgramError> {
123        let total_liquidity = self.liquidity.total_supply()?;
124        self.collateral.exchange_rate(total_liquidity)
125    }
126
127    /// Update borrow rate and accrue interest
128    pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult {
129        let slots_elapsed = self.last_update.slots_elapsed(current_slot)?;
130        if slots_elapsed > 0 {
131            let current_borrow_rate = self.current_borrow_rate()?;
132            self.liquidity
133                .compound_interest(current_borrow_rate, slots_elapsed)?;
134        }
135        Ok(())
136    }
137
138    /// Borrow liquidity up to a maximum market value
139    pub fn calculate_borrow(
140        &self,
141        amount_to_borrow: u64,
142        max_borrow_value: Decimal,
143    ) -> Result<CalculateBorrowResult, ProgramError> {
144        // @TODO: add lookup table https://git.io/JOCYq
145        let decimals = 10u64
146            .checked_pow(self.liquidity.mint_decimals as u32)
147            .ok_or(LendingError::MathOverflow)?;
148        if amount_to_borrow == u64::MAX {
149            let borrow_amount = max_borrow_value
150                .try_mul(decimals)?
151                .try_div(self.liquidity.market_price)?
152                .min(self.liquidity.available_amount.into());
153            let (borrow_fee, host_fee) = self
154                .config
155                .fees
156                .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?;
157            let receive_amount = borrow_amount
158                .try_floor_u64()?
159                .checked_sub(borrow_fee)
160                .ok_or(LendingError::MathOverflow)?;
161
162            Ok(CalculateBorrowResult {
163                borrow_amount,
164                receive_amount,
165                borrow_fee,
166                host_fee,
167            })
168        } else {
169            let receive_amount = amount_to_borrow;
170            let borrow_amount = Decimal::from(receive_amount);
171            let (borrow_fee, host_fee) = self
172                .config
173                .fees
174                .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?;
175
176            let borrow_amount = borrow_amount.try_add(borrow_fee.into())?;
177            let borrow_value = borrow_amount
178                .try_mul(self.liquidity.market_price)?
179                .try_div(decimals)?;
180            if borrow_value > max_borrow_value {
181                msg!("Borrow value cannot exceed maximum borrow value");
182                return Err(LendingError::BorrowTooLarge.into());
183            }
184
185            Ok(CalculateBorrowResult {
186                borrow_amount,
187                receive_amount,
188                borrow_fee,
189                host_fee,
190            })
191        }
192    }
193
194    /// Repay liquidity up to the borrowed amount
195    pub fn calculate_repay(
196        &self,
197        amount_to_repay: u64,
198        borrowed_amount: Decimal,
199    ) -> Result<CalculateRepayResult, ProgramError> {
200        let settle_amount = if amount_to_repay == u64::MAX {
201            borrowed_amount
202        } else {
203            Decimal::from(amount_to_repay).min(borrowed_amount)
204        };
205        let repay_amount = settle_amount.try_ceil_u64()?;
206
207        Ok(CalculateRepayResult {
208            settle_amount,
209            repay_amount,
210        })
211    }
212
213    /// Liquidate some or all of an unhealthy obligation
214    pub fn calculate_liquidation(
215        &self,
216        amount_to_liquidate: u64,
217        obligation: &Obligation,
218        liquidity: &ObligationLiquidity,
219        collateral: &ObligationCollateral,
220    ) -> Result<CalculateLiquidationResult, ProgramError> {
221        let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?;
222
223        let max_amount = if amount_to_liquidate == u64::MAX {
224            liquidity.borrowed_amount_wads
225        } else {
226            Decimal::from(amount_to_liquidate).min(liquidity.borrowed_amount_wads)
227        };
228
229        let settle_amount;
230        let repay_amount;
231        let withdraw_amount;
232
233        // Close out obligations that are too small to liquidate normally
234        if liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() {
235            // settle_amount is fixed, calculate withdraw_amount and repay_amount
236            settle_amount = liquidity.borrowed_amount_wads;
237
238            let liquidation_value = liquidity.market_value.try_mul(bonus_rate)?;
239            match liquidation_value.cmp(&collateral.market_value) {
240                Ordering::Greater => {
241                    let repay_pct = collateral.market_value.try_div(liquidation_value)?;
242                    repay_amount = max_amount.try_mul(repay_pct)?.try_ceil_u64()?;
243                    withdraw_amount = collateral.deposited_amount;
244                }
245                Ordering::Equal => {
246                    repay_amount = max_amount.try_ceil_u64()?;
247                    withdraw_amount = collateral.deposited_amount;
248                }
249                Ordering::Less => {
250                    let withdraw_pct = liquidation_value.try_div(collateral.market_value)?;
251                    repay_amount = max_amount.try_floor_u64()?;
252                    withdraw_amount = Decimal::from(collateral.deposited_amount)
253                        .try_mul(withdraw_pct)?
254                        .try_floor_u64()?;
255                }
256            }
257        } else {
258            // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded
259            let liquidation_amount = obligation
260                .max_liquidation_amount(liquidity)?
261                .min(max_amount);
262            let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?;
263            let liquidation_value = liquidity
264                .market_value
265                .try_mul(liquidation_pct)?
266                .try_mul(bonus_rate)?;
267
268            match liquidation_value.cmp(&collateral.market_value) {
269                Ordering::Greater => {
270                    let repay_pct = collateral.market_value.try_div(liquidation_value)?;
271                    settle_amount = liquidation_amount.try_mul(repay_pct)?;
272                    repay_amount = settle_amount.try_ceil_u64()?;
273                    withdraw_amount = collateral.deposited_amount;
274                }
275                Ordering::Equal => {
276                    settle_amount = liquidation_amount;
277                    repay_amount = settle_amount.try_ceil_u64()?;
278                    withdraw_amount = collateral.deposited_amount;
279                }
280                Ordering::Less => {
281                    let withdraw_pct = liquidation_value.try_div(collateral.market_value)?;
282                    settle_amount = liquidation_amount;
283                    repay_amount = settle_amount.try_floor_u64()?;
284                    withdraw_amount = Decimal::from(collateral.deposited_amount)
285                        .try_mul(withdraw_pct)?
286                        .try_floor_u64()?;
287                }
288            }
289        }
290
291        Ok(CalculateLiquidationResult {
292            settle_amount,
293            repay_amount,
294            withdraw_amount,
295        })
296    }
297}
298
299/// Initialize a reserve
300pub struct InitReserveParams {
301    /// Last slot when supply and rates updated
302    pub current_slot: Slot,
303    /// Lending market address
304    pub lending_market: Pubkey,
305    /// Reserve liquidity
306    pub liquidity: ReserveLiquidity,
307    /// Reserve collateral
308    pub collateral: ReserveCollateral,
309    /// Reserve configuration values
310    pub config: ReserveConfig,
311}
312
313/// Calculate borrow result
314#[derive(Debug)]
315pub struct CalculateBorrowResult {
316    /// Total amount of borrow including fees
317    pub borrow_amount: Decimal,
318    /// Borrow amount portion of total amount
319    pub receive_amount: u64,
320    /// Loan origination fee
321    pub borrow_fee: u64,
322    /// Host fee portion of origination fee
323    pub host_fee: u64,
324}
325
326/// Calculate repay result
327#[derive(Debug)]
328pub struct CalculateRepayResult {
329    /// Amount of liquidity that is settled from the obligation.
330    pub settle_amount: Decimal,
331    /// Amount that will be repaid as u64
332    pub repay_amount: u64,
333}
334
335/// Calculate liquidation result
336#[derive(Debug)]
337pub struct CalculateLiquidationResult {
338    /// Amount of liquidity that is settled from the obligation. It includes
339    /// the amount of loan that was defaulted if collateral is depleted.
340    pub settle_amount: Decimal,
341    /// Amount that will be repaid as u64
342    pub repay_amount: u64,
343    /// Amount of collateral to withdraw in exchange for repay amount
344    pub withdraw_amount: u64,
345}
346
347/// Reserve liquidity
348#[derive(Clone, Debug, Default, PartialEq)]
349pub struct ReserveLiquidity {
350    /// Reserve liquidity mint address
351    pub mint_pubkey: Pubkey,
352    /// Reserve liquidity mint decimals
353    pub mint_decimals: u8,
354    /// Reserve liquidity supply address
355    pub supply_pubkey: Pubkey,
356    /// Reserve liquidity fee receiver address
357    pub fee_receiver: Pubkey,
358    /// Reserve liquidity oracle account
359    pub oracle_pubkey: Pubkey,
360    /// Reserve liquidity available
361    pub available_amount: u64,
362    /// Reserve liquidity borrowed
363    pub borrowed_amount_wads: Decimal,
364    /// Reserve liquidity cumulative borrow rate
365    pub cumulative_borrow_rate_wads: Decimal,
366    /// Reserve liquidity market price in quote currency
367    pub market_price: Decimal,
368}
369
370impl ReserveLiquidity {
371    /// Create a new reserve liquidity
372    pub fn new(params: NewReserveLiquidityParams) -> Self {
373        Self {
374            mint_pubkey: params.mint_pubkey,
375            mint_decimals: params.mint_decimals,
376            supply_pubkey: params.supply_pubkey,
377            fee_receiver: params.fee_receiver,
378            oracle_pubkey: params.oracle_pubkey,
379            available_amount: 0,
380            borrowed_amount_wads: Decimal::zero(),
381            cumulative_borrow_rate_wads: Decimal::one(),
382            market_price: params.market_price,
383        }
384    }
385
386    /// Calculate the total reserve supply including active loans
387    pub fn total_supply(&self) -> Result<Decimal, ProgramError> {
388        Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads)
389    }
390
391    /// Add liquidity to available amount
392    pub fn deposit(&mut self, liquidity_amount: u64) -> ProgramResult {
393        self.available_amount = self
394            .available_amount
395            .checked_add(liquidity_amount)
396            .ok_or(LendingError::MathOverflow)?;
397        Ok(())
398    }
399
400    /// Remove liquidity from available amount
401    pub fn withdraw(&mut self, liquidity_amount: u64) -> ProgramResult {
402        if liquidity_amount > self.available_amount {
403            msg!("Withdraw amount cannot exceed available amount");
404            return Err(LendingError::InsufficientLiquidity.into());
405        }
406        self.available_amount = self
407            .available_amount
408            .checked_sub(liquidity_amount)
409            .ok_or(LendingError::MathOverflow)?;
410        Ok(())
411    }
412
413    /// Subtract borrow amount from available liquidity and add to borrows
414    pub fn borrow(&mut self, borrow_decimal: Decimal) -> ProgramResult {
415        let borrow_amount = borrow_decimal.try_floor_u64()?;
416        if borrow_amount > self.available_amount {
417            msg!("Borrow amount cannot exceed available amount");
418            return Err(LendingError::InsufficientLiquidity.into());
419        }
420
421        self.available_amount = self
422            .available_amount
423            .checked_sub(borrow_amount)
424            .ok_or(LendingError::MathOverflow)?;
425        self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_decimal)?;
426
427        Ok(())
428    }
429
430    /// Add repay amount to available liquidity and subtract settle amount from total borrows
431    pub fn repay(&mut self, repay_amount: u64, settle_amount: Decimal) -> ProgramResult {
432        self.available_amount = self
433            .available_amount
434            .checked_add(repay_amount)
435            .ok_or(LendingError::MathOverflow)?;
436        self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
437
438        Ok(())
439    }
440
441    /// Calculate the liquidity utilization rate of the reserve
442    pub fn utilization_rate(&self) -> Result<Rate, ProgramError> {
443        let total_supply = self.total_supply()?;
444        if total_supply == Decimal::zero() {
445            return Ok(Rate::zero());
446        }
447        self.borrowed_amount_wads.try_div(total_supply)?.try_into()
448    }
449
450    /// Compound current borrow rate over elapsed slots
451    fn compound_interest(
452        &mut self,
453        current_borrow_rate: Rate,
454        slots_elapsed: u64,
455    ) -> ProgramResult {
456        let slot_interest_rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?;
457        let compounded_interest_rate = Rate::one()
458            .try_add(slot_interest_rate)?
459            .try_pow(slots_elapsed)?;
460        self.cumulative_borrow_rate_wads = self
461            .cumulative_borrow_rate_wads
462            .try_mul(compounded_interest_rate)?;
463        self.borrowed_amount_wads = self
464            .borrowed_amount_wads
465            .try_mul(compounded_interest_rate)?;
466        Ok(())
467    }
468}
469
470/// Create a new reserve liquidity
471pub struct NewReserveLiquidityParams {
472    /// Reserve liquidity mint address
473    pub mint_pubkey: Pubkey,
474    /// Reserve liquidity mint decimals
475    pub mint_decimals: u8,
476    /// Reserve liquidity supply address
477    pub supply_pubkey: Pubkey,
478    /// Reserve liquidity fee receiver address
479    pub fee_receiver: Pubkey,
480    /// Reserve liquidity oracle account
481    pub oracle_pubkey: Pubkey,
482    /// Reserve liquidity market price in quote currency
483    pub market_price: Decimal,
484}
485
486/// Reserve collateral
487#[derive(Clone, Debug, Default, PartialEq)]
488pub struct ReserveCollateral {
489    /// Reserve collateral mint address
490    pub mint_pubkey: Pubkey,
491    /// Reserve collateral mint supply, used for exchange rate
492    pub mint_total_supply: u64,
493    /// Reserve collateral supply address
494    pub supply_pubkey: Pubkey,
495}
496
497impl ReserveCollateral {
498    /// Create a new reserve collateral
499    pub fn new(params: NewReserveCollateralParams) -> Self {
500        Self {
501            mint_pubkey: params.mint_pubkey,
502            mint_total_supply: 0,
503            supply_pubkey: params.supply_pubkey,
504        }
505    }
506
507    /// Add collateral to total supply
508    pub fn mint(&mut self, collateral_amount: u64) -> ProgramResult {
509        self.mint_total_supply = self
510            .mint_total_supply
511            .checked_add(collateral_amount)
512            .ok_or(LendingError::MathOverflow)?;
513        Ok(())
514    }
515
516    /// Remove collateral from total supply
517    pub fn burn(&mut self, collateral_amount: u64) -> ProgramResult {
518        self.mint_total_supply = self
519            .mint_total_supply
520            .checked_sub(collateral_amount)
521            .ok_or(LendingError::MathOverflow)?;
522        Ok(())
523    }
524
525    /// Return the current collateral exchange rate.
526    fn exchange_rate(
527        &self,
528        total_liquidity: Decimal,
529    ) -> Result<CollateralExchangeRate, ProgramError> {
530        let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() {
531            Rate::from_scaled_val(INITIAL_COLLATERAL_RATE)
532        } else {
533            let mint_total_supply = Decimal::from(self.mint_total_supply);
534            Rate::try_from(mint_total_supply.try_div(total_liquidity)?)?
535        };
536
537        Ok(CollateralExchangeRate(rate))
538    }
539}
540
541/// Create a new reserve collateral
542pub struct NewReserveCollateralParams {
543    /// Reserve collateral mint address
544    pub mint_pubkey: Pubkey,
545    /// Reserve collateral supply address
546    pub supply_pubkey: Pubkey,
547}
548
549/// Collateral exchange rate
550#[derive(Clone, Copy, Debug)]
551pub struct CollateralExchangeRate(Rate);
552
553impl CollateralExchangeRate {
554    /// Convert reserve collateral to liquidity
555    pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result<u64, ProgramError> {
556        self.decimal_collateral_to_liquidity(collateral_amount.into())?
557            .try_floor_u64()
558    }
559
560    /// Convert reserve collateral to liquidity
561    pub fn decimal_collateral_to_liquidity(
562        &self,
563        collateral_amount: Decimal,
564    ) -> Result<Decimal, ProgramError> {
565        collateral_amount.try_div(self.0)
566    }
567
568    /// Convert reserve liquidity to collateral
569    pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> Result<u64, ProgramError> {
570        self.decimal_liquidity_to_collateral(liquidity_amount.into())?
571            .try_floor_u64()
572    }
573
574    /// Convert reserve liquidity to collateral
575    pub fn decimal_liquidity_to_collateral(
576        &self,
577        liquidity_amount: Decimal,
578    ) -> Result<Decimal, ProgramError> {
579        liquidity_amount.try_mul(self.0)
580    }
581}
582
583impl From<CollateralExchangeRate> for Rate {
584    fn from(exchange_rate: CollateralExchangeRate) -> Self {
585        exchange_rate.0
586    }
587}
588
589/// Reserve configuration values
590#[derive(Clone, Copy, Debug, Default, PartialEq)]
591pub struct ReserveConfig {
592    /// Optimal utilization rate, as a percentage
593    pub optimal_utilization_rate: u8,
594    /// Target ratio of the value of borrows to deposits, as a percentage
595    /// 0 if use as collateral is disabled
596    pub loan_to_value_ratio: u8,
597    /// Bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage
598    pub liquidation_bonus: u8,
599    /// Loan to value ratio at which an obligation can be liquidated, as a percentage
600    pub liquidation_threshold: u8,
601    /// Min borrow APY
602    pub min_borrow_rate: u8,
603    /// Optimal (utilization) borrow APY
604    pub optimal_borrow_rate: u8,
605    /// Max borrow APY
606    pub max_borrow_rate: u8,
607    /// Program owner fees assessed, separate from gains due to interest accrual
608    pub fees: ReserveFees,
609}
610
611/// Additional fee information on a reserve
612///
613/// These exist separately from interest accrual fees, and are specifically for the program owner
614/// and frontend host. The fees are paid out as a percentage of liquidity token amounts during
615/// repayments and liquidations.
616#[derive(Clone, Copy, Debug, Default, PartialEq)]
617pub struct ReserveFees {
618    /// Fee assessed on `BorrowObligationLiquidity`, expressed as a Wad.
619    /// Must be between 0 and 10^18, such that 10^18 = 1.  A few examples for
620    /// clarity:
621    /// 1% = 10_000_000_000_000_000
622    /// 0.01% (1 basis point) = 100_000_000_000_000
623    /// 0.00001% (Aave borrow fee) = 100_000_000_000
624    pub borrow_fee_wad: u64,
625    /// Fee for flash loan, expressed as a Wad.
626    /// 0.3% (Aave flash loan fee) = 3_000_000_000_000_000
627    pub flash_loan_fee_wad: u64,
628    /// Amount of fee going to host account, if provided in liquidate and repay
629    pub host_fee_percentage: u8,
630}
631
632impl ReserveFees {
633    /// Calculate the owner and host fees on borrow
634    pub fn calculate_borrow_fees(
635        &self,
636        borrow_amount: Decimal,
637        fee_calculation: FeeCalculation,
638    ) -> Result<(u64, u64), ProgramError> {
639        self.calculate_fees(borrow_amount, self.borrow_fee_wad, fee_calculation)
640    }
641
642    /// Calculate the owner and host fees on flash loan
643    pub fn calculate_flash_loan_fees(
644        &self,
645        flash_loan_amount: Decimal,
646    ) -> Result<(u64, u64), ProgramError> {
647        self.calculate_fees(
648            flash_loan_amount,
649            self.flash_loan_fee_wad,
650            FeeCalculation::Exclusive,
651        )
652    }
653
654    fn calculate_fees(
655        &self,
656        amount: Decimal,
657        fee_wad: u64,
658        fee_calculation: FeeCalculation,
659    ) -> Result<(u64, u64), ProgramError> {
660        let borrow_fee_rate = Rate::from_scaled_val(fee_wad);
661        let host_fee_rate = Rate::from_percent(self.host_fee_percentage);
662        if borrow_fee_rate > Rate::zero() && amount > Decimal::zero() {
663            let need_to_assess_host_fee = host_fee_rate > Rate::zero();
664            let minimum_fee = if need_to_assess_host_fee {
665                2u64 // 1 token to owner, 1 to host
666            } else {
667                1u64 // 1 token to owner, nothing else
668            };
669
670            let borrow_fee_amount = match fee_calculation {
671                // Calculate fee to be added to borrow: fee = amount * rate
672                FeeCalculation::Exclusive => amount.try_mul(borrow_fee_rate)?,
673                // Calculate fee to be subtracted from borrow: fee = amount * (rate / (rate + 1))
674                FeeCalculation::Inclusive => {
675                    let borrow_fee_rate =
676                        borrow_fee_rate.try_div(borrow_fee_rate.try_add(Rate::one())?)?;
677                    amount.try_mul(borrow_fee_rate)?
678                }
679            };
680
681            let borrow_fee_decimal = borrow_fee_amount.max(minimum_fee.into());
682            if borrow_fee_decimal >= amount {
683                msg!("Borrow amount is too small to receive liquidity after fees");
684                return Err(LendingError::BorrowTooSmall.into());
685            }
686
687            let borrow_fee = borrow_fee_decimal.try_round_u64()?;
688            let host_fee = if need_to_assess_host_fee {
689                borrow_fee_decimal
690                    .try_mul(host_fee_rate)?
691                    .try_round_u64()?
692                    .max(1u64)
693            } else {
694                0
695            };
696
697            Ok((borrow_fee, host_fee))
698        } else {
699            Ok((0, 0))
700        }
701    }
702}
703
704/// Calculate fees exlusive or inclusive of an amount
705pub enum FeeCalculation {
706    /// Fee added to amount: fee = rate * amount
707    Exclusive,
708    /// Fee included in amount: fee = (rate / (1 + rate)) * amount
709    Inclusive,
710}
711
712impl Sealed for Reserve {}
713impl IsInitialized for Reserve {
714    fn is_initialized(&self) -> bool {
715        self.version != UNINITIALIZED_VERSION
716    }
717}
718
719const RESERVE_LEN: usize = 571; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 248
720impl Pack for Reserve {
721    const LEN: usize = RESERVE_LEN;
722
723    // @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca
724    fn pack_into_slice(&self, output: &mut [u8]) {
725        let output = array_mut_ref![output, 0, RESERVE_LEN];
726        #[allow(clippy::ptr_offset_with_cast)]
727        let (
728            version,
729            last_update_slot,
730            last_update_stale,
731            lending_market,
732            liquidity_mint_pubkey,
733            liquidity_mint_decimals,
734            liquidity_supply_pubkey,
735            liquidity_fee_receiver,
736            liquidity_oracle_pubkey,
737            liquidity_available_amount,
738            liquidity_borrowed_amount_wads,
739            liquidity_cumulative_borrow_rate_wads,
740            liquidity_market_price,
741            collateral_mint_pubkey,
742            collateral_mint_total_supply,
743            collateral_supply_pubkey,
744            config_optimal_utilization_rate,
745            config_loan_to_value_ratio,
746            config_liquidation_bonus,
747            config_liquidation_threshold,
748            config_min_borrow_rate,
749            config_optimal_borrow_rate,
750            config_max_borrow_rate,
751            config_fees_borrow_fee_wad,
752            config_fees_flash_loan_fee_wad,
753            config_fees_host_fee_percentage,
754            _padding,
755        ) = mut_array_refs![
756            output,
757            1,
758            8,
759            1,
760            PUBKEY_BYTES,
761            PUBKEY_BYTES,
762            1,
763            PUBKEY_BYTES,
764            PUBKEY_BYTES,
765            PUBKEY_BYTES,
766            8,
767            16,
768            16,
769            16,
770            PUBKEY_BYTES,
771            8,
772            PUBKEY_BYTES,
773            1,
774            1,
775            1,
776            1,
777            1,
778            1,
779            1,
780            8,
781            8,
782            1,
783            248
784        ];
785
786        // reserve
787        *version = self.version.to_le_bytes();
788        *last_update_slot = self.last_update.slot.to_le_bytes();
789        pack_bool(self.last_update.stale, last_update_stale);
790        lending_market.copy_from_slice(self.lending_market.as_ref());
791
792        // liquidity
793        liquidity_mint_pubkey.copy_from_slice(self.liquidity.mint_pubkey.as_ref());
794        *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes();
795        liquidity_supply_pubkey.copy_from_slice(self.liquidity.supply_pubkey.as_ref());
796        liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref());
797        liquidity_oracle_pubkey.copy_from_slice(self.liquidity.oracle_pubkey.as_ref());
798        *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes();
799        pack_decimal(
800            self.liquidity.borrowed_amount_wads,
801            liquidity_borrowed_amount_wads,
802        );
803        pack_decimal(
804            self.liquidity.cumulative_borrow_rate_wads,
805            liquidity_cumulative_borrow_rate_wads,
806        );
807        pack_decimal(self.liquidity.market_price, liquidity_market_price);
808
809        // collateral
810        collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref());
811        *collateral_mint_total_supply = self.collateral.mint_total_supply.to_le_bytes();
812        collateral_supply_pubkey.copy_from_slice(self.collateral.supply_pubkey.as_ref());
813
814        // config
815        *config_optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes();
816        *config_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes();
817        *config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes();
818        *config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes();
819        *config_min_borrow_rate = self.config.min_borrow_rate.to_le_bytes();
820        *config_optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes();
821        *config_max_borrow_rate = self.config.max_borrow_rate.to_le_bytes();
822        *config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes();
823        *config_fees_flash_loan_fee_wad = self.config.fees.flash_loan_fee_wad.to_le_bytes();
824        *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes();
825    }
826
827    /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html).
828    fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
829        let input = array_ref![input, 0, RESERVE_LEN];
830        #[allow(clippy::ptr_offset_with_cast)]
831        let (
832            version,
833            last_update_slot,
834            last_update_stale,
835            lending_market,
836            liquidity_mint_pubkey,
837            liquidity_mint_decimals,
838            liquidity_supply_pubkey,
839            liquidity_fee_receiver,
840            liquidity_oracle_pubkey,
841            liquidity_available_amount,
842            liquidity_borrowed_amount_wads,
843            liquidity_cumulative_borrow_rate_wads,
844            liquidity_market_price,
845            collateral_mint_pubkey,
846            collateral_mint_total_supply,
847            collateral_supply_pubkey,
848            config_optimal_utilization_rate,
849            config_loan_to_value_ratio,
850            config_liquidation_bonus,
851            config_liquidation_threshold,
852            config_min_borrow_rate,
853            config_optimal_borrow_rate,
854            config_max_borrow_rate,
855            config_fees_borrow_fee_wad,
856            config_fees_flash_loan_fee_wad,
857            config_fees_host_fee_percentage,
858            _padding,
859        ) = array_refs![
860            input,
861            1,
862            8,
863            1,
864            PUBKEY_BYTES,
865            PUBKEY_BYTES,
866            1,
867            PUBKEY_BYTES,
868            PUBKEY_BYTES,
869            PUBKEY_BYTES,
870            8,
871            16,
872            16,
873            16,
874            PUBKEY_BYTES,
875            8,
876            PUBKEY_BYTES,
877            1,
878            1,
879            1,
880            1,
881            1,
882            1,
883            1,
884            8,
885            8,
886            1,
887            248
888        ];
889
890        let version = u8::from_le_bytes(*version);
891        if version > PROGRAM_VERSION {
892            msg!("Reserve version does not match lending program version");
893            return Err(ProgramError::InvalidAccountData);
894        }
895
896        Ok(Self {
897            version,
898            last_update: LastUpdate {
899                slot: u64::from_le_bytes(*last_update_slot),
900                stale: unpack_bool(last_update_stale)?,
901            },
902            lending_market: Pubkey::new_from_array(*lending_market),
903            liquidity: ReserveLiquidity {
904                mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey),
905                mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals),
906                supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey),
907                fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver),
908                oracle_pubkey: Pubkey::new_from_array(*liquidity_oracle_pubkey),
909                available_amount: u64::from_le_bytes(*liquidity_available_amount),
910                borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads),
911                cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads),
912                market_price: unpack_decimal(liquidity_market_price),
913            },
914            collateral: ReserveCollateral {
915                mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey),
916                mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply),
917                supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey),
918            },
919            config: ReserveConfig {
920                optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate),
921                loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio),
922                liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus),
923                liquidation_threshold: u8::from_le_bytes(*config_liquidation_threshold),
924                min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate),
925                optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate),
926                max_borrow_rate: u8::from_le_bytes(*config_max_borrow_rate),
927                fees: ReserveFees {
928                    borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad),
929                    flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad),
930                    host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage),
931                },
932            },
933        })
934    }
935}
936
937#[cfg(test)]
938mod test {
939    use super::*;
940    use crate::math::{PERCENT_SCALER, WAD};
941    use proptest::prelude::*;
942    use std::cmp::Ordering;
943
944    const MAX_LIQUIDITY: u64 = u64::MAX / 5;
945
946    // Creates rates (min, opt, max) where 0 <= min <= opt <= max <= MAX
947    prop_compose! {
948        fn borrow_rates()(optimal_rate in 0..=u8::MAX)(
949            min_rate in 0..=optimal_rate,
950            optimal_rate in Just(optimal_rate),
951            max_rate in optimal_rate..=u8::MAX,
952        ) -> (u8, u8, u8) {
953            (min_rate, optimal_rate, max_rate)
954        }
955    }
956
957    // Creates rates (threshold, ltv) where 2 <= threshold <= 100 and threshold <= ltv <= 1,000%
958    prop_compose! {
959        fn unhealthy_rates()(threshold in 2..=100u8)(
960            ltv_rate in threshold as u64..=1000u64,
961            threshold in Just(threshold),
962        ) -> (Decimal, u8) {
963            (Decimal::from_scaled_val(ltv_rate as u128 * PERCENT_SCALER as u128), threshold)
964        }
965    }
966
967    // Creates a range of reasonable token conversion rates
968    prop_compose! {
969        fn token_conversion_rate()(
970            conversion_rate in 1..=u16::MAX,
971            invert_conversion_rate: bool,
972        ) -> Decimal {
973            let conversion_rate = Decimal::from(conversion_rate as u64);
974            if invert_conversion_rate {
975                Decimal::one().try_div(conversion_rate).unwrap()
976            } else {
977                conversion_rate
978            }
979        }
980    }
981
982    // Creates a range of reasonable collateral exchange rates
983    prop_compose! {
984        fn collateral_exchange_rate_range()(percent in 1..=500u64) -> CollateralExchangeRate {
985            CollateralExchangeRate(Rate::from_scaled_val(percent * PERCENT_SCALER))
986        }
987    }
988
989    proptest! {
990        #[test]
991        fn current_borrow_rate(
992            total_liquidity in 0..=MAX_LIQUIDITY,
993            borrowed_percent in 0..=WAD,
994            optimal_utilization_rate in 0..=100u8,
995            (min_borrow_rate, optimal_borrow_rate, max_borrow_rate) in borrow_rates(),
996        ) {
997            let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
998            let reserve = Reserve {
999                liquidity: ReserveLiquidity {
1000                    borrowed_amount_wads,
1001                    available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?,
1002                    ..ReserveLiquidity::default()
1003                },
1004                config: ReserveConfig { optimal_utilization_rate, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, ..ReserveConfig::default() },
1005                ..Reserve::default()
1006            };
1007
1008            let current_borrow_rate = reserve.current_borrow_rate()?;
1009            assert!(current_borrow_rate >= Rate::from_percent(min_borrow_rate));
1010            assert!(current_borrow_rate <= Rate::from_percent(max_borrow_rate));
1011
1012            let optimal_borrow_rate = Rate::from_percent(optimal_borrow_rate);
1013            let current_rate = reserve.liquidity.utilization_rate()?;
1014            match current_rate.cmp(&Rate::from_percent(optimal_utilization_rate)) {
1015                Ordering::Less => {
1016                    if min_borrow_rate == reserve.config.optimal_borrow_rate {
1017                        assert_eq!(current_borrow_rate, optimal_borrow_rate);
1018                    } else {
1019                        assert!(current_borrow_rate < optimal_borrow_rate);
1020                    }
1021                }
1022                Ordering::Equal => assert!(current_borrow_rate == optimal_borrow_rate),
1023                Ordering::Greater => {
1024                    if max_borrow_rate == reserve.config.optimal_borrow_rate {
1025                        assert_eq!(current_borrow_rate, optimal_borrow_rate);
1026                    } else {
1027                        assert!(current_borrow_rate > optimal_borrow_rate);
1028                    }
1029                }
1030            }
1031        }
1032
1033        #[test]
1034        fn current_utilization_rate(
1035            total_liquidity in 0..=MAX_LIQUIDITY,
1036            borrowed_percent in 0..=WAD,
1037        ) {
1038            let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
1039            let liquidity = ReserveLiquidity {
1040                borrowed_amount_wads,
1041                available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?,
1042                ..ReserveLiquidity::default()
1043            };
1044
1045            let current_rate = liquidity.utilization_rate()?;
1046            assert!(current_rate <= Rate::one());
1047        }
1048
1049        #[test]
1050        fn collateral_exchange_rate(
1051            total_liquidity in 0..=MAX_LIQUIDITY,
1052            borrowed_percent in 0..=WAD,
1053            collateral_multiplier in 0..=(5*WAD),
1054            borrow_rate in 0..=u8::MAX,
1055        ) {
1056            let borrowed_liquidity_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
1057            let available_liquidity = total_liquidity - borrowed_liquidity_wads.try_round_u64()?;
1058            let mint_total_supply = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(collateral_multiplier))?.try_round_u64()?;
1059
1060            let mut reserve = Reserve {
1061                collateral: ReserveCollateral {
1062                    mint_total_supply,
1063                    ..ReserveCollateral::default()
1064                },
1065                liquidity: ReserveLiquidity {
1066                    borrowed_amount_wads: borrowed_liquidity_wads,
1067                    available_amount: available_liquidity,
1068                    ..ReserveLiquidity::default()
1069                },
1070                config: ReserveConfig {
1071                    min_borrow_rate: borrow_rate,
1072                    optimal_borrow_rate: borrow_rate,
1073                    optimal_utilization_rate: 100,
1074                    ..ReserveConfig::default()
1075                },
1076                ..Reserve::default()
1077            };
1078
1079            let exchange_rate = reserve.collateral_exchange_rate()?;
1080            assert!(exchange_rate.0.to_scaled_val() <= 5u128 * WAD as u128);
1081
1082            // After interest accrual, total liquidity increases and collateral are worth more
1083            reserve.accrue_interest(1)?;
1084
1085            let new_exchange_rate = reserve.collateral_exchange_rate()?;
1086            if borrow_rate > 0 && total_liquidity > 0 && borrowed_percent > 0 {
1087                assert!(new_exchange_rate.0 < exchange_rate.0);
1088            } else {
1089                assert_eq!(new_exchange_rate.0, exchange_rate.0);
1090            }
1091        }
1092
1093        #[test]
1094        fn compound_interest(
1095            slots_elapsed in 0..=SLOTS_PER_YEAR,
1096            borrow_rate in 0..=u8::MAX,
1097        ) {
1098            let mut reserve = Reserve::default();
1099            let borrow_rate = Rate::from_percent(borrow_rate);
1100
1101            // Simulate running for max 1000 years, assuming that interest is
1102            // compounded at least once a year
1103            for _ in 0..1000 {
1104                reserve.liquidity.compound_interest(borrow_rate, slots_elapsed)?;
1105                reserve.liquidity.cumulative_borrow_rate_wads.to_scaled_val()?;
1106            }
1107        }
1108
1109        #[test]
1110        fn reserve_accrue_interest(
1111            slots_elapsed in 0..=SLOTS_PER_YEAR,
1112            borrowed_liquidity in 0..=u64::MAX,
1113            borrow_rate in 0..=u8::MAX,
1114        ) {
1115            let borrowed_amount_wads = Decimal::from(borrowed_liquidity);
1116            let mut reserve = Reserve {
1117                liquidity: ReserveLiquidity {
1118                    borrowed_amount_wads,
1119                    ..ReserveLiquidity::default()
1120                },
1121                config: ReserveConfig {
1122                    max_borrow_rate: borrow_rate,
1123                    ..ReserveConfig::default()
1124                },
1125                ..Reserve::default()
1126            };
1127
1128            reserve.accrue_interest(slots_elapsed)?;
1129
1130            if borrow_rate > 0 && slots_elapsed > 0 {
1131                assert!(reserve.liquidity.borrowed_amount_wads > borrowed_amount_wads);
1132            } else {
1133                assert!(reserve.liquidity.borrowed_amount_wads == borrowed_amount_wads);
1134            }
1135        }
1136
1137        #[test]
1138        fn borrow_fee_calculation(
1139            borrow_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails
1140            flash_loan_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails
1141            host_fee_percentage in 0..=100u8,
1142            borrow_amount in 3..=u64::MAX, // start at 3 to ensure calculation success
1143                                           // 0, 1, and 2 are covered in the minimum tests
1144                                           // @FIXME: ^ no longer true
1145        ) {
1146            let fees = ReserveFees {
1147                borrow_fee_wad,
1148                flash_loan_fee_wad,
1149                host_fee_percentage,
1150            };
1151            let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(borrow_amount), FeeCalculation::Exclusive)?;
1152
1153            // The total fee can't be greater than the amount borrowed, as long
1154            // as amount borrowed is greater than 2.
1155            // At a borrow amount of 2, we can get a total fee of 2 if a host
1156            // fee is also specified.
1157            assert!(total_fee <= borrow_amount);
1158
1159            // the host fee can't be greater than the total fee
1160            assert!(host_fee <= total_fee);
1161
1162            // for all fee rates greater than 0, we must have some fee
1163            if borrow_fee_wad > 0 {
1164                assert!(total_fee > 0);
1165            }
1166
1167            if host_fee_percentage == 100 {
1168                // if the host fee percentage is maxed at 100%, it should get all the fee
1169                assert_eq!(host_fee, total_fee);
1170            }
1171
1172            // if there's a host fee and some borrow fee, host fee must be greater than 0
1173            if host_fee_percentage > 0 && borrow_fee_wad > 0 {
1174                assert!(host_fee > 0);
1175            } else {
1176                assert_eq!(host_fee, 0);
1177            }
1178        }
1179
1180        #[test]
1181        fn flash_loan_fee_calculation(
1182            borrow_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails
1183            flash_loan_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails
1184            host_fee_percentage in 0..=100u8,
1185            borrow_amount in 3..=u64::MAX, // start at 3 to ensure calculation success
1186                                           // 0, 1, and 2 are covered in the minimum tests
1187                                           // @FIXME: ^ no longer true
1188        ) {
1189            let fees = ReserveFees {
1190                borrow_fee_wad,
1191                flash_loan_fee_wad,
1192                host_fee_percentage,
1193            };
1194            let (total_fee, host_fee) = fees.calculate_flash_loan_fees(Decimal::from(borrow_amount))?;
1195
1196            // The total fee can't be greater than the amount borrowed, as long
1197            // as amount borrowed is greater than 2.
1198            // At a borrow amount of 2, we can get a total fee of 2 if a host
1199            // fee is also specified.
1200            assert!(total_fee <= borrow_amount);
1201
1202            // the host fee can't be greater than the total fee
1203            assert!(host_fee <= total_fee);
1204
1205            // for all fee rates greater than 0, we must have some fee
1206            if borrow_fee_wad > 0 {
1207                assert!(total_fee > 0);
1208            }
1209
1210            if host_fee_percentage == 100 {
1211                // if the host fee percentage is maxed at 100%, it should get all the fee
1212                assert_eq!(host_fee, total_fee);
1213            }
1214
1215            // if there's a host fee and some borrow fee, host fee must be greater than 0
1216            if host_fee_percentage > 0 && borrow_fee_wad > 0 {
1217                assert!(host_fee > 0);
1218            } else {
1219                assert_eq!(host_fee, 0);
1220            }
1221        }
1222    }
1223
1224    #[test]
1225    fn borrow_fee_calculation_min_host() {
1226        let fees = ReserveFees {
1227            borrow_fee_wad: 10_000_000_000_000_000, // 1%
1228            flash_loan_fee_wad: 0,
1229            host_fee_percentage: 20,
1230        };
1231
1232        // only 2 tokens borrowed, get error
1233        let err = fees
1234            .calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive)
1235            .unwrap_err();
1236        assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 3 tokens
1237
1238        // only 1 token borrowed, get error
1239        let err = fees
1240            .calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive)
1241            .unwrap_err();
1242        assert_eq!(err, LendingError::BorrowTooSmall.into());
1243
1244        // 0 amount borrowed, 0 fee
1245        let (total_fee, host_fee) = fees
1246            .calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive)
1247            .unwrap();
1248        assert_eq!(total_fee, 0);
1249        assert_eq!(host_fee, 0);
1250    }
1251
1252    #[test]
1253    fn borrow_fee_calculation_min_no_host() {
1254        let fees = ReserveFees {
1255            borrow_fee_wad: 10_000_000_000_000_000, // 1%
1256            flash_loan_fee_wad: 0,
1257            host_fee_percentage: 0,
1258        };
1259
1260        // only 2 tokens borrowed, ok
1261        let (total_fee, host_fee) = fees
1262            .calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive)
1263            .unwrap();
1264        assert_eq!(total_fee, 1);
1265        assert_eq!(host_fee, 0);
1266
1267        // only 1 token borrowed, get error
1268        let err = fees
1269            .calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive)
1270            .unwrap_err();
1271        assert_eq!(err, LendingError::BorrowTooSmall.into()); // minimum of 2 tokens
1272
1273        // 0 amount borrowed, 0 fee
1274        let (total_fee, host_fee) = fees
1275            .calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive)
1276            .unwrap();
1277        assert_eq!(total_fee, 0);
1278        assert_eq!(host_fee, 0);
1279    }
1280
1281    #[test]
1282    fn borrow_fee_calculation_host() {
1283        let fees = ReserveFees {
1284            borrow_fee_wad: 10_000_000_000_000_000, // 1%
1285            flash_loan_fee_wad: 0,
1286            host_fee_percentage: 20,
1287        };
1288
1289        let (total_fee, host_fee) = fees
1290            .calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive)
1291            .unwrap();
1292
1293        assert_eq!(total_fee, 10); // 1% of 1000
1294        assert_eq!(host_fee, 2); // 20% of 10
1295    }
1296
1297    #[test]
1298    fn borrow_fee_calculation_no_host() {
1299        let fees = ReserveFees {
1300            borrow_fee_wad: 10_000_000_000_000_000, // 1%
1301            flash_loan_fee_wad: 0,
1302            host_fee_percentage: 0,
1303        };
1304
1305        let (total_fee, host_fee) = fees
1306            .calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive)
1307            .unwrap();
1308
1309        assert_eq!(total_fee, 10); // 1% of 1000
1310        assert_eq!(host_fee, 0); // 0 host fee
1311    }
1312}