use pyra_types::{
KaminoBigFractionBytes, KaminoObligationCollateral, KaminoObligationLiquidity, KaminoReserve,
KAMINO_FRACTION_SCALE,
};
use crate::error::{MathError, MathResult};
fn bigfraction_lower_u128(bsf: &KaminoBigFractionBytes) -> u128 {
debug_assert!(
bsf.value[2] == 0 && bsf.value[3] == 0,
"cumulative borrow rate overflow: upper 128 bits are non-zero"
);
let lo = bsf.value[0] as u128;
let hi = bsf.value[1] as u128;
hi.checked_shl(64).unwrap_or(0) | lo
}
fn checked_mul_div(a: u128, b: u128, c: u128) -> MathResult<u128> {
if c == 0 {
return Err(MathError::Overflow);
}
if let Some(product) = a.checked_mul(b) {
return product.checked_div(c).ok_or(MathError::Overflow);
}
let q = a.checked_div(c).ok_or(MathError::Overflow)?;
let r = a.checked_rem(c).ok_or(MathError::Overflow)?;
let term1 = q.checked_mul(b).ok_or(MathError::Overflow)?;
let term2 = if let Some(rb) = r.checked_mul(b) {
rb.checked_div(c).ok_or(MathError::Overflow)?
} else {
let bq = b.checked_div(c).ok_or(MathError::Overflow)?;
let br = b.checked_rem(c).ok_or(MathError::Overflow)?;
let t1 = r.checked_mul(bq).ok_or(MathError::Overflow)?;
let t2 = r
.checked_mul(br)
.ok_or(MathError::Overflow)?
.checked_div(c)
.ok_or(MathError::Overflow)?;
t1.checked_add(t2).ok_or(MathError::Overflow)?
};
term1.checked_add(term2).ok_or(MathError::Overflow)
}
fn total_liquidity_base_units(reserve: &KaminoReserve) -> MathResult<u128> {
let liq = &reserve.liquidity;
let borrowed_base = liq
.borrowed_amount_sf
.checked_div(KAMINO_FRACTION_SCALE)
.ok_or(MathError::Overflow)?;
let protocol_fees = liq
.accumulated_protocol_fees_sf
.checked_div(KAMINO_FRACTION_SCALE)
.ok_or(MathError::Overflow)?;
let referrer_fees = liq
.accumulated_referrer_fees_sf
.checked_div(KAMINO_FRACTION_SCALE)
.ok_or(MathError::Overflow)?;
let pending_fees = liq
.pending_referrer_fees_sf
.checked_div(KAMINO_FRACTION_SCALE)
.ok_or(MathError::Overflow)?;
(liq.total_available_amount as u128)
.checked_add(borrowed_base)
.ok_or(MathError::Overflow)?
.checked_sub(protocol_fees)
.ok_or(MathError::Overflow)?
.checked_sub(referrer_fees)
.ok_or(MathError::Overflow)?
.checked_sub(pending_fees)
.ok_or(MathError::Overflow)
}
pub fn get_kamino_deposit_balance(
collateral: &KaminoObligationCollateral,
reserve: &KaminoReserve,
) -> MathResult<i128> {
let deposited = collateral.deposited_amount as u128;
if deposited == 0 {
return Ok(0);
}
let collateral_supply = reserve.collateral.mint_total_supply as u128;
if collateral_supply == 0 {
return i128::try_from(deposited).map_err(|_| MathError::Overflow);
}
let total_liquidity = total_liquidity_base_units(reserve)?;
if total_liquidity == 0 {
return Ok(0);
}
let result = checked_mul_div(deposited, total_liquidity, collateral_supply)?;
i128::try_from(result).map_err(|_| MathError::Overflow)
}
pub fn get_kamino_borrow_balance(
liquidity: &KaminoObligationLiquidity,
reserve: &KaminoReserve,
) -> MathResult<i128> {
let borrowed_sf = liquidity.borrowed_amount_sf;
if borrowed_sf == 0 {
return Ok(0);
}
let base_amount = borrowed_sf
.checked_div(KAMINO_FRACTION_SCALE)
.ok_or(MathError::Overflow)?;
let obligation_rate = bigfraction_lower_u128(&liquidity.cumulative_borrow_rate_bsf);
let reserve_rate = bigfraction_lower_u128(&reserve.liquidity.cumulative_borrow_rate_bsf);
let accrued = if obligation_rate == 0 || reserve_rate <= obligation_rate {
base_amount
} else {
checked_mul_div(base_amount, reserve_rate, obligation_rate)?
};
let signed = i128::try_from(accrued).map_err(|_| MathError::Overflow)?;
Ok(signed.saturating_neg())
}
#[cfg(test)]
#[allow(
clippy::allow_attributes,
clippy::allow_attributes_without_reason,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
reason = "test code"
)]
mod tests {
use super::*;
use pyra_types::{
KaminoLastUpdate, KaminoReserveCollateral, KaminoReserveConfig, KaminoReserveLiquidity,
};
use solana_pubkey::Pubkey;
const FRACTION_ONE: u128 = 1 << 60;
fn rate_to_bsf(rate: u128) -> KaminoBigFractionBytes {
KaminoBigFractionBytes {
value: [rate as u64, (rate >> 64) as u64, 0, 0],
}
}
fn make_reserve(
total_available: u64,
borrowed_sf: u128,
collateral_supply: u64,
mint_decimals: u64,
) -> KaminoReserve {
KaminoReserve {
lending_market: Pubkey::default(),
liquidity: KaminoReserveLiquidity {
mint_pubkey: Pubkey::default(),
supply_vault: Pubkey::default(),
fee_vault: Pubkey::default(),
total_available_amount: total_available,
borrowed_amount_sf: borrowed_sf,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
mint_decimals,
market_price_sf: 0,
accumulated_protocol_fees_sf: 0,
accumulated_referrer_fees_sf: 0,
pending_referrer_fees_sf: 0,
token_program: Pubkey::default(),
},
collateral: KaminoReserveCollateral {
mint_pubkey: Pubkey::default(),
supply_vault: Pubkey::default(),
mint_total_supply: collateral_supply,
},
config: KaminoReserveConfig {
loan_to_value_pct: 80,
liquidation_threshold_pct: 85,
protocol_take_rate_pct: 0,
protocol_liquidation_fee_pct: 0,
borrow_factor_pct: 100,
deposit_limit: 0,
borrow_limit: 0,
fees: Default::default(),
borrow_rate_curve: Default::default(),
deposit_withdrawal_cap: Default::default(),
debt_withdrawal_cap: Default::default(),
elevation_groups: [0; 20],
},
last_update: KaminoLastUpdate::default(),
}
}
fn make_collateral(deposited_amount: u64) -> KaminoObligationCollateral {
KaminoObligationCollateral {
deposit_reserve: Pubkey::default(),
deposited_amount,
market_value_sf: 0,
}
}
fn make_liquidity(borrowed_sf: u128, obligation_rate: u128) -> KaminoObligationLiquidity {
KaminoObligationLiquidity {
borrow_reserve: Pubkey::default(),
cumulative_borrow_rate_bsf: rate_to_bsf(obligation_rate),
borrowed_amount_sf: borrowed_sf,
market_value_sf: 0,
borrow_factor_adjusted_market_value_sf: 0,
}
}
#[test]
fn deposit_zero_returns_zero() {
let reserve = make_reserve(1_000_000, 0, 1_000_000, 6);
let collateral = make_collateral(0);
assert_eq!(get_kamino_deposit_balance(&collateral, &reserve).unwrap(), 0);
}
#[test]
fn deposit_one_to_one_exchange_rate() {
let reserve = make_reserve(1_000_000, 0, 1_000_000, 6);
let collateral = make_collateral(500_000);
assert_eq!(
get_kamino_deposit_balance(&collateral, &reserve).unwrap(),
500_000
);
}
#[test]
fn deposit_exchange_rate_with_interest() {
let borrowed_sf = 100_000u128 * FRACTION_ONE;
let reserve = make_reserve(1_000_000, borrowed_sf, 1_000_000, 6);
let collateral = make_collateral(1_000_000);
assert_eq!(
get_kamino_deposit_balance(&collateral, &reserve).unwrap(),
1_100_000 );
}
#[test]
fn deposit_zero_collateral_supply_uses_one_to_one() {
let reserve = make_reserve(1_000_000, 0, 0, 6);
let collateral = make_collateral(500_000);
assert_eq!(
get_kamino_deposit_balance(&collateral, &reserve).unwrap(),
500_000
);
}
#[test]
fn deposit_exchange_rate_with_fees_subtracted() {
let borrowed_sf = 200_000u128 * FRACTION_ONE;
let mut reserve = make_reserve(1_000_000, borrowed_sf, 1_000_000, 6);
reserve.liquidity.accumulated_protocol_fees_sf = 50_000u128 * FRACTION_ONE;
reserve.liquidity.accumulated_referrer_fees_sf = 30_000u128 * FRACTION_ONE;
reserve.liquidity.pending_referrer_fees_sf = 20_000u128 * FRACTION_ONE;
let collateral = make_collateral(1_000_000);
assert_eq!(
get_kamino_deposit_balance(&collateral, &reserve).unwrap(),
1_100_000
);
}
#[test]
fn deposit_zero_total_liquidity_returns_zero() {
let reserve = make_reserve(0, 0, 1_000_000, 6);
let collateral = make_collateral(500_000);
assert_eq!(get_kamino_deposit_balance(&collateral, &reserve).unwrap(), 0);
}
#[test]
fn borrow_zero_returns_zero() {
let reserve = make_reserve(0, 0, 0, 6);
let liquidity = make_liquidity(0, FRACTION_ONE);
assert_eq!(get_kamino_borrow_balance(&liquidity, &reserve).unwrap(), 0);
}
#[test]
fn borrow_returns_negative() {
let borrowed_sf = 1_000_000u128 * FRACTION_ONE;
let reserve = make_reserve(0, 0, 0, 6);
let liquidity = make_liquidity(borrowed_sf, FRACTION_ONE);
assert_eq!(
get_kamino_borrow_balance(&liquidity, &reserve).unwrap(),
-1_000_000
);
}
#[test]
fn borrow_with_interest_accrual() {
let borrowed_sf = 1_000_000u128 * FRACTION_ONE;
let obligation_rate = FRACTION_ONE;
let reserve_rate = FRACTION_ONE * 2;
let mut reserve = make_reserve(0, 0, 0, 6);
reserve.liquidity.cumulative_borrow_rate_bsf = rate_to_bsf(reserve_rate);
let liquidity = make_liquidity(borrowed_sf, obligation_rate);
assert_eq!(
get_kamino_borrow_balance(&liquidity, &reserve).unwrap(),
-2_000_000
);
let rate_1_1 = FRACTION_ONE + FRACTION_ONE / 10;
let mut reserve2 = make_reserve(0, 0, 0, 6);
reserve2.liquidity.cumulative_borrow_rate_bsf = rate_to_bsf(rate_1_1);
let liquidity2 = make_liquidity(borrowed_sf, obligation_rate);
let balance = get_kamino_borrow_balance(&liquidity2, &reserve2).unwrap();
assert!(balance == -1_100_000 || balance == -1_099_999);
}
#[test]
fn borrow_no_accrual_when_rates_equal() {
let borrowed_sf = 500_000u128 * FRACTION_ONE;
let rate = FRACTION_ONE;
let mut reserve = make_reserve(0, 0, 0, 6);
reserve.liquidity.cumulative_borrow_rate_bsf = rate_to_bsf(rate);
let liquidity = make_liquidity(borrowed_sf, rate);
assert_eq!(
get_kamino_borrow_balance(&liquidity, &reserve).unwrap(),
-500_000
);
}
#[test]
fn borrow_no_accrual_when_obligation_rate_zero() {
let borrowed_sf = 500_000u128 * FRACTION_ONE;
let reserve = make_reserve(0, 0, 0, 6);
let liquidity = make_liquidity(borrowed_sf, 0);
assert_eq!(
get_kamino_borrow_balance(&liquidity, &reserve).unwrap(),
-500_000
);
}
#[test]
fn mul_div_basic() {
assert_eq!(checked_mul_div(100, 200, 50).unwrap(), 400);
}
#[test]
fn mul_div_zero_divisor() {
assert!(checked_mul_div(100, 200, 0).is_err());
}
#[test]
fn mul_div_large_values() {
let a = u128::MAX / 2;
let b = 3u128;
let c = 2u128;
let result = checked_mul_div(a, b, c).unwrap();
let expected = (a / c) * b + (a % c) * b / c;
assert_eq!(result, expected);
}
}