lending: Use checked math and add prop tests (#1075)

* Use checked math and add prop tests

* Feedback

* Feedback

* Fix exchange rate test

* remove borrow compute unit limit

* Bump compute limit
This commit is contained in:
Justin Starry 2021-01-17 15:10:04 +08:00 committed by GitHub
parent 76fcea0dbb
commit d1bc753359
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 624 additions and 317 deletions

View File

@ -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());
}

View File

@ -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")]

View File

@ -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<Self, ProgramError>;
}
/// Try to subtract, return an error on overflow
pub trait TryAdd: Sized {
/// Add
fn try_add(self, rhs: Self) -> Result<Self, ProgramError>;
}
/// Try to divide, return an error on overflow or divide by zero
pub trait TryDiv<RHS>: Sized {
/// Divide
fn try_div(self, rhs: RHS) -> Result<Self, ProgramError>;
}
/// Try to multiply, return an error on overflow
pub trait TryMul<RHS>: Sized {
/// Multiply
fn try_mul(self, rhs: RHS) -> Result<Self, ProgramError>;
}

View File

@ -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<u128, ProgramError> {
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<u64, ProgramError> {
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<Rate> 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<Self, ProgramError> {
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<Self, ProgramError> {
Ok(Self(
self.0
.checked_sub(rhs.0)
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Div<u64> for Decimal {
type Output = Self;
fn div(self, rhs: u64) -> Self::Output {
Self(self.0 / U192::from(rhs))
impl TryDiv<u64> for Decimal {
fn try_div(self, rhs: u64) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_div(U192::from(rhs))
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Div<Rate> for Decimal {
type Output = Self;
fn div(self, rhs: Rate) -> Self::Output {
self / Self::from(rhs)
impl TryDiv<Rate> for Decimal {
fn try_div(self, rhs: Rate) -> Result<Self, ProgramError> {
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<Decimal> for Decimal {
fn try_div(self, rhs: Self) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_mul(Self::wad())
.ok_or(LendingError::MathOverflow)?
.checked_div(rhs.0)
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Mul<u64> for Decimal {
type Output = Self;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * U192::from(rhs))
impl TryMul<u64> for Decimal {
fn try_mul(self, rhs: u64) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_mul(U192::from(rhs))
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Mul<Rate> 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<Rate> for Decimal {
fn try_mul(self, rhs: Rate) -> Result<Self, ProgramError> {
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<Rate> for Decimal {
fn mul_assign(&mut self, rhs: Rate) {
*self = *self * rhs;
impl TryMul<Decimal> for Decimal {
fn try_mul(self, rhs: Self) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_mul(rhs.0)
.ok_or(LendingError::MathOverflow)?
.checked_div(Self::wad())
.ok_or(LendingError::MathOverflow)?,
))
}
}

View File

@ -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<u64, ProgramError> {
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<Rate, ProgramError> {
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<u64> for Rate {
fn from(val: u64) -> Self {
Self(Self::wad() * U128::from(val))
impl TryFrom<Decimal> for Rate {
type Error = ProgramError;
fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
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<Self, ProgramError> {
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<Self, ProgramError> {
Ok(Self(
self.0
.checked_sub(rhs.0)
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Div<u8> for Rate {
type Output = Self;
fn div(self, rhs: u8) -> Self::Output {
Self(self.0 / U128::from(rhs))
impl TryDiv<u64> for Rate {
fn try_div(self, rhs: u64) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_div(U128::from(rhs))
.ok_or(LendingError::MathOverflow)?,
))
}
}
impl std::ops::Div<u64> for Rate {
type Output = Self;
fn div(self, rhs: u64) -> Self::Output {
Self(self.0 / U128::from(rhs))
impl TryDiv<Rate> for Rate {
fn try_div(self, rhs: Self) -> Result<Self, ProgramError> {
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<u64> for Rate {
fn try_mul(self, rhs: u64) -> Result<Self, ProgramError> {
Ok(Self(
self.0
.checked_mul(U128::from(rhs))
.ok_or(LendingError::MathOverflow)?,
))
}
}
// TODO: Returned rate could cause overflows
impl std::ops::Mul<u8> for Rate {
type Output = Self;
fn mul(self, rhs: u8) -> Self::Output {
Self(self.0 * U128::from(rhs))
impl TryMul<Rate> for Rate {
fn try_mul(self, rhs: Self) -> Result<Self, ProgramError> {
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<u64> 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());
}
}

View File

@ -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())?;

View File

@ -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<Self, ProgramError> {
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<u64, ProgramError> {
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<Decimal, ProgramError> {
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<u64, ProgramError> {
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<Decimal, ProgramError> {
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<u64, ProgramError> {
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<Rate, ProgramError> {
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<CollateralExchangeRate, ProgramError> {
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<Rate, ProgramError> {
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<Rate, ProgramError> {
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<u64, ProgramError> {
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<u64, ProgramError> {
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<COption<Pubkey>, 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<Value = (u128, u128)> {
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
);
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -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()
},

View File

@ -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;

View File

@ -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
);
}

View File

@ -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()
);
}

View File

@ -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;

View File

@ -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,