use super::*;
use crate::{
error::LendingError,
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
};
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
use solana_program::{
clock::Slot,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
pubkey::{Pubkey, PUBKEY_BYTES},
};
use std::{
cmp::Ordering,
convert::{TryFrom, TryInto},
};
pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50;
pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Reserve {
pub version: u8,
pub last_update: LastUpdate,
pub lending_market: Pubkey,
pub liquidity: ReserveLiquidity,
pub collateral: ReserveCollateral,
pub config: ReserveConfig,
}
impl Reserve {
pub fn new(params: InitReserveParams) -> Self {
let mut reserve = Self::default();
Self::init(&mut reserve, params);
reserve
}
pub fn init(&mut self, params: InitReserveParams) {
self.version = PROGRAM_VERSION;
self.last_update = LastUpdate::new(params.current_slot);
self.lending_market = params.lending_market;
self.liquidity = params.liquidity;
self.collateral = params.collateral;
self.config = params.config;
}
pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result<u64, ProgramError> {
let collateral_amount = self
.collateral_exchange_rate()?
.liquidity_to_collateral(liquidity_amount)?;
self.liquidity.deposit(liquidity_amount)?;
self.collateral.mint(collateral_amount)?;
Ok(collateral_amount)
}
pub fn redeem_collateral(&mut self, collateral_amount: u64) -> Result<u64, ProgramError> {
let collateral_exchange_rate = self.collateral_exchange_rate()?;
let liquidity_amount =
collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?;
self.collateral.burn(collateral_amount)?;
self.liquidity.withdraw(liquidity_amount)?;
Ok(liquidity_amount)
}
pub fn current_borrow_rate(&self) -> Result<Rate, ProgramError> {
let utilization_rate = self.liquidity.utilization_rate()?;
let optimal_utilization_rate = Rate::from_percent(self.config.optimal_utilization_rate);
let low_utilization = utilization_rate < optimal_utilization_rate;
if low_utilization || self.config.optimal_utilization_rate == 100 {
let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?;
let min_rate = Rate::from_percent(self.config.min_borrow_rate);
let rate_range = Rate::from_percent(
self.config
.optimal_borrow_rate
.checked_sub(self.config.min_borrow_rate)
.ok_or(LendingError::MathOverflow)?,
);
Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
} else {
let normalized_rate = utilization_rate
.try_sub(optimal_utilization_rate)?
.try_div(Rate::from_percent(
100u8
.checked_sub(self.config.optimal_utilization_rate)
.ok_or(LendingError::MathOverflow)?,
))?;
let min_rate = Rate::from_percent(self.config.optimal_borrow_rate);
let rate_range = Rate::from_percent(
self.config
.max_borrow_rate
.checked_sub(self.config.optimal_borrow_rate)
.ok_or(LendingError::MathOverflow)?,
);
Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
}
}
pub fn collateral_exchange_rate(&self) -> Result<CollateralExchangeRate, ProgramError> {
let total_liquidity = self.liquidity.total_supply()?;
self.collateral.exchange_rate(total_liquidity)
}
pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult {
let slots_elapsed = self.last_update.slots_elapsed(current_slot)?;
if slots_elapsed > 0 {
let current_borrow_rate = self.current_borrow_rate()?;
self.liquidity
.compound_interest(current_borrow_rate, slots_elapsed)?;
}
Ok(())
}
pub fn calculate_borrow(
&self,
amount_to_borrow: u64,
max_borrow_value: Decimal,
) -> Result<CalculateBorrowResult, ProgramError> {
let decimals = 10u64
.checked_pow(self.liquidity.mint_decimals as u32)
.ok_or(LendingError::MathOverflow)?;
if amount_to_borrow == u64::MAX {
let borrow_amount = max_borrow_value
.try_mul(decimals)?
.try_div(self.liquidity.market_price)?
.min(self.liquidity.available_amount.into());
let (borrow_fee, host_fee) = self
.config
.fees
.calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?;
let receive_amount = borrow_amount
.try_floor_u64()?
.checked_sub(borrow_fee)
.ok_or(LendingError::MathOverflow)?;
Ok(CalculateBorrowResult {
borrow_amount,
receive_amount,
borrow_fee,
host_fee,
})
} else {
let receive_amount = amount_to_borrow;
let borrow_amount = Decimal::from(receive_amount);
let (borrow_fee, host_fee) = self
.config
.fees
.calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?;
let borrow_amount = borrow_amount.try_add(borrow_fee.into())?;
let borrow_value = borrow_amount
.try_mul(self.liquidity.market_price)?
.try_div(decimals)?;
if borrow_value > max_borrow_value {
msg!("Borrow value cannot exceed maximum borrow value");
return Err(LendingError::BorrowTooLarge.into());
}
Ok(CalculateBorrowResult {
borrow_amount,
receive_amount,
borrow_fee,
host_fee,
})
}
}
pub fn calculate_repay(
&self,
amount_to_repay: u64,
borrowed_amount: Decimal,
) -> Result<CalculateRepayResult, ProgramError> {
let settle_amount = if amount_to_repay == u64::MAX {
borrowed_amount
} else {
Decimal::from(amount_to_repay).min(borrowed_amount)
};
let repay_amount = settle_amount.try_ceil_u64()?;
Ok(CalculateRepayResult {
settle_amount,
repay_amount,
})
}
pub fn calculate_liquidation(
&self,
amount_to_liquidate: u64,
obligation: &Obligation,
liquidity: &ObligationLiquidity,
collateral: &ObligationCollateral,
) -> Result<CalculateLiquidationResult, ProgramError> {
let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?;
let max_amount = if amount_to_liquidate == u64::MAX {
liquidity.borrowed_amount_wads
} else {
Decimal::from(amount_to_liquidate).min(liquidity.borrowed_amount_wads)
};
let settle_amount;
let repay_amount;
let withdraw_amount;
if liquidity.borrowed_amount_wads < LIQUIDATION_CLOSE_AMOUNT.into() {
settle_amount = liquidity.borrowed_amount_wads;
let liquidation_value = liquidity.market_value.try_mul(bonus_rate)?;
match liquidation_value.cmp(&collateral.market_value) {
Ordering::Greater => {
let repay_pct = collateral.market_value.try_div(liquidation_value)?;
repay_amount = max_amount.try_mul(repay_pct)?.try_ceil_u64()?;
withdraw_amount = collateral.deposited_amount;
}
Ordering::Equal => {
repay_amount = max_amount.try_ceil_u64()?;
withdraw_amount = collateral.deposited_amount;
}
Ordering::Less => {
let withdraw_pct = liquidation_value.try_div(collateral.market_value)?;
repay_amount = max_amount.try_floor_u64()?;
withdraw_amount = Decimal::from(collateral.deposited_amount)
.try_mul(withdraw_pct)?
.try_floor_u64()?;
}
}
} else {
let liquidation_amount = obligation
.max_liquidation_amount(liquidity)?
.min(max_amount);
let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?;
let liquidation_value = liquidity
.market_value
.try_mul(liquidation_pct)?
.try_mul(bonus_rate)?;
match liquidation_value.cmp(&collateral.market_value) {
Ordering::Greater => {
let repay_pct = collateral.market_value.try_div(liquidation_value)?;
settle_amount = liquidation_amount.try_mul(repay_pct)?;
repay_amount = settle_amount.try_ceil_u64()?;
withdraw_amount = collateral.deposited_amount;
}
Ordering::Equal => {
settle_amount = liquidation_amount;
repay_amount = settle_amount.try_ceil_u64()?;
withdraw_amount = collateral.deposited_amount;
}
Ordering::Less => {
let withdraw_pct = liquidation_value.try_div(collateral.market_value)?;
settle_amount = liquidation_amount;
repay_amount = settle_amount.try_floor_u64()?;
withdraw_amount = Decimal::from(collateral.deposited_amount)
.try_mul(withdraw_pct)?
.try_floor_u64()?;
}
}
}
Ok(CalculateLiquidationResult {
settle_amount,
repay_amount,
withdraw_amount,
})
}
}
pub struct InitReserveParams {
pub current_slot: Slot,
pub lending_market: Pubkey,
pub liquidity: ReserveLiquidity,
pub collateral: ReserveCollateral,
pub config: ReserveConfig,
}
#[derive(Debug)]
pub struct CalculateBorrowResult {
pub borrow_amount: Decimal,
pub receive_amount: u64,
pub borrow_fee: u64,
pub host_fee: u64,
}
#[derive(Debug)]
pub struct CalculateRepayResult {
pub settle_amount: Decimal,
pub repay_amount: u64,
}
#[derive(Debug)]
pub struct CalculateLiquidationResult {
pub settle_amount: Decimal,
pub repay_amount: u64,
pub withdraw_amount: u64,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ReserveLiquidity {
pub mint_pubkey: Pubkey,
pub mint_decimals: u8,
pub supply_pubkey: Pubkey,
pub fee_receiver: Pubkey,
pub oracle_pubkey: Pubkey,
pub available_amount: u64,
pub borrowed_amount_wads: Decimal,
pub cumulative_borrow_rate_wads: Decimal,
pub market_price: Decimal,
}
impl ReserveLiquidity {
pub fn new(params: NewReserveLiquidityParams) -> Self {
Self {
mint_pubkey: params.mint_pubkey,
mint_decimals: params.mint_decimals,
supply_pubkey: params.supply_pubkey,
fee_receiver: params.fee_receiver,
oracle_pubkey: params.oracle_pubkey,
available_amount: 0,
borrowed_amount_wads: Decimal::zero(),
cumulative_borrow_rate_wads: Decimal::one(),
market_price: params.market_price,
}
}
pub fn total_supply(&self) -> Result<Decimal, ProgramError> {
Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads)
}
pub fn deposit(&mut self, liquidity_amount: u64) -> ProgramResult {
self.available_amount = self
.available_amount
.checked_add(liquidity_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
pub fn withdraw(&mut self, liquidity_amount: u64) -> ProgramResult {
if liquidity_amount > self.available_amount {
msg!("Withdraw amount cannot exceed available amount");
return Err(LendingError::InsufficientLiquidity.into());
}
self.available_amount = self
.available_amount
.checked_sub(liquidity_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
pub fn borrow(&mut self, borrow_decimal: Decimal) -> ProgramResult {
let borrow_amount = borrow_decimal.try_floor_u64()?;
if borrow_amount > self.available_amount {
msg!("Borrow amount cannot exceed available amount");
return Err(LendingError::InsufficientLiquidity.into());
}
self.available_amount = self
.available_amount
.checked_sub(borrow_amount)
.ok_or(LendingError::MathOverflow)?;
self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_decimal)?;
Ok(())
}
pub fn repay(&mut self, repay_amount: u64, settle_amount: Decimal) -> ProgramResult {
self.available_amount = self
.available_amount
.checked_add(repay_amount)
.ok_or(LendingError::MathOverflow)?;
self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
Ok(())
}
pub fn utilization_rate(&self) -> Result<Rate, ProgramError> {
let total_supply = self.total_supply()?;
if total_supply == Decimal::zero() {
return Ok(Rate::zero());
}
self.borrowed_amount_wads.try_div(total_supply)?.try_into()
}
fn compound_interest(
&mut self,
current_borrow_rate: Rate,
slots_elapsed: u64,
) -> ProgramResult {
let slot_interest_rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?;
let compounded_interest_rate = Rate::one()
.try_add(slot_interest_rate)?
.try_pow(slots_elapsed)?;
self.cumulative_borrow_rate_wads = self
.cumulative_borrow_rate_wads
.try_mul(compounded_interest_rate)?;
self.borrowed_amount_wads = self
.borrowed_amount_wads
.try_mul(compounded_interest_rate)?;
Ok(())
}
}
pub struct NewReserveLiquidityParams {
pub mint_pubkey: Pubkey,
pub mint_decimals: u8,
pub supply_pubkey: Pubkey,
pub fee_receiver: Pubkey,
pub oracle_pubkey: Pubkey,
pub market_price: Decimal,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ReserveCollateral {
pub mint_pubkey: Pubkey,
pub mint_total_supply: u64,
pub supply_pubkey: Pubkey,
}
impl ReserveCollateral {
pub fn new(params: NewReserveCollateralParams) -> Self {
Self {
mint_pubkey: params.mint_pubkey,
mint_total_supply: 0,
supply_pubkey: params.supply_pubkey,
}
}
pub fn mint(&mut self, collateral_amount: u64) -> ProgramResult {
self.mint_total_supply = self
.mint_total_supply
.checked_add(collateral_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
pub fn burn(&mut self, collateral_amount: u64) -> ProgramResult {
self.mint_total_supply = self
.mint_total_supply
.checked_sub(collateral_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
fn exchange_rate(
&self,
total_liquidity: Decimal,
) -> Result<CollateralExchangeRate, ProgramError> {
let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() {
Rate::from_scaled_val(INITIAL_COLLATERAL_RATE)
} else {
let mint_total_supply = Decimal::from(self.mint_total_supply);
Rate::try_from(mint_total_supply.try_div(total_liquidity)?)?
};
Ok(CollateralExchangeRate(rate))
}
}
pub struct NewReserveCollateralParams {
pub mint_pubkey: Pubkey,
pub supply_pubkey: Pubkey,
}
#[derive(Clone, Copy, Debug)]
pub struct CollateralExchangeRate(Rate);
impl CollateralExchangeRate {
pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result<u64, ProgramError> {
self.decimal_collateral_to_liquidity(collateral_amount.into())?
.try_floor_u64()
}
pub fn decimal_collateral_to_liquidity(
&self,
collateral_amount: Decimal,
) -> Result<Decimal, ProgramError> {
collateral_amount.try_div(self.0)
}
pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> Result<u64, ProgramError> {
self.decimal_liquidity_to_collateral(liquidity_amount.into())?
.try_floor_u64()
}
pub fn decimal_liquidity_to_collateral(
&self,
liquidity_amount: Decimal,
) -> Result<Decimal, ProgramError> {
liquidity_amount.try_mul(self.0)
}
}
impl From<CollateralExchangeRate> for Rate {
fn from(exchange_rate: CollateralExchangeRate) -> Self {
exchange_rate.0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ReserveConfig {
pub optimal_utilization_rate: u8,
pub loan_to_value_ratio: u8,
pub liquidation_bonus: u8,
pub liquidation_threshold: u8,
pub min_borrow_rate: u8,
pub optimal_borrow_rate: u8,
pub max_borrow_rate: u8,
pub fees: ReserveFees,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ReserveFees {
pub borrow_fee_wad: u64,
pub flash_loan_fee_wad: u64,
pub host_fee_percentage: u8,
}
impl ReserveFees {
pub fn calculate_borrow_fees(
&self,
borrow_amount: Decimal,
fee_calculation: FeeCalculation,
) -> Result<(u64, u64), ProgramError> {
self.calculate_fees(borrow_amount, self.borrow_fee_wad, fee_calculation)
}
pub fn calculate_flash_loan_fees(
&self,
flash_loan_amount: Decimal,
) -> Result<(u64, u64), ProgramError> {
self.calculate_fees(
flash_loan_amount,
self.flash_loan_fee_wad,
FeeCalculation::Exclusive,
)
}
fn calculate_fees(
&self,
amount: Decimal,
fee_wad: u64,
fee_calculation: FeeCalculation,
) -> Result<(u64, u64), ProgramError> {
let borrow_fee_rate = Rate::from_scaled_val(fee_wad);
let host_fee_rate = Rate::from_percent(self.host_fee_percentage);
if borrow_fee_rate > Rate::zero() && amount > Decimal::zero() {
let need_to_assess_host_fee = host_fee_rate > Rate::zero();
let minimum_fee = if need_to_assess_host_fee {
2u64 } else {
1u64 };
let borrow_fee_amount = match fee_calculation {
FeeCalculation::Exclusive => amount.try_mul(borrow_fee_rate)?,
FeeCalculation::Inclusive => {
let borrow_fee_rate =
borrow_fee_rate.try_div(borrow_fee_rate.try_add(Rate::one())?)?;
amount.try_mul(borrow_fee_rate)?
}
};
let borrow_fee_decimal = borrow_fee_amount.max(minimum_fee.into());
if borrow_fee_decimal >= amount {
msg!("Borrow amount is too small to receive liquidity after fees");
return Err(LendingError::BorrowTooSmall.into());
}
let borrow_fee = borrow_fee_decimal.try_round_u64()?;
let host_fee = if need_to_assess_host_fee {
borrow_fee_decimal
.try_mul(host_fee_rate)?
.try_round_u64()?
.max(1u64)
} else {
0
};
Ok((borrow_fee, host_fee))
} else {
Ok((0, 0))
}
}
}
pub enum FeeCalculation {
Exclusive,
Inclusive,
}
impl Sealed for Reserve {}
impl IsInitialized for Reserve {
fn is_initialized(&self) -> bool {
self.version != UNINITIALIZED_VERSION
}
}
const RESERVE_LEN: usize = 571; impl Pack for Reserve {
const LEN: usize = RESERVE_LEN;
fn pack_into_slice(&self, output: &mut [u8]) {
let output = array_mut_ref![output, 0, RESERVE_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (
version,
last_update_slot,
last_update_stale,
lending_market,
liquidity_mint_pubkey,
liquidity_mint_decimals,
liquidity_supply_pubkey,
liquidity_fee_receiver,
liquidity_oracle_pubkey,
liquidity_available_amount,
liquidity_borrowed_amount_wads,
liquidity_cumulative_borrow_rate_wads,
liquidity_market_price,
collateral_mint_pubkey,
collateral_mint_total_supply,
collateral_supply_pubkey,
config_optimal_utilization_rate,
config_loan_to_value_ratio,
config_liquidation_bonus,
config_liquidation_threshold,
config_min_borrow_rate,
config_optimal_borrow_rate,
config_max_borrow_rate,
config_fees_borrow_fee_wad,
config_fees_flash_loan_fee_wad,
config_fees_host_fee_percentage,
_padding,
) = mut_array_refs![
output,
1,
8,
1,
PUBKEY_BYTES,
PUBKEY_BYTES,
1,
PUBKEY_BYTES,
PUBKEY_BYTES,
PUBKEY_BYTES,
8,
16,
16,
16,
PUBKEY_BYTES,
8,
PUBKEY_BYTES,
1,
1,
1,
1,
1,
1,
1,
8,
8,
1,
248
];
*version = self.version.to_le_bytes();
*last_update_slot = self.last_update.slot.to_le_bytes();
pack_bool(self.last_update.stale, last_update_stale);
lending_market.copy_from_slice(self.lending_market.as_ref());
liquidity_mint_pubkey.copy_from_slice(self.liquidity.mint_pubkey.as_ref());
*liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes();
liquidity_supply_pubkey.copy_from_slice(self.liquidity.supply_pubkey.as_ref());
liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref());
liquidity_oracle_pubkey.copy_from_slice(self.liquidity.oracle_pubkey.as_ref());
*liquidity_available_amount = self.liquidity.available_amount.to_le_bytes();
pack_decimal(
self.liquidity.borrowed_amount_wads,
liquidity_borrowed_amount_wads,
);
pack_decimal(
self.liquidity.cumulative_borrow_rate_wads,
liquidity_cumulative_borrow_rate_wads,
);
pack_decimal(self.liquidity.market_price, liquidity_market_price);
collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref());
*collateral_mint_total_supply = self.collateral.mint_total_supply.to_le_bytes();
collateral_supply_pubkey.copy_from_slice(self.collateral.supply_pubkey.as_ref());
*config_optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes();
*config_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes();
*config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes();
*config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes();
*config_min_borrow_rate = self.config.min_borrow_rate.to_le_bytes();
*config_optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes();
*config_max_borrow_rate = self.config.max_borrow_rate.to_le_bytes();
*config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes();
*config_fees_flash_loan_fee_wad = self.config.fees.flash_loan_fee_wad.to_le_bytes();
*config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes();
}
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
let input = array_ref![input, 0, RESERVE_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (
version,
last_update_slot,
last_update_stale,
lending_market,
liquidity_mint_pubkey,
liquidity_mint_decimals,
liquidity_supply_pubkey,
liquidity_fee_receiver,
liquidity_oracle_pubkey,
liquidity_available_amount,
liquidity_borrowed_amount_wads,
liquidity_cumulative_borrow_rate_wads,
liquidity_market_price,
collateral_mint_pubkey,
collateral_mint_total_supply,
collateral_supply_pubkey,
config_optimal_utilization_rate,
config_loan_to_value_ratio,
config_liquidation_bonus,
config_liquidation_threshold,
config_min_borrow_rate,
config_optimal_borrow_rate,
config_max_borrow_rate,
config_fees_borrow_fee_wad,
config_fees_flash_loan_fee_wad,
config_fees_host_fee_percentage,
_padding,
) = array_refs![
input,
1,
8,
1,
PUBKEY_BYTES,
PUBKEY_BYTES,
1,
PUBKEY_BYTES,
PUBKEY_BYTES,
PUBKEY_BYTES,
8,
16,
16,
16,
PUBKEY_BYTES,
8,
PUBKEY_BYTES,
1,
1,
1,
1,
1,
1,
1,
8,
8,
1,
248
];
let version = u8::from_le_bytes(*version);
if version > PROGRAM_VERSION {
msg!("Reserve version does not match lending program version");
return Err(ProgramError::InvalidAccountData);
}
Ok(Self {
version,
last_update: LastUpdate {
slot: u64::from_le_bytes(*last_update_slot),
stale: unpack_bool(last_update_stale)?,
},
lending_market: Pubkey::new_from_array(*lending_market),
liquidity: ReserveLiquidity {
mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey),
mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals),
supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey),
fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver),
oracle_pubkey: Pubkey::new_from_array(*liquidity_oracle_pubkey),
available_amount: u64::from_le_bytes(*liquidity_available_amount),
borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads),
cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads),
market_price: unpack_decimal(liquidity_market_price),
},
collateral: ReserveCollateral {
mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey),
mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply),
supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey),
},
config: ReserveConfig {
optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate),
loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio),
liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus),
liquidation_threshold: u8::from_le_bytes(*config_liquidation_threshold),
min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate),
optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate),
max_borrow_rate: u8::from_le_bytes(*config_max_borrow_rate),
fees: ReserveFees {
borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad),
flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad),
host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage),
},
},
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::math::{PERCENT_SCALER, WAD};
use proptest::prelude::*;
use std::cmp::Ordering;
const MAX_LIQUIDITY: u64 = u64::MAX / 5;
prop_compose! {
fn borrow_rates()(optimal_rate in 0..=u8::MAX)(
min_rate in 0..=optimal_rate,
optimal_rate in Just(optimal_rate),
max_rate in optimal_rate..=u8::MAX,
) -> (u8, u8, u8) {
(min_rate, optimal_rate, max_rate)
}
}
prop_compose! {
fn unhealthy_rates()(threshold in 2..=100u8)(
ltv_rate in threshold as u64..=1000u64,
threshold in Just(threshold),
) -> (Decimal, u8) {
(Decimal::from_scaled_val(ltv_rate as u128 * PERCENT_SCALER as u128), threshold)
}
}
prop_compose! {
fn token_conversion_rate()(
conversion_rate in 1..=u16::MAX,
invert_conversion_rate: bool,
) -> Decimal {
let conversion_rate = Decimal::from(conversion_rate as u64);
if invert_conversion_rate {
Decimal::one().try_div(conversion_rate).unwrap()
} else {
conversion_rate
}
}
}
prop_compose! {
fn collateral_exchange_rate_range()(percent in 1..=500u64) -> CollateralExchangeRate {
CollateralExchangeRate(Rate::from_scaled_val(percent * PERCENT_SCALER))
}
}
proptest! {
#[test]
fn current_borrow_rate(
total_liquidity in 0..=MAX_LIQUIDITY,
borrowed_percent in 0..=WAD,
optimal_utilization_rate in 0..=100u8,
(min_borrow_rate, optimal_borrow_rate, max_borrow_rate) in borrow_rates(),
) {
let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
let reserve = Reserve {
liquidity: ReserveLiquidity {
borrowed_amount_wads,
available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?,
..ReserveLiquidity::default()
},
config: ReserveConfig { optimal_utilization_rate, min_borrow_rate, optimal_borrow_rate, max_borrow_rate, ..ReserveConfig::default() },
..Reserve::default()
};
let current_borrow_rate = reserve.current_borrow_rate()?;
assert!(current_borrow_rate >= Rate::from_percent(min_borrow_rate));
assert!(current_borrow_rate <= Rate::from_percent(max_borrow_rate));
let optimal_borrow_rate = Rate::from_percent(optimal_borrow_rate);
let current_rate = reserve.liquidity.utilization_rate()?;
match current_rate.cmp(&Rate::from_percent(optimal_utilization_rate)) {
Ordering::Less => {
if min_borrow_rate == reserve.config.optimal_borrow_rate {
assert_eq!(current_borrow_rate, optimal_borrow_rate);
} else {
assert!(current_borrow_rate < optimal_borrow_rate);
}
}
Ordering::Equal => assert!(current_borrow_rate == optimal_borrow_rate),
Ordering::Greater => {
if max_borrow_rate == reserve.config.optimal_borrow_rate {
assert_eq!(current_borrow_rate, optimal_borrow_rate);
} else {
assert!(current_borrow_rate > optimal_borrow_rate);
}
}
}
}
#[test]
fn current_utilization_rate(
total_liquidity in 0..=MAX_LIQUIDITY,
borrowed_percent in 0..=WAD,
) {
let borrowed_amount_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
let liquidity = ReserveLiquidity {
borrowed_amount_wads,
available_amount: total_liquidity - borrowed_amount_wads.try_round_u64()?,
..ReserveLiquidity::default()
};
let current_rate = liquidity.utilization_rate()?;
assert!(current_rate <= Rate::one());
}
#[test]
fn collateral_exchange_rate(
total_liquidity in 0..=MAX_LIQUIDITY,
borrowed_percent in 0..=WAD,
collateral_multiplier in 0..=(5*WAD),
borrow_rate in 0..=u8::MAX,
) {
let borrowed_liquidity_wads = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(borrowed_percent))?;
let available_liquidity = total_liquidity - borrowed_liquidity_wads.try_round_u64()?;
let mint_total_supply = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(collateral_multiplier))?.try_round_u64()?;
let mut reserve = Reserve {
collateral: ReserveCollateral {
mint_total_supply,
..ReserveCollateral::default()
},
liquidity: ReserveLiquidity {
borrowed_amount_wads: borrowed_liquidity_wads,
available_amount: available_liquidity,
..ReserveLiquidity::default()
},
config: ReserveConfig {
min_borrow_rate: borrow_rate,
optimal_borrow_rate: borrow_rate,
optimal_utilization_rate: 100,
..ReserveConfig::default()
},
..Reserve::default()
};
let exchange_rate = reserve.collateral_exchange_rate()?;
assert!(exchange_rate.0.to_scaled_val() <= 5u128 * WAD as u128);
reserve.accrue_interest(1)?;
let new_exchange_rate = reserve.collateral_exchange_rate()?;
if borrow_rate > 0 && total_liquidity > 0 && borrowed_percent > 0 {
assert!(new_exchange_rate.0 < exchange_rate.0);
} else {
assert_eq!(new_exchange_rate.0, exchange_rate.0);
}
}
#[test]
fn compound_interest(
slots_elapsed in 0..=SLOTS_PER_YEAR,
borrow_rate in 0..=u8::MAX,
) {
let mut reserve = Reserve::default();
let borrow_rate = Rate::from_percent(borrow_rate);
for _ in 0..1000 {
reserve.liquidity.compound_interest(borrow_rate, slots_elapsed)?;
reserve.liquidity.cumulative_borrow_rate_wads.to_scaled_val()?;
}
}
#[test]
fn reserve_accrue_interest(
slots_elapsed in 0..=SLOTS_PER_YEAR,
borrowed_liquidity in 0..=u64::MAX,
borrow_rate in 0..=u8::MAX,
) {
let borrowed_amount_wads = Decimal::from(borrowed_liquidity);
let mut reserve = Reserve {
liquidity: ReserveLiquidity {
borrowed_amount_wads,
..ReserveLiquidity::default()
},
config: ReserveConfig {
max_borrow_rate: borrow_rate,
..ReserveConfig::default()
},
..Reserve::default()
};
reserve.accrue_interest(slots_elapsed)?;
if borrow_rate > 0 && slots_elapsed > 0 {
assert!(reserve.liquidity.borrowed_amount_wads > borrowed_amount_wads);
} else {
assert!(reserve.liquidity.borrowed_amount_wads == borrowed_amount_wads);
}
}
#[test]
fn borrow_fee_calculation(
borrow_fee_wad in 0..WAD, flash_loan_fee_wad in 0..WAD, host_fee_percentage in 0..=100u8,
borrow_amount in 3..=u64::MAX, ) {
let fees = ReserveFees {
borrow_fee_wad,
flash_loan_fee_wad,
host_fee_percentage,
};
let (total_fee, host_fee) = fees.calculate_borrow_fees(Decimal::from(borrow_amount), FeeCalculation::Exclusive)?;
assert!(total_fee <= borrow_amount);
assert!(host_fee <= total_fee);
if borrow_fee_wad > 0 {
assert!(total_fee > 0);
}
if host_fee_percentage == 100 {
assert_eq!(host_fee, total_fee);
}
if host_fee_percentage > 0 && borrow_fee_wad > 0 {
assert!(host_fee > 0);
} else {
assert_eq!(host_fee, 0);
}
}
#[test]
fn flash_loan_fee_calculation(
borrow_fee_wad in 0..WAD, flash_loan_fee_wad in 0..WAD, host_fee_percentage in 0..=100u8,
borrow_amount in 3..=u64::MAX, ) {
let fees = ReserveFees {
borrow_fee_wad,
flash_loan_fee_wad,
host_fee_percentage,
};
let (total_fee, host_fee) = fees.calculate_flash_loan_fees(Decimal::from(borrow_amount))?;
assert!(total_fee <= borrow_amount);
assert!(host_fee <= total_fee);
if borrow_fee_wad > 0 {
assert!(total_fee > 0);
}
if host_fee_percentage == 100 {
assert_eq!(host_fee, total_fee);
}
if host_fee_percentage > 0 && borrow_fee_wad > 0 {
assert!(host_fee > 0);
} else {
assert_eq!(host_fee, 0);
}
}
}
#[test]
fn borrow_fee_calculation_min_host() {
let fees = ReserveFees {
borrow_fee_wad: 10_000_000_000_000_000, flash_loan_fee_wad: 0,
host_fee_percentage: 20,
};
let err = fees
.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive)
.unwrap_err();
assert_eq!(err, LendingError::BorrowTooSmall.into());
let err = fees
.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive)
.unwrap_err();
assert_eq!(err, LendingError::BorrowTooSmall.into());
let (total_fee, host_fee) = fees
.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive)
.unwrap();
assert_eq!(total_fee, 0);
assert_eq!(host_fee, 0);
}
#[test]
fn borrow_fee_calculation_min_no_host() {
let fees = ReserveFees {
borrow_fee_wad: 10_000_000_000_000_000, flash_loan_fee_wad: 0,
host_fee_percentage: 0,
};
let (total_fee, host_fee) = fees
.calculate_borrow_fees(Decimal::from(2u64), FeeCalculation::Exclusive)
.unwrap();
assert_eq!(total_fee, 1);
assert_eq!(host_fee, 0);
let err = fees
.calculate_borrow_fees(Decimal::one(), FeeCalculation::Exclusive)
.unwrap_err();
assert_eq!(err, LendingError::BorrowTooSmall.into());
let (total_fee, host_fee) = fees
.calculate_borrow_fees(Decimal::zero(), FeeCalculation::Exclusive)
.unwrap();
assert_eq!(total_fee, 0);
assert_eq!(host_fee, 0);
}
#[test]
fn borrow_fee_calculation_host() {
let fees = ReserveFees {
borrow_fee_wad: 10_000_000_000_000_000, flash_loan_fee_wad: 0,
host_fee_percentage: 20,
};
let (total_fee, host_fee) = fees
.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive)
.unwrap();
assert_eq!(total_fee, 10); assert_eq!(host_fee, 2); }
#[test]
fn borrow_fee_calculation_no_host() {
let fees = ReserveFees {
borrow_fee_wad: 10_000_000_000_000_000, flash_loan_fee_wad: 0,
host_fee_percentage: 0,
};
let (total_fee, host_fee) = fees
.calculate_borrow_fees(Decimal::from(1000u64), FeeCalculation::Exclusive)
.unwrap();
assert_eq!(total_fee, 10); assert_eq!(host_fee, 0); }
}