diff --git a/token-lending/program/src/dex_market.rs b/token-lending/program/src/dex_market.rs index 565be9d6..41251849 100644 --- a/token-lending/program/src/dex_market.rs +++ b/token-lending/program/src/dex_market.rs @@ -3,7 +3,7 @@ use crate::{ error::LendingError, instruction::BorrowAmountType, - math::{Decimal, Rate}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, state::Reserve, }; use arrayref::{array_refs, mut_array_refs}; @@ -148,10 +148,10 @@ impl<'a> TradeSimulator<'a> { return Err(LendingError::DexInvalidOrderBookSide.into()); } - let input_quantity: Decimal = quantity / self.dex_market.get_lots(currency); + let input_quantity: Decimal = quantity.try_div(self.dex_market.get_lots(currency))?; let output_quantity = self.exchange_with_order_book(input_quantity, currency, cache_orders)?; - Ok(output_quantity * self.dex_market.get_lots(currency.opposite())) + Ok(output_quantity.try_mul(self.dex_market.get_lots(currency.opposite()))?) } /// Exchange tokens by filling orders @@ -180,15 +180,15 @@ impl<'a> TradeSimulator<'a> { let (filled, output) = if currency == Currency::Base { let filled = input_quantity.min(Decimal::from(base_quantity)); - (filled, filled * next_order_price) + (filled, filled.try_mul(next_order_price)?) } else { - let quote_quantity = base_quantity as u128 * next_order_price as u128; - let filled = input_quantity.min(Decimal::from(quote_quantity)); - (filled, filled / next_order_price) + let quote_quantity = Decimal::from(base_quantity).try_mul(next_order_price)?; + let filled = input_quantity.min(quote_quantity); + (filled, filled.try_div(next_order_price)?) }; - input_quantity -= filled; - output_quantity += output; + input_quantity = input_quantity.try_sub(filled)?; + output_quantity = output_quantity.try_add(output)?; if cache_orders { order_cache.push_back(next_order); @@ -214,7 +214,7 @@ impl<'a> TradeSimulator<'a> { amount: u64, ) -> Result<(u64, u64), ProgramError> { let deposit_reserve_collateral_exchange_rate = - deposit_reserve.state.collateral_exchange_rate(); + deposit_reserve.state.collateral_exchange_rate()?; match amount_type { BorrowAmountType::LiquidityBorrowAmount => { let borrow_amount = amount; @@ -229,11 +229,12 @@ impl<'a> TradeSimulator<'a> { )?; let loan_in_deposit_collateral = deposit_reserve_collateral_exchange_rate - .decimal_liquidity_to_collateral(loan_in_deposit_underlying); - let required_deposit_collateral: Decimal = loan_in_deposit_collateral - / Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); + .decimal_liquidity_to_collateral(loan_in_deposit_underlying)?; + let required_deposit_collateral: Decimal = loan_in_deposit_collateral.try_div( + Rate::from_percent(deposit_reserve.config.loan_to_value_ratio), + )?; - let collateral_deposit_amount = required_deposit_collateral.round_u64(); + let collateral_deposit_amount = required_deposit_collateral.try_round_u64()?; if collateral_deposit_amount == 0 { return Err(LendingError::InvalidAmount.into()); } @@ -244,9 +245,11 @@ impl<'a> TradeSimulator<'a> { let collateral_deposit_amount = amount; let loan_in_deposit_collateral: Decimal = Decimal::from(collateral_deposit_amount) - * Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); + .try_mul(Rate::from_percent( + deposit_reserve.config.loan_to_value_ratio, + ))?; let loan_in_deposit_underlying = deposit_reserve_collateral_exchange_rate - .decimal_collateral_to_liquidity(loan_in_deposit_collateral); + .decimal_collateral_to_liquidity(loan_in_deposit_collateral)?; // Simulate selling `loan_in_deposit_underlying` amount of deposit reserve underlying // tokens to determine how much to lend to the user @@ -257,7 +260,7 @@ impl<'a> TradeSimulator<'a> { false, )?; - let borrow_amount = borrow_amount.round_u64(); + let borrow_amount = borrow_amount.try_round_u64()?; if borrow_amount == 0 { return Err(LendingError::InvalidAmount.into()); } diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index cc90fe99..126703c1 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -46,6 +46,12 @@ pub enum LendingError { /// Invalid account input #[error("Invalid account input")] InvalidAccountInput, + /// Math operation overflow + #[error("Math operation overflow")] + MathOverflow, + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, /// Memory is too small #[error("Memory is too small")] @@ -80,9 +86,6 @@ pub enum LendingError { /// Borrow amount too small #[error("Borrow amount too small")] BorrowTooSmall, - /// Negative interest rate - #[error("Interest rate cannot be negative")] - NegativeInterestRate, /// Trade simulation error #[error("Trade simulation error")] diff --git a/token-lending/program/src/math/common.rs b/token-lending/program/src/math/common.rs index be5f129e..878e224f 100644 --- a/token-lending/program/src/math/common.rs +++ b/token-lending/program/src/math/common.rs @@ -1,4 +1,6 @@ -//! Common constants used for both Decimal and Rate +//! Common module for Decimal and Rate + +use solana_program::program_error::ProgramError; /// Scale of precision pub const SCALE: usize = 18; @@ -8,3 +10,27 @@ pub const WAD: u64 = 1_000_000_000_000_000_000; pub const HALF_WAD: u64 = 500_000_000_000_000_000; /// Scale for percentages pub const PERCENT_SCALER: u64 = 10_000_000_000_000_000; + +/// Try to subtract, return an error on underflow +pub trait TrySub: Sized { + /// Subtract + fn try_sub(self, rhs: Self) -> Result; +} + +/// Try to subtract, return an error on overflow +pub trait TryAdd: Sized { + /// Add + fn try_add(self, rhs: Self) -> Result; +} + +/// Try to divide, return an error on overflow or divide by zero +pub trait TryDiv: Sized { + /// Divide + fn try_div(self, rhs: RHS) -> Result; +} + +/// Try to multiply, return an error on overflow +pub trait TryMul: Sized { + /// Multiply + fn try_mul(self, rhs: RHS) -> Result; +} diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index 95fea5ef..c19e1661 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -1,11 +1,21 @@ -//! Math for preserving precision +//! Math for preserving precision of token amounts which are limited +//! by the SPL Token program to be at most u64::MAX. +//! +//! Decimals are internally scaled by a WAD (10^18) to preserve +//! precision up to 18 decimal places. Decimals are sized to support +//! both serialization and precise math for the full range of +//! unsigned 64-bit integers. The underlying representation is a +//! u192 rather than u256 to reduce compute cost while losing +//! support for arithmetic operations at the high end of u64 range. #![allow(clippy::assign_op_pattern)] #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::manual_range_contains)] -use crate::math::common::*; use crate::math::Rate; +use crate::{error::LendingError, math::common::*}; +use solana_program::program_error::ProgramError; +use std::convert::TryFrom; use std::fmt; use uint::construct_uint; @@ -14,7 +24,7 @@ construct_uint! { pub struct U192(3); } -/// Large decimal value precise to 18 digits +/// Large decimal values, precise to 18 digits #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] pub struct Decimal(pub U192); @@ -29,12 +39,12 @@ impl Decimal { Self(U192::zero()) } - // TODO: use const slices when fixed + // OPTIMIZE: use const slice when fixed in BPF toolchain fn wad() -> U192 { U192::from(WAD) } - // TODO: use const slices when fixed + // OPTIMIZE: use const slice when fixed in BPF toolchain fn half_wad() -> U192 { U192::from(HALF_WAD) } @@ -44,20 +54,9 @@ impl Decimal { Self(U192::from(percent as u64 * PERCENT_SCALER)) } - /// Create scaled decimal from value and scale - pub fn new(val: u64, scale: usize) -> Self { - assert!(scale <= SCALE); - Self(Self::wad() / U192::exp10(scale) * U192::from(val)) - } - - /// Convert to lower precision rate - pub fn as_rate(self) -> Rate { - Rate::from_scaled_val(self.0.as_u64() as u128) - } - - /// Return raw scaled value - pub fn to_scaled_val(&self) -> u128 { - self.0.as_u128() + /// Return raw scaled value if it fits within u128 + pub fn to_scaled_val(&self) -> Result { + Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?) } /// Create decimal from scaled value @@ -66,8 +65,13 @@ impl Decimal { } /// Round scaled decimal to u64 - pub fn round_u64(&self) -> u64 { - ((Self::half_wad() + self.0) / Self::wad()).as_u64() + pub fn try_round_u64(&self) -> Result { + let rounded_val = Self::half_wad() + .checked_add(self.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?; + Ok(u64::try_from(rounded_val).map_err(|_| LendingError::MathOverflow)?) } } @@ -102,76 +106,79 @@ impl From for Decimal { } } -impl std::ops::Add for Decimal { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) +impl TryAdd for Decimal { + fn try_add(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_add(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Sub for Decimal { - type Output = Self; - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) +impl TrySub for Decimal { + fn try_sub(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_sub(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Div for Decimal { - type Output = Self; - fn div(self, rhs: u64) -> Self::Output { - Self(self.0 / U192::from(rhs)) +impl TryDiv for Decimal { + fn try_div(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_div(U192::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Div for Decimal { - type Output = Self; - fn div(self, rhs: Rate) -> Self::Output { - self / Self::from(rhs) +impl TryDiv for Decimal { + fn try_div(self, rhs: Rate) -> Result { + self.try_div(Self::from(rhs)) } } -impl std::ops::Div for Decimal { - type Output = Self; - fn div(self, rhs: Self) -> Self::Output { - Self(Self::wad() * self.0 / rhs.0) +impl TryDiv for Decimal { + fn try_div(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(Self::wad()) + .ok_or(LendingError::MathOverflow)? + .checked_div(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Mul for Decimal { - type Output = Self; - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * U192::from(rhs)) +impl TryMul for Decimal { + fn try_mul(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_mul(U192::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Mul for Decimal { - type Output = Self; - fn mul(self, rhs: Rate) -> Self::Output { - Self(self.0 * U192::from(rhs.to_scaled_val()) / Self::wad()) +impl TryMul for Decimal { + fn try_mul(self, rhs: Rate) -> Result { + self.try_mul(Self::from(rhs)) } } -impl std::ops::AddAssign for Decimal { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl std::ops::SubAssign for Decimal { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl std::ops::DivAssign for Decimal { - fn div_assign(&mut self, rhs: Self) { - *self = *self / rhs; - } -} - -impl std::ops::MulAssign for Decimal { - fn mul_assign(&mut self, rhs: Rate) { - *self = *self * rhs; +impl TryMul for Decimal { + fn try_mul(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(rhs.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?, + )) } } diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 29b8d5ce..36674d83 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -1,12 +1,29 @@ -//! Math for preserving precision +//! Math for preserving precision of ratios and percentages. +//! +//! Usages and their ranges include: +//! - Collateral exchange ratio <= 5.0 +//! - Loan to value ratio <= 0.9 +//! - Max borrow rate <= 2.56 +//! - Percentages <= 1.0 +//! +//! Rates are internally scaled by a WAD (10^18) to preserve +//! precision up to 18 decimal places. Rates are sized to support +//! both serialization and precise math for the full range of +//! unsigned 8-bit integers. The underlying representation is a +//! u128 rather than u192 to reduce compute cost while losing +//! support for arithmetic operations at the high end of u8 range. #![allow(clippy::assign_op_pattern)] #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::reversed_empty_ranges)] #![allow(clippy::manual_range_contains)] -use crate::math::common::*; -use std::fmt; +use crate::{ + error::LendingError, + math::{common::*, decimal::Decimal}, +}; +use solana_program::program_error::ProgramError; +use std::{convert::TryFrom, fmt}; use uint::construct_uint; // U128 with 128 bits consisting of 2 x 64-bit words @@ -14,7 +31,7 @@ construct_uint! { pub struct U128(2); } -/// Small decimal value precise to 18 digits +/// Small decimal values, precise to 18 digits #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] pub struct Rate(pub U128); @@ -29,12 +46,12 @@ impl Rate { Self(U128::from(0)) } - // TODO: use const slices when fixed + // OPTIMIZE: use const slice when fixed in BPF toolchain fn wad() -> U128 { U128::from(WAD) } - // TODO: use const slices when fixed + // OPTIMIZE: use const slice when fixed in BPF toolchain fn half_wad() -> U128 { U128::from(HALF_WAD) } @@ -44,29 +61,28 @@ impl Rate { Self(U128::from(percent as u64 * PERCENT_SCALER)) } - /// Create scaled decimal from value and scale - pub fn new(val: u64, scale: usize) -> Self { - assert!(scale <= SCALE); - Self(Self::wad() / U128::exp10(scale) * U128::from(val)) - } - /// Return raw scaled value pub fn to_scaled_val(&self) -> u128 { self.0.as_u128() } /// Create decimal from scaled value - pub fn from_scaled_val(scaled_val: u128) -> Self { + pub fn from_scaled_val(scaled_val: u64) -> Self { Self(U128::from(scaled_val)) } /// Round scaled decimal to u64 - pub fn round_u64(&self) -> u64 { - ((Self::half_wad() + self.0) / Self::wad()).as_u64() + pub fn try_round_u64(&self) -> Result { + let rounded_val = Self::half_wad() + .checked_add(self.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?; + Ok(u64::try_from(rounded_val).map_err(|_| LendingError::MathOverflow)?) } /// Calculates base^exp - pub fn pow(&self, mut exp: u64) -> Rate { + pub fn try_pow(&self, mut exp: u64) -> Result { let mut base = *self; let mut ret = if exp % 2 != 0 { base @@ -76,14 +92,14 @@ impl Rate { while exp > 0 { exp /= 2; - base *= base; + base = base.try_mul(base)?; if exp % 2 != 0 { - ret *= base; + ret = ret.try_mul(base)?; } } - ret + Ok(ret) } } @@ -100,93 +116,83 @@ impl fmt::Display for Rate { } } -// TODO: assert that `val` doesn't exceed max u64 wad (~1844) -impl From for Rate { - fn from(val: u64) -> Self { - Self(Self::wad() * U128::from(val)) +impl TryFrom for Rate { + type Error = ProgramError; + fn try_from(decimal: Decimal) -> Result { + Ok(Self(U128::from(decimal.to_scaled_val()?))) } } -impl std::ops::Add for Rate { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) +impl TryAdd for Rate { + fn try_add(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_add(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Sub for Rate { - type Output = Self; - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) +impl TrySub for Rate { + fn try_sub(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_sub(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Div for Rate { - type Output = Self; - fn div(self, rhs: u8) -> Self::Output { - Self(self.0 / U128::from(rhs)) +impl TryDiv for Rate { + fn try_div(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_div(U128::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) } } -impl std::ops::Div for Rate { - type Output = Self; - fn div(self, rhs: u64) -> Self::Output { - Self(self.0 / U128::from(rhs)) +impl TryDiv for Rate { + fn try_div(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(Self::wad()) + .ok_or(LendingError::MathOverflow)? + .checked_div(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) } } -// TODO: Returned rate could cause overflows -impl std::ops::Div for Rate { - type Output = Self; - fn div(self, rhs: Self) -> Self::Output { - Self(Self::wad() * self.0 / rhs.0) +impl TryMul for Rate { + fn try_mul(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_mul(U128::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) } } -// TODO: Returned rate could cause overflows -impl std::ops::Mul for Rate { - type Output = Self; - fn mul(self, rhs: u8) -> Self::Output { - Self(self.0 * U128::from(rhs)) +impl TryMul for Rate { + fn try_mul(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(rhs.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?, + )) } } -// TODO: Returned rate could cause overflows -impl std::ops::Mul for Rate { - type Output = Self; - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * U128::from(rhs)) - } -} +#[cfg(test)] +mod test { + use super::*; -// TODO: Returned rate could cause overflows -impl std::ops::Mul for Rate { - type Output = Self; - fn mul(self, rhs: Self) -> Self::Output { - Self(self.0 * rhs.0 / Self::wad()) - } -} - -impl std::ops::AddAssign for Rate { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl std::ops::SubAssign for Rate { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl std::ops::DivAssign for Rate { - fn div_assign(&mut self, rhs: Self) { - *self = *self / rhs; - } -} - -impl std::ops::MulAssign for Rate { - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; + #[test] + fn checked_pow() { + assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); } } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index afc412df..3fbe7637 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -4,7 +4,7 @@ use crate::{ dex_market::{DexMarket, TradeAction, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, instruction::{BorrowAmountType, LendingInstruction}, - math::{Decimal, Rate, WAD}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, state::{LendingMarket, Obligation, Reserve, ReserveConfig, ReserveState, PROGRAM_VERSION}, }; use num_traits::FromPrimitive; @@ -103,6 +103,7 @@ fn process_init_reserve( accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { + msg!("Reserve must be initialized with liquidity"); return Err(LendingError::InvalidAmount.into()); } if config.optimal_utilization_rate > 100 { @@ -255,7 +256,7 @@ fn process_init_reserve( token_program: token_program_id.clone(), })?; - let reserve_state = ReserveState::new(clock.slot, liquidity_amount); + let reserve_state = ReserveState::new(clock.slot, liquidity_amount)?; spl_token_mint_to(TokenMintToParams { mint: reserve_collateral_mint_info.clone(), destination: destination_collateral_info.clone(), @@ -339,8 +340,8 @@ fn process_deposit( return Err(LendingError::InvalidAccountInput.into()); } - reserve.accrue_interest(clock.slot); - let collateral_amount = reserve.deposit_liquidity(liquidity_amount); + reserve.accrue_interest(clock.slot)?; + let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; let authority_signer_seeds = &[ @@ -428,7 +429,7 @@ fn process_withdraw( return Err(LendingError::InvalidAccountInput.into()); } - reserve.accrue_interest(clock.slot); + reserve.accrue_interest(clock.slot)?; let liquidity_withdraw_amount = reserve.redeem_collateral(collateral_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; @@ -569,8 +570,8 @@ fn process_borrow( } // accrue interest and update rates - borrow_reserve.accrue_interest(clock.slot); - deposit_reserve.accrue_interest(clock.slot); + borrow_reserve.accrue_interest(clock.slot)?; + deposit_reserve.accrue_interest(clock.slot)?; let cumulative_borrow_rate = borrow_reserve.state.cumulative_borrow_rate_wads; let mut trade_simulator = TradeSimulator::new( @@ -618,7 +619,9 @@ fn process_borrow( } obligation.accrue_interest(cumulative_borrow_rate)?; - obligation.borrowed_liquidity_wads += Decimal::from(borrow_amount); + obligation.borrowed_liquidity_wads = obligation + .borrowed_liquidity_wads + .try_add(Decimal::from(borrow_amount))?; obligation.deposited_collateral_tokens += collateral_deposit_amount; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; } else { @@ -834,26 +837,27 @@ fn process_repay( } // accrue interest and update rates - repay_reserve.accrue_interest(clock.slot); + repay_reserve.accrue_interest(clock.slot)?; obligation.accrue_interest(repay_reserve.state.cumulative_borrow_rate_wads)?; let repay_amount = Decimal::from(liquidity_amount).min(obligation.borrowed_liquidity_wads); let rounded_repay_amount = repay_reserve.state.subtract_repay(repay_amount)?; Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; - let repay_pct: Decimal = repay_amount / obligation.borrowed_liquidity_wads; + let repay_pct: Decimal = repay_amount.try_div(obligation.borrowed_liquidity_wads)?; let collateral_withdraw_amount = { - let withdraw_amount: Decimal = repay_pct * obligation.deposited_collateral_tokens; - withdraw_amount.round_u64() + let withdraw_amount: Decimal = repay_pct.try_mul(obligation.deposited_collateral_tokens)?; + withdraw_amount.try_round_u64()? }; let obligation_token_amount = { let obligation_mint = &unpack_mint(&obligation_token_mint_info.data.borrow())?; - let token_amount: Decimal = repay_pct * obligation_mint.supply; - token_amount.round_u64() + let token_amount: Decimal = repay_pct.try_mul(obligation_mint.supply)?; + token_amount.try_round_u64()? }; - obligation.borrowed_liquidity_wads -= repay_amount; + obligation.borrowed_liquidity_wads = + obligation.borrowed_liquidity_wads.try_sub(repay_amount)?; obligation.deposited_collateral_tokens -= collateral_withdraw_amount; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; @@ -1003,8 +1007,8 @@ fn process_liquidate( } // accrue interest and update rates - repay_reserve.accrue_interest(clock.slot); - withdraw_reserve.accrue_interest(clock.slot); + repay_reserve.accrue_interest(clock.slot)?; + withdraw_reserve.accrue_interest(clock.slot)?; obligation.accrue_interest(repay_reserve.state.cumulative_borrow_rate_wads)?; let mut trade_simulator = TradeSimulator::new( @@ -1016,15 +1020,15 @@ fn process_liquidate( // calculate obligation health let withdraw_reserve_collateral_exchange_rate = - withdraw_reserve.state.collateral_exchange_rate(); + withdraw_reserve.state.collateral_exchange_rate()?; let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate .decimal_liquidity_to_collateral(trade_simulator.simulate_trade( TradeAction::Sell, obligation.borrowed_liquidity_wads, &repay_reserve.liquidity_mint, true, - )?) - .round_u64(); + )?)? + .try_round_u64()?; if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens < withdraw_reserve.config.liquidation_threshold as u64 @@ -1034,8 +1038,8 @@ fn process_liquidate( // calculate the amount of liquidity that will be repaid let close_factor = Rate::from_percent(50); - let repay_amount = - Decimal::from(liquidity_amount).min(obligation.borrowed_liquidity_wads * close_factor); + let repay_amount = Decimal::from(liquidity_amount) + .min(obligation.borrowed_liquidity_wads.try_mul(close_factor)?); let rounded_repay_amount = repay_reserve.state.subtract_repay(repay_amount)?; // TODO: check math precision @@ -1048,8 +1052,8 @@ fn process_liquidate( false, )?; let withdraw_amount_as_collateral = withdraw_reserve_collateral_exchange_rate - .decimal_liquidity_to_collateral(withdraw_liquidity_amount) - .round_u64(); + .decimal_liquidity_to_collateral(withdraw_liquidity_amount)? + .try_round_u64()?; let liquidation_bonus_amount = withdraw_amount_as_collateral * (withdraw_reserve.config.liquidation_bonus as u64) / 100; let collateral_withdraw_amount = obligation @@ -1062,7 +1066,8 @@ fn process_liquidate( &mut withdraw_reserve_info.data.borrow_mut(), )?; - obligation.borrowed_liquidity_wads -= repay_amount; + obligation.borrowed_liquidity_wads = + obligation.borrowed_liquidity_wads.try_sub(repay_amount)?; obligation.deposited_collateral_tokens -= collateral_withdraw_amount; Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; diff --git a/token-lending/program/src/state.rs b/token-lending/program/src/state.rs index 9096ffd9..576ca5f0 100644 --- a/token-lending/program/src/state.rs +++ b/token-lending/program/src/state.rs @@ -2,20 +2,23 @@ use crate::{ error::LendingError, - math::{Decimal, Rate, SCALE}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, }; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ clock::{Slot, DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, entrypoint::ProgramResult, + msg, program_error::ProgramError, program_option::COption, program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; +use std::convert::{TryFrom, TryInto}; /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) -pub const INITIAL_COLLATERAL_RATE: u64 = 5; +pub const INITIAL_COLLATERAL_RATIO: u64 = 5; +const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; /// Current version of the program and all new accounts created pub const PROGRAM_VERSION: u8 = 1; @@ -67,7 +70,7 @@ impl ReserveFees { &self, collateral_amount: u64, ) -> Result<(u64, u64), ProgramError> { - let borrow_fee_rate = Rate::new(self.borrow_fee_wad, SCALE); + let borrow_fee_rate = Rate::from_scaled_val(self.borrow_fee_wad); let host_fee_rate = Rate::from_percent(self.host_fee_percentage); if borrow_fee_rate > Rate::zero() && collateral_amount > 0 { let need_to_assess_host_fee = host_fee_rate > Rate::zero(); @@ -76,15 +79,18 @@ impl ReserveFees { } else { 1 // 1 token to owner, nothing else }; - let borrow_fee = std::cmp::max( - minimum_fee, - (borrow_fee_rate * collateral_amount).round_u64(), - ); + + let borrow_fee = borrow_fee_rate + .try_mul(collateral_amount)? + .try_round_u64()? + .max(minimum_fee); + let host_fee = if need_to_assess_host_fee { - std::cmp::max(1, (host_fee_rate * borrow_fee).round_u64()) + host_fee_rate.try_mul(borrow_fee)?.try_round_u64()?.max(1) } else { 0 }; + if borrow_fee >= collateral_amount { Err(LendingError::BorrowTooSmall.into()) } else { @@ -134,14 +140,20 @@ pub struct ReserveState { impl ReserveState { /// Initialize new reserve state - pub fn new(current_slot: Slot, liquidity_amount: u64) -> Self { - Self { + pub fn new(current_slot: Slot, liquidity_amount: u64) -> Result { + let collateral_mint_supply = liquidity_amount + .checked_mul(INITIAL_COLLATERAL_RATIO) + .ok_or_else(|| { + msg!("Collateral token supply overflow"); + ProgramError::from(LendingError::MathOverflow) + })?; + Ok(Self { last_update_slot: current_slot, cumulative_borrow_rate_wads: Decimal::one(), available_liquidity: liquidity_amount, - collateral_mint_supply: INITIAL_COLLATERAL_RATE * liquidity_amount, // TODO check overflow + collateral_mint_supply, borrowed_liquidity_wads: Decimal::zero(), - } + }) } } @@ -181,23 +193,31 @@ pub struct CollateralExchangeRate(Rate); impl CollateralExchangeRate { /// Convert reserve collateral to liquidity - pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> u64 { - (Decimal::from(collateral_amount) / self.0).round_u64() + pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result { + Decimal::from(collateral_amount) + .try_div(self.0)? + .try_round_u64() } /// Convert reserve collateral to liquidity - pub fn decimal_collateral_to_liquidity(&self, collateral_amount: Decimal) -> Decimal { - collateral_amount / self.0 + pub fn decimal_collateral_to_liquidity( + &self, + collateral_amount: Decimal, + ) -> Result { + collateral_amount.try_div(self.0) } /// Convert reserve liquidity to collateral - pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> u64 { - (self.0 * liquidity_amount).round_u64() + pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> Result { + self.0.try_mul(liquidity_amount)?.try_round_u64() } /// Convert reserve liquidity to collateral - pub fn decimal_liquidity_to_collateral(&self, liquidity_amount: Decimal) -> Decimal { - liquidity_amount * self.0 + pub fn decimal_liquidity_to_collateral( + &self, + liquidity_amount: Decimal, + ) -> Result { + liquidity_amount.try_mul(self.0) } } @@ -215,47 +235,61 @@ impl ReserveState { } self.available_liquidity -= borrow_amount; - self.borrowed_liquidity_wads += Decimal::from(borrow_amount); + self.borrowed_liquidity_wads = self + .borrowed_liquidity_wads + .try_add(Decimal::from(borrow_amount))?; Ok(()) } /// Subtract repay amount from total borrows and return rounded repay value pub fn subtract_repay(&mut self, repay_amount: Decimal) -> Result { - let rounded_repay_amount = repay_amount.round_u64(); + let rounded_repay_amount = repay_amount.try_round_u64()?; if rounded_repay_amount == 0 { return Err(LendingError::ObligationTooSmall.into()); } - self.available_liquidity += rounded_repay_amount; - self.borrowed_liquidity_wads -= repay_amount; + self.available_liquidity = self + .available_liquidity + .checked_add(rounded_repay_amount) + .ok_or(LendingError::MathOverflow)?; + self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?; Ok(rounded_repay_amount) } /// Calculate the current utilization rate of the reserve - pub fn current_utilization_rate(&self) -> Rate { + pub fn current_utilization_rate(&self) -> Result { let available_liquidity = Decimal::from(self.available_liquidity); - let total_supply = self.borrowed_liquidity_wads + available_liquidity; + let total_supply = self.borrowed_liquidity_wads.try_add(available_liquidity)?; let zero = Decimal::zero(); if total_supply == zero { - return Rate::zero(); + return Ok(Rate::zero()); } - (self.borrowed_liquidity_wads / total_supply).as_rate() + self.borrowed_liquidity_wads + .try_div(total_supply)? + .try_into() } - // TODO: is exchange rate fixed within a slot? /// Return the current collateral exchange rate. - pub fn collateral_exchange_rate(&self) -> CollateralExchangeRate { - if self.collateral_mint_supply == 0 { - CollateralExchangeRate(Rate::from(INITIAL_COLLATERAL_RATE)) + pub fn collateral_exchange_rate(&self) -> Result { + let rate = if self.collateral_mint_supply == 0 { + Rate::from_scaled_val(INITIAL_COLLATERAL_RATE) } else { let collateral_supply = Decimal::from(self.collateral_mint_supply); - let total_supply = - self.borrowed_liquidity_wads + Decimal::from(self.available_liquidity); - CollateralExchangeRate((collateral_supply / total_supply).as_rate()) - } + let total_supply = self + .borrowed_liquidity_wads + .try_add(Decimal::from(self.available_liquidity))?; + + if total_supply == Decimal::zero() { + Rate::from_scaled_val(INITIAL_COLLATERAL_RATE) + } else { + Rate::try_from(collateral_supply.try_div(total_supply)?)? + } + }; + + Ok(CollateralExchangeRate(rate)) } /// Return slots elapsed since last update @@ -265,48 +299,67 @@ impl ReserveState { slots_elapsed } - fn apply_interest(&mut self, compounded_interest_rate: Rate) { - self.borrowed_liquidity_wads *= compounded_interest_rate; - self.cumulative_borrow_rate_wads *= compounded_interest_rate; + /// Compound current borrow rate over elapsed slots + fn compound_interest( + &mut self, + current_borrow_rate: Rate, + slots_elapsed: u64, + ) -> Result { + let slot_interest_rate: 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)?; + Ok(compounded_interest_rate) } } impl Reserve { /// Calculate the current borrow rate - pub fn current_borrow_rate(&self) -> Rate { - let utilization_rate = self.state.current_utilization_rate(); + pub fn current_borrow_rate(&self) -> Result { + let utilization_rate = self.state.current_utilization_rate()?; let optimal_utilization_rate = Rate::from_percent(self.config.optimal_utilization_rate); - if self.config.optimal_utilization_rate == 100 - || utilization_rate < optimal_utilization_rate - { - let normalized_rate = utilization_rate / optimal_utilization_rate; - normalized_rate - * Rate::from_percent(self.config.optimal_borrow_rate - self.config.min_borrow_rate) - + Rate::from_percent(self.config.min_borrow_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 - self.config.min_borrow_rate); + + Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?) } else { - let normalized_rate = (utilization_rate - optimal_utilization_rate) - / Rate::from_percent(100 - self.config.optimal_utilization_rate); - normalized_rate - * Rate::from_percent(self.config.max_borrow_rate - self.config.optimal_borrow_rate) - + Rate::from_percent(self.config.optimal_borrow_rate) + let normalized_rate = utilization_rate + .try_sub(optimal_utilization_rate)? + .try_div(Rate::from_percent( + 100 - self.config.optimal_utilization_rate, + ))?; + let min_rate = Rate::from_percent(self.config.optimal_borrow_rate); + let rate_range = + Rate::from_percent(self.config.max_borrow_rate - self.config.optimal_borrow_rate); + + Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?) } } /// Record deposited liquidity and return amount of collateral tokens to mint - pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> u64 { - let collateral_exchange_rate = self.state.collateral_exchange_rate(); - let collateral_amount = collateral_exchange_rate.liquidity_to_collateral(liquidity_amount); + pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result { + let collateral_exchange_rate = self.state.collateral_exchange_rate()?; + let collateral_amount = + collateral_exchange_rate.liquidity_to_collateral(liquidity_amount)?; self.state.available_liquidity += liquidity_amount; self.state.collateral_mint_supply += collateral_amount; - collateral_amount + Ok(collateral_amount) } /// Record redeemed collateral and return amount of liquidity to withdraw pub fn redeem_collateral(&mut self, collateral_amount: u64) -> Result { - let collateral_exchange_rate = self.state.collateral_exchange_rate(); - let liquidity_amount = collateral_exchange_rate.collateral_to_liquidity(collateral_amount); + let collateral_exchange_rate = self.state.collateral_exchange_rate()?; + let liquidity_amount = + collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?; if liquidity_amount > self.state.available_liquidity { return Err(LendingError::InsufficientLiquidity.into()); } @@ -318,14 +371,19 @@ impl Reserve { } /// Update borrow rate and accrue interest - pub fn accrue_interest(&mut self, current_slot: Slot) { + pub fn accrue_interest(&mut self, current_slot: Slot) -> Result<(), ProgramError> { let slots_elapsed = self.state.update_slot(current_slot); if slots_elapsed > 0 { - let borrow_rate = self.current_borrow_rate(); - let slot_interest_rate: Rate = borrow_rate / SLOTS_PER_YEAR; - let compounded_interest_rate = (Rate::one() + slot_interest_rate).pow(slots_elapsed); - self.state.apply_interest(compounded_interest_rate); + let current_borrow_rate = self.current_borrow_rate()?; + let compounded_interest_rate = self + .state + .compound_interest(current_borrow_rate, slots_elapsed)?; + self.state.borrowed_liquidity_wads = self + .state + .borrowed_liquidity_wads + .try_mul(compounded_interest_rate)?; } + Ok(()) } } @@ -351,15 +409,20 @@ pub struct Obligation { impl Obligation { /// Accrue interest pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> Result<(), ProgramError> { - let compounded_interest_rate: Rate = - (cumulative_borrow_rate / self.cumulative_borrow_rate_wads).as_rate(); - - if compounded_interest_rate < Rate::one() { + if cumulative_borrow_rate < self.cumulative_borrow_rate_wads { return Err(LendingError::NegativeInterestRate.into()); } - self.borrowed_liquidity_wads *= compounded_interest_rate; + let compounded_interest_rate: Rate = cumulative_borrow_rate + .try_div(self.cumulative_borrow_rate_wads)? + .try_into()?; + + self.borrowed_liquidity_wads = self + .borrowed_liquidity_wads + .try_mul(compounded_interest_rate)?; + self.cumulative_borrow_rate_wads = cumulative_borrow_rate; + Ok(()) } } @@ -628,7 +691,10 @@ fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { } fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { - *dst = decimal.to_scaled_val().to_le_bytes(); + *dst = decimal + .to_scaled_val() + .expect("could not pack decimal") + .to_le_bytes(); } fn unpack_decimal(src: &[u8; 16]) -> Decimal { @@ -641,7 +707,174 @@ mod test { use crate::math::WAD; use proptest::prelude::*; + const MAX_LIQUIDITY: u64 = u64::MAX / 5; + const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000% + + #[test] + fn obligation_accrue_interest_failure() { + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::zero(), + ..Obligation::default() + } + .accrue_interest(Decimal::one()), + Err(LendingError::MathOverflow.into()) + ); + + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::from(2u64), + ..Obligation::default() + } + .accrue_interest(Decimal::one()), + Err(LendingError::NegativeInterestRate.into()) + ); + + assert_eq!( + Obligation { + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_liquidity_wads: Decimal::from(u64::MAX), + ..Obligation::default() + } + .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), + Err(LendingError::MathOverflow.into()) + ); + } + + // Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1 + fn cumulative_rates() -> impl Strategy { + prop::num::u128::ANY.prop_flat_map(|rate| { + let current_rate = rate.max(1); + let max_new_rate = current_rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128); + (Just(current_rate), current_rate..=max_new_rate) + }) + } + proptest! { + #[test] + fn current_utilization_rate( + total_liquidity in 0..=MAX_LIQUIDITY, + borrowed_percent in 0..=WAD, + ) { + 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 state = ReserveState { + borrowed_liquidity_wads, + available_liquidity, + ..ReserveState::default() + }; + + let current_rate = state.current_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 collateral_mint_supply = Decimal::from(total_liquidity).try_mul(Rate::from_scaled_val(collateral_multiplier))?.try_round_u64()?; + let mut reserve = Reserve { + state: ReserveState { + borrowed_liquidity_wads, + available_liquidity, + collateral_mint_supply, + ..ReserveState::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.state.collateral_exchange_rate()?; + assert!(exchange_rate.0.to_scaled_val() <= 5u128 * WAD as u128); + + // After interest accrual, collateral tokens should be worth more + reserve.accrue_interest(SLOTS_PER_YEAR)?; + + let new_exchange_rate = reserve.state.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 state = ReserveState::default(); + let borrow_rate = Rate::from_percent(borrow_rate); + + // Simulate running for max 1000 years, assuming that interest is + // compounded at least once a year + for _ in 0..1000 { + state.compound_interest(borrow_rate, slots_elapsed)?; + state.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_liquidity_wads = Decimal::from(borrowed_liquidity); + let mut reserve = Reserve { + state: ReserveState { + borrowed_liquidity_wads, + ..ReserveState::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.state.borrowed_liquidity_wads > borrowed_liquidity_wads); + } else { + assert!(reserve.state.borrowed_liquidity_wads == borrowed_liquidity_wads); + } + } + + #[test] + fn obligation_accrue_interest( + borrowed_liquidity in 0..=u64::MAX, + (current_borrow_rate, new_borrow_rate) in cumulative_rates(), + ) { + let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity); + let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; + let mut state = Obligation { + borrowed_liquidity_wads, + cumulative_borrow_rate_wads, + ..Obligation::default() + }; + + let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; + state.accrue_interest(next_cumulative_borrow_rate)?; + + if next_cumulative_borrow_rate > cumulative_borrow_rate_wads { + assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); + } else { + assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); + } + } + #[test] fn borrow_fee_calculation( borrow_fee_wad in 0..WAD, // at WAD, fee == borrow amount, which fails @@ -653,7 +886,7 @@ mod test { borrow_fee_wad, host_fee_percentage, }; - let (total_fee, host_fee) = fees.calculate_borrow_fees(borrow_amount).unwrap(); + let (total_fee, host_fee) = fees.calculate_borrow_fees(borrow_amount)?; // The total fee can't be greater than the amount borrowed, as long // as amount borrowed is greater than 2. @@ -751,4 +984,12 @@ mod test { assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 0); // 0 host fee } + + #[test] + fn initial_collateral_rate_sanity() { + assert_eq!( + INITIAL_COLLATERAL_RATIO.checked_mul(WAD).unwrap(), + INITIAL_COLLATERAL_RATE + ); + } } diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index 5b23844a..f226359f 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -8,7 +8,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_lending::{ instruction::BorrowAmountType, processor::process_instruction, - state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR}, + state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, }; const LAMPORTS_TO_SOL: u64 = 1_000_000_000; @@ -34,9 +34,6 @@ async fn test_borrow_quote_currency() { processor!(process_instruction), ); - // limit to track compute unit increase - test.set_bpf_compute_max_units(188_000); - let user_accounts_owner = Keypair::new(); let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); @@ -84,7 +81,7 @@ async fn test_borrow_quote_currency() { get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; assert_eq!(collateral_supply, 0); - let collateral_deposit_amount = INITIAL_COLLATERAL_RATE * SOL_COLLATERAL_AMOUNT_LAMPORTS; + let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * SOL_COLLATERAL_AMOUNT_LAMPORTS; let obligation = lending_market .borrow( &mut banks_client, @@ -175,9 +172,6 @@ async fn test_borrow_base_currency() { processor!(process_instruction), ); - // limit to track compute unit increase - test.set_bpf_compute_max_units(188_000); - let user_accounts_owner = Keypair::new(); let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); @@ -225,7 +219,7 @@ async fn test_borrow_base_currency() { get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; assert_eq!(collateral_supply, 0); - let collateral_deposit_amount = INITIAL_COLLATERAL_RATE * USDC_COLLATERAL_LAMPORTS; + let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * USDC_COLLATERAL_LAMPORTS; let obligation = lending_market .borrow( &mut banks_client, diff --git a/token-lending/program/tests/deposit.rs b/token-lending/program/tests/deposit.rs index eb8cdfd2..a4ed8631 100644 --- a/token-lending/program/tests/deposit.rs +++ b/token-lending/program/tests/deposit.rs @@ -18,7 +18,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(60_000); + test.set_bpf_compute_max_units(65_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); diff --git a/token-lending/program/tests/genesis_accounts.rs b/token-lending/program/tests/genesis_accounts.rs index 6595dbed..1376923b 100644 --- a/token-lending/program/tests/genesis_accounts.rs +++ b/token-lending/program/tests/genesis_accounts.rs @@ -6,7 +6,7 @@ use helpers::*; use solana_sdk::signature::Keypair; use spl_token_lending::{ instruction::BorrowAmountType, - state::{INITIAL_COLLATERAL_RATE, PROGRAM_VERSION}, + state::{INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION}, }; #[tokio::test] @@ -111,7 +111,7 @@ async fn test_success() { get_token_balance(&mut banks_client, usdc_reserve.user_collateral_account).await; assert_eq!( user_usdc_collateral_balance, - INITIAL_COLLATERAL_RATE * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL + INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL ); let sol_liquidity_supply = @@ -124,7 +124,7 @@ async fn test_success() { get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; assert_eq!( user_sol_collateral_balance, - INITIAL_COLLATERAL_RATE * INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS + INITIAL_COLLATERAL_RATIO * INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS ); // Deposit SOL @@ -152,7 +152,7 @@ async fn test_success() { get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; assert_eq!( user_sol_collateral_balance, - INITIAL_COLLATERAL_RATE * TOTAL_SOL + INITIAL_COLLATERAL_RATIO * TOTAL_SOL ); // Borrow USDC with SOL collateral @@ -165,7 +165,7 @@ async fn test_success() { borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, borrow_amount_type: BorrowAmountType::CollateralDepositAmount, - amount: INITIAL_COLLATERAL_RATE * USER_SOL_COLLATERAL_LAMPORTS, + amount: INITIAL_COLLATERAL_RATIO * USER_SOL_COLLATERAL_LAMPORTS, user_accounts_owner: &user_accounts_owner, obligation: None, }, @@ -199,7 +199,7 @@ async fn test_success() { &user_accounts_owner, &payer, &usdc_reserve, - 2 * INITIAL_COLLATERAL_RATE + 2 * INITIAL_COLLATERAL_RATIO * lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS / 100, @@ -217,7 +217,7 @@ async fn test_success() { borrow_reserve: &sol_reserve, dex_market: &sol_usdc_dex_market, borrow_amount_type: BorrowAmountType::CollateralDepositAmount, - amount: INITIAL_COLLATERAL_RATE + amount: INITIAL_COLLATERAL_RATIO * lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS @@ -239,7 +239,7 @@ async fn test_success() { borrow_reserve: &srm_reserve, dex_market: &srm_usdc_dex_market, borrow_amount_type: BorrowAmountType::CollateralDepositAmount, - amount: INITIAL_COLLATERAL_RATE + amount: INITIAL_COLLATERAL_RATIO * lamports_to_usdc_fractional( usdc_reserve.config.loan_to_value_ratio as u64 * USER_SOL_COLLATERAL_LAMPORTS diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index ad608a61..43c90f1b 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -22,7 +22,7 @@ use spl_token_lending::{ processor::process_instruction, state::{ LendingMarket, Obligation, Reserve, ReserveConfig, ReserveFees, ReserveState, - INITIAL_COLLATERAL_RATE, PROGRAM_VERSION, + INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION, }, }; use std::str::FromStr; @@ -120,7 +120,7 @@ pub fn add_lending_market(test: &mut ProgramTest, quote_token_mint: Pubkey) -> T let (authority, bump_seed) = Pubkey::find_program_address(&[pubkey.as_ref()], &spl_token_lending::id()); - let owner = Keypair::new(); + let owner = read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); test.add_packable_account( pubkey, @@ -335,7 +335,8 @@ pub fn add_reserve( let reserve_keypair = Keypair::new(); let reserve_pubkey = reserve_keypair.pubkey(); - let mut reserve_state = ReserveState::new(1u64.wrapping_sub(slots_elapsed), liquidity_amount); + let mut reserve_state = + ReserveState::new(1u64.wrapping_sub(slots_elapsed), liquidity_amount).unwrap(); reserve_state.add_borrow(borrow_amount).unwrap(); test.add_packable_account( reserve_pubkey, @@ -383,7 +384,7 @@ pub fn add_reserve( &Token { mint: collateral_mint_pubkey, owner: user_accounts_owner.pubkey(), - amount: liquidity_amount * INITIAL_COLLATERAL_RATE, + amount: liquidity_amount * INITIAL_COLLATERAL_RATIO, state: AccountState::Initialized, ..Token::default() }, diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index b0fc6a65..3a2a1e98 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -21,6 +21,9 @@ async fn test_success() { processor!(process_instruction), ); + // limit to track compute unit increase + test.set_bpf_compute_max_units(5_000); + let usdc_mint = add_usdc_mint(&mut test); let (mut banks_client, payer, _recent_blockhash) = test.start().await; diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index c38a9f83..e99ee439 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -14,7 +14,7 @@ use spl_token_lending::{ error::LendingError, instruction::init_reserve, processor::process_instruction, - state::{ReserveFees, INITIAL_COLLATERAL_RATE}, + state::{ReserveFees, INITIAL_COLLATERAL_RATIO}, }; #[tokio::test] @@ -25,6 +25,9 @@ async fn test_success() { processor!(process_instruction), ); + // limit to track compute unit increase + test.set_bpf_compute_max_units(66_000); + let user_accounts_owner = Keypair::new(); let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); @@ -70,7 +73,7 @@ async fn test_success() { get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await; assert_eq!( user_sol_collateral_balance, - INITIAL_COLLATERAL_RATE * RESERVE_AMOUNT + INITIAL_COLLATERAL_RATIO * RESERVE_AMOUNT ); } diff --git a/token-lending/program/tests/liquidate.rs b/token-lending/program/tests/liquidate.rs index ffe38b69..6b337819 100644 --- a/token-lending/program/tests/liquidate.rs +++ b/token-lending/program/tests/liquidate.rs @@ -8,7 +8,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_lending::{ math::Decimal, processor::process_instruction, - state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR}, + state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, }; const LAMPORTS_TO_SOL: u64 = 1_000_000_000; @@ -26,14 +26,14 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(154_000); + test.set_bpf_compute_max_units(165_000); // set loan values to about 90% of collateral value so that it gets liquidated const USDC_LOAN: u64 = 2 * FRACTIONAL_TO_USDC; - const USDC_LOAN_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATE * LAMPORTS_TO_SOL; + const USDC_LOAN_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATIO * LAMPORTS_TO_SOL; const SOL_LOAN: u64 = LAMPORTS_TO_SOL; - const SOL_LOAN_USDC_COLLATERAL: u64 = 2 * INITIAL_COLLATERAL_RATE * FRACTIONAL_TO_USDC; + const SOL_LOAN_USDC_COLLATERAL: u64 = 2 * INITIAL_COLLATERAL_RATIO * FRACTIONAL_TO_USDC; let user_accounts_owner = Keypair::new(); let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); @@ -142,7 +142,10 @@ async fn test_success() { assert!(usdc_liquidated > USDC_LOAN / 2); assert_eq!( usdc_liquidated, - usdc_loan_state.borrowed_liquidity_wads.round_u64() + usdc_loan_state + .borrowed_liquidity_wads + .try_round_u64() + .unwrap() ); let sol_liquidity_supply = @@ -152,6 +155,9 @@ async fn test_success() { assert!(sol_liquidated > SOL_LOAN / 2); assert_eq!( sol_liquidated, - sol_loan_state.borrowed_liquidity_wads.round_u64() + sol_loan_state + .borrowed_liquidity_wads + .try_round_u64() + .unwrap() ); } diff --git a/token-lending/program/tests/repay.rs b/token-lending/program/tests/repay.rs index e8526341..3eed5147 100644 --- a/token-lending/program/tests/repay.rs +++ b/token-lending/program/tests/repay.rs @@ -12,9 +12,9 @@ use solana_sdk::{ use spl_token::instruction::approve; use spl_token_lending::{ instruction::repay_reserve_liquidity, - math::Decimal, + math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, processor::process_instruction, - state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR}, + state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, }; const LAMPORTS_TO_SOL: u64 = 1_000_000_000; @@ -29,13 +29,13 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(79_000); + test.set_bpf_compute_max_units(85_000); const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC; - const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATE; + const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; let user_accounts_owner = Keypair::new(); let user_transfer_authority = Keypair::new(); @@ -157,21 +157,30 @@ async fn test_success() { ); // use cumulative borrow rate directly since test rate starts at 1.0 - let expected_obligation_interest = (obligation_state.cumulative_borrow_rate_wads - * OBLIGATION_LOAN) - - Decimal::from(OBLIGATION_LOAN); + let expected_obligation_interest = obligation_state + .cumulative_borrow_rate_wads + .try_mul(OBLIGATION_LOAN) + .unwrap() + .try_sub(Decimal::from(OBLIGATION_LOAN)) + .unwrap(); assert_eq!( obligation_state.borrowed_liquidity_wads, expected_obligation_interest ); - let expected_obligation_total = Decimal::from(OBLIGATION_LOAN) + expected_obligation_interest; + let expected_obligation_total = Decimal::from(OBLIGATION_LOAN) + .try_add(expected_obligation_interest) + .unwrap(); - let expected_obligation_repaid_percent = - Decimal::from(OBLIGATION_LOAN) / expected_obligation_total; + let expected_obligation_repaid_percent = Decimal::from(OBLIGATION_LOAN) + .try_div(expected_obligation_total) + .unwrap(); - let expected_collateral_received = - (expected_obligation_repaid_percent * OBLIGATION_COLLATERAL).round_u64(); + let expected_collateral_received = expected_obligation_repaid_percent + .try_mul(OBLIGATION_COLLATERAL) + .unwrap() + .try_round_u64() + .unwrap(); assert_eq!(collateral_received, expected_collateral_received); let expected_collateral_remaining = OBLIGATION_COLLATERAL - expected_collateral_received; diff --git a/token-lending/program/tests/withdraw.rs b/token-lending/program/tests/withdraw.rs index 31df3000..a9d0a207 100644 --- a/token-lending/program/tests/withdraw.rs +++ b/token-lending/program/tests/withdraw.rs @@ -13,7 +13,7 @@ use spl_token::instruction::approve; use spl_token_lending::{ instruction::withdraw_reserve_liquidity, processor::process_instruction, - state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR}, + state::{INITIAL_COLLATERAL_RATIO, SLOTS_PER_YEAR}, }; const FRACTIONAL_TO_USDC: u64 = 1_000_000; @@ -28,14 +28,14 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(62_000); + test.set_bpf_compute_max_units(66_000); let user_accounts_owner = Keypair::new(); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); const WITHDRAW_COLLATERAL_AMOUNT: u64 = - INITIAL_COLLATERAL_RATE * INITIAL_USDC_RESERVE_SUPPLY_LAMPORTS; + INITIAL_COLLATERAL_RATIO * INITIAL_USDC_RESERVE_SUPPLY_LAMPORTS; let usdc_reserve = add_reserve( &mut test,