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:
parent
76fcea0dbb
commit
d1bc753359
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())?;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue