use std::cmp;
use std::collections::HashMap;
use pyra_tokens::AssetId;
use pyra_types::{KaminoObligation, KaminoReserve};
use solana_pubkey::Pubkey;
use super::balance::{get_kamino_borrow_balance, get_kamino_deposit_balance};
use super::weights::{get_kamino_asset_weight, get_kamino_liability_weight, get_kamino_price};
use crate::drift::balance::calculate_value_usdc_base_units;
use crate::error::{MathError, MathResult};
const MARGIN_PRECISION: i128 = 10_000;
const PRICE_PRECISION: i128 = 1_000_000;
#[derive(Debug, Clone, Copy)]
pub struct KaminoMarginState {
pub total_collateral: i128,
pub total_weighted_collateral: i128,
pub total_liabilities: i128,
pub total_weighted_liabilities: i128,
}
impl KaminoMarginState {
pub fn calculate(
obligation: &KaminoObligation,
reserves: &HashMap<Pubkey, KaminoReserve>,
) -> MathResult<Self> {
if obligation.elevation_group != 0 {
return Err(MathError::Overflow);
}
let mut total_collateral: i128 = 0;
let mut total_weighted_collateral: i128 = 0;
let mut total_liabilities: i128 = 0;
let mut total_weighted_liabilities: i128 = 0;
for deposit in &obligation.deposits {
if deposit.deposited_amount == 0 {
continue;
}
let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
continue;
};
let balance = get_kamino_deposit_balance(deposit, reserve)?;
let price = get_kamino_price(reserve)?;
let price_u64 = u64::try_from(price).map_err(|_| MathError::Overflow)?;
let decimals = u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| MathError::Overflow)?;
let value = calculate_value_usdc_base_units(balance, price_u64, decimals)?;
total_collateral = total_collateral
.checked_add(value)
.ok_or(MathError::Overflow)?;
let weight = get_kamino_asset_weight(reserve) as i128;
let weighted = value
.checked_mul(weight)
.ok_or(MathError::Overflow)?
.checked_div(MARGIN_PRECISION)
.ok_or(MathError::Overflow)?;
total_weighted_collateral = total_weighted_collateral
.checked_add(weighted)
.ok_or(MathError::Overflow)?;
}
for borrow in &obligation.borrows {
if borrow.borrowed_amount_sf == 0 {
continue;
}
let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
continue;
};
let balance = get_kamino_borrow_balance(borrow, reserve)?;
let price = get_kamino_price(reserve)?;
let price_u64 = u64::try_from(price).map_err(|_| MathError::Overflow)?;
let decimals = u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| MathError::Overflow)?;
let value = calculate_value_usdc_base_units(balance, price_u64, decimals)?;
let liability = value.checked_neg().ok_or(MathError::Overflow)?;
let borrow_factor = i128::from(std::cmp::max(100u64, reserve.config.borrow_factor_pct));
let factor_adjusted = liability
.checked_mul(borrow_factor)
.ok_or(MathError::Overflow)?
.checked_div(100)
.ok_or(MathError::Overflow)?;
total_liabilities = total_liabilities
.checked_add(factor_adjusted)
.ok_or(MathError::Overflow)?;
let weight = get_kamino_liability_weight(reserve)? as i128;
let weighted = factor_adjusted
.checked_mul(weight)
.ok_or(MathError::Overflow)?
.checked_div(MARGIN_PRECISION)
.ok_or(MathError::Overflow)?;
total_weighted_liabilities = total_weighted_liabilities
.checked_add(weighted)
.ok_or(MathError::Overflow)?;
}
Ok(Self {
total_collateral,
total_weighted_collateral,
total_liabilities,
total_weighted_liabilities,
})
}
pub fn free_collateral(&self) -> u64 {
let fc = self
.total_weighted_collateral
.saturating_sub(self.total_liabilities)
.max(0);
clamp_to_u64(fc)
}
pub fn credit_usage_bps(&self) -> MathResult<u64> {
if self.total_weighted_collateral <= 0 {
return Ok(0);
}
let usage = self
.total_weighted_liabilities
.checked_mul(10_000)
.ok_or(MathError::Overflow)?
.checked_div(self.total_weighted_collateral)
.ok_or(MathError::Overflow)?;
Ok(cmp::min(clamp_to_u64(cmp::max(0, usage)), 10_000))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KaminoPositionLimits {
pub withdraw_limit: u64,
pub borrow_limit: u64,
}
pub fn calculate_kamino_position_limits(
margin_state: &KaminoMarginState,
reserve: &KaminoReserve,
asset_id: AssetId,
obligation: &KaminoObligation,
) -> MathResult<KaminoPositionLimits> {
let reserve_address = asset_id
.token()
.kamino_reserve
.ok_or(MathError::Overflow)?;
let price = get_kamino_price(reserve)?;
if price == 0 {
return Ok(KaminoPositionLimits {
withdraw_limit: 0,
borrow_limit: 0,
});
}
let free_collateral = i128::from(margin_state.free_collateral());
let asset_weight = get_kamino_asset_weight(reserve) as i128;
let decimals = u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| MathError::Overflow)?;
let (numerator_scale, denominator_scale) = decimal_scale(decimals)?;
let deposit_balance = obligation
.deposits
.iter()
.find(|d| d.deposit_reserve == reserve_address)
.map(|d| get_kamino_deposit_balance(d, reserve))
.transpose()?
.unwrap_or(0);
let deposit_balance_u64 = clamp_to_u64(cmp::max(0, deposit_balance));
let withdraw_limit = if asset_weight == 0 || margin_state.total_weighted_liabilities == 0 {
deposit_balance_u64
} else {
let limit = free_collateral
.checked_mul(MARGIN_PRECISION)
.and_then(|v| v.checked_div(asset_weight))
.and_then(|v| v.checked_mul(PRICE_PRECISION))
.and_then(|v| v.checked_div(price))
.and_then(|v| v.checked_mul(numerator_scale as i128))
.and_then(|v| v.checked_div(denominator_scale as i128))
.ok_or(MathError::Overflow)?;
cmp::min(deposit_balance_u64, clamp_to_u64(cmp::max(0, limit)))
};
let free_collateral_after = if deposit_balance > 0 {
let price_u64 = u64::try_from(price).map_err(|_| MathError::Overflow)?;
let position_value =
calculate_value_usdc_base_units(deposit_balance, price_u64, decimals)?;
let weighted = position_value
.checked_mul(asset_weight)
.and_then(|v| v.checked_div(MARGIN_PRECISION))
.ok_or(MathError::Overflow)?;
cmp::max(0, free_collateral.saturating_sub(weighted))
} else {
free_collateral
};
let liability_weight = get_kamino_liability_weight(reserve)? as i128;
let max_additional = free_collateral_after
.checked_mul(MARGIN_PRECISION)
.and_then(|v| v.checked_div(liability_weight))
.and_then(|v| v.checked_mul(PRICE_PRECISION))
.and_then(|v| v.checked_div(price))
.and_then(|v| v.checked_mul(numerator_scale as i128))
.and_then(|v| v.checked_div(denominator_scale as i128))
.ok_or(MathError::Overflow)?;
let borrow_limit = (withdraw_limit as i128)
.checked_add(cmp::max(0, max_additional))
.ok_or(MathError::Overflow)?;
Ok(KaminoPositionLimits {
withdraw_limit,
borrow_limit: clamp_to_u64(cmp::max(0, borrow_limit)),
})
}
fn decimal_scale(token_decimals: u32) -> MathResult<(u32, u32)> {
if token_decimals > 6 {
let numerator = 10u32
.checked_pow(
token_decimals
.checked_sub(6)
.ok_or(MathError::Overflow)?,
)
.ok_or(MathError::Overflow)?;
Ok((numerator, 1))
} else {
let denominator = 10u32
.checked_pow(6u32.checked_sub(token_decimals).ok_or(MathError::Overflow)?)
.ok_or(MathError::Overflow)?;
Ok((1, denominator))
}
}
fn clamp_to_u64(value: i128) -> u64 {
if value < 0 {
0
} else if value > u64::MAX as i128 {
u64::MAX
} else {
value as u64
}
}
#[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::{
KaminoBigFractionBytes, KaminoLastUpdate, KaminoObligationCollateral,
KaminoObligationLiquidity, KaminoReserveCollateral, KaminoReserveConfig,
KaminoReserveLiquidity,
};
const FRACTION_ONE: u128 = 1 << 60;
fn usdc_asset_id() -> AssetId {
pyra_tokens::USDC.asset_id
}
fn usdc_reserve_key() -> Pubkey {
pyra_tokens::USDC.kamino_reserve.unwrap()
}
fn rate_to_bsf(rate: u128) -> KaminoBigFractionBytes {
KaminoBigFractionBytes {
value: [rate as u64, (rate >> 64) as u64, 0, 0],
}
}
fn make_reserve(
ltv: u8,
liq_threshold: u8,
price_usd: u128,
decimals: u64,
total_available: u64,
collateral_supply: u64,
) -> KaminoReserve {
let price_sf = price_usd << 60;
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: 0,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
mint_decimals: decimals,
market_price_sf: price_sf,
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: ltv,
liquidation_threshold_pct: liq_threshold,
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 empty_obligation() -> KaminoObligation {
KaminoObligation {
lending_market: Pubkey::default(),
owner: Pubkey::default(),
deposits: vec![],
borrows: vec![],
deposited_value_sf: 0,
borrowed_assets_market_value_sf: 0,
allowed_borrow_value_sf: 0,
unhealthy_borrow_value_sf: 0,
has_debt: 0,
elevation_group: 0,
last_update: KaminoLastUpdate::default(),
}
}
#[test]
fn empty_margin_state() {
let state =
KaminoMarginState::calculate(&empty_obligation(), &HashMap::new()).unwrap();
assert_eq!(state.total_collateral, 0);
assert_eq!(state.total_weighted_collateral, 0);
assert_eq!(state.total_liabilities, 0);
assert_eq!(state.total_weighted_liabilities, 0);
assert_eq!(state.free_collateral(), 0);
assert_eq!(state.credit_usage_bps().unwrap(), 0);
}
#[test]
fn margin_state_single_deposit() {
let reserve_key = Pubkey::new_unique();
let reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let reserves: HashMap<Pubkey, KaminoReserve> = [(reserve_key, reserve)].into();
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: reserve_key,
deposited_amount: 10_000_000,
market_value_sf: 0,
});
let state = KaminoMarginState::calculate(&obligation, &reserves).unwrap();
assert_eq!(state.total_collateral, 10_000_000);
assert_eq!(state.total_weighted_collateral, 10_000_000);
assert_eq!(state.total_liabilities, 0);
assert_eq!(state.free_collateral(), 10_000_000);
assert_eq!(state.credit_usage_bps().unwrap(), 0);
}
#[test]
fn margin_state_deposit_and_borrow() {
let reserve_key = Pubkey::new_unique();
let reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let reserves: HashMap<Pubkey, KaminoReserve> = [(reserve_key, reserve)].into();
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: reserve_key,
deposited_amount: 10_000_000,
market_value_sf: 0,
});
obligation.borrows.push(KaminoObligationLiquidity {
borrow_reserve: reserve_key,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
borrowed_amount_sf: 5_000_000u128 * FRACTION_ONE,
market_value_sf: 0,
borrow_factor_adjusted_market_value_sf: 0,
});
let state = KaminoMarginState::calculate(&obligation, &reserves).unwrap();
assert_eq!(state.total_collateral, 10_000_000);
assert_eq!(state.total_liabilities, 5_000_000);
assert_eq!(state.free_collateral(), 5_000_000);
assert_eq!(state.credit_usage_bps().unwrap(), 5_000); }
#[test]
fn free_collateral_clamped_to_zero() {
let state = KaminoMarginState {
total_collateral: 5_000_000,
total_weighted_collateral: 5_000_000,
total_liabilities: 10_000_000,
total_weighted_liabilities: 10_000_000,
};
assert_eq!(state.free_collateral(), 0);
assert_eq!(state.credit_usage_bps().unwrap(), 10_000); }
#[test]
fn credit_usage_over_100_capped() {
let state = KaminoMarginState {
total_collateral: 5_000_000,
total_weighted_collateral: 5_000_000,
total_liabilities: 20_000_000,
total_weighted_liabilities: 20_000_000,
};
assert_eq!(state.credit_usage_bps().unwrap(), 10_000);
}
#[test]
fn elevation_group_nonzero_errors() {
let mut obligation = empty_obligation();
obligation.elevation_group = 1;
assert!(KaminoMarginState::calculate(&obligation, &HashMap::new()).is_err());
}
#[test]
fn withdraw_limit_no_borrows() {
let key = usdc_reserve_key();
let reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let state = KaminoMarginState {
total_collateral: 10_000_000,
total_weighted_collateral: 10_000_000,
total_liabilities: 0,
total_weighted_liabilities: 0,
};
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: key,
deposited_amount: 10_000_000,
market_value_sf: 0,
});
let limits = calculate_kamino_position_limits(
&state,
&reserve,
usdc_asset_id(),
&obligation,
)
.unwrap();
assert_eq!(limits.withdraw_limit, 10_000_000);
}
#[test]
fn withdraw_limit_with_borrows() {
let key = usdc_reserve_key();
let reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let state = KaminoMarginState {
total_collateral: 10_000_000,
total_weighted_collateral: 10_000_000,
total_liabilities: 5_000_000,
total_weighted_liabilities: 5_000_000,
};
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: key,
deposited_amount: 10_000_000,
market_value_sf: 0,
});
let limits = calculate_kamino_position_limits(
&state,
&reserve,
usdc_asset_id(),
&obligation,
)
.unwrap();
assert_eq!(limits.withdraw_limit, 5_000_000);
}
#[test]
fn borrow_limit_basic() {
let key = usdc_reserve_key();
let reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let state = KaminoMarginState {
total_collateral: 10_000_000,
total_weighted_collateral: 10_000_000,
total_liabilities: 0,
total_weighted_liabilities: 0,
};
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: key,
deposited_amount: 10_000_000,
market_value_sf: 0,
});
let limits = calculate_kamino_position_limits(
&state,
&reserve,
usdc_asset_id(),
&obligation,
)
.unwrap();
assert_eq!(limits.withdraw_limit, 10_000_000);
assert_eq!(limits.borrow_limit, 10_000_000);
}
#[test]
fn borrow_limit_with_other_collateral() {
let usdc_reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
let state = KaminoMarginState {
total_collateral: 100_000_000,
total_weighted_collateral: 80_000_000,
total_liabilities: 0,
total_weighted_liabilities: 0,
};
let obligation = empty_obligation();
let limits = calculate_kamino_position_limits(
&state,
&usdc_reserve,
usdc_asset_id(),
&obligation,
)
.unwrap();
assert_eq!(limits.withdraw_limit, 0);
assert_eq!(limits.borrow_limit, 80_000_000);
}
#[test]
fn borrow_factor_increases_weighted_liability() {
let reserve_key = Pubkey::new_unique();
let mut reserve = make_reserve(100, 100, 1, 6, 10_000_000_000, 10_000_000_000);
reserve.config.borrow_factor_pct = 150;
let mut obligation = empty_obligation();
obligation.deposits.push(KaminoObligationCollateral {
deposit_reserve: reserve_key,
deposited_amount: 100_000_000,
market_value_sf: 0,
});
obligation.borrows.push(KaminoObligationLiquidity {
borrow_reserve: reserve_key,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
borrowed_amount_sf: 50_000_000u128 * FRACTION_ONE,
market_value_sf: 0,
borrow_factor_adjusted_market_value_sf: 0,
});
let reserves: HashMap<Pubkey, KaminoReserve> = [(reserve_key, reserve)].into();
let state = KaminoMarginState::calculate(&obligation, &reserves).unwrap();
assert_eq!(state.total_liabilities, 75_000_000);
}
#[test]
fn zero_price_returns_zero_limits() {
let mut reserve = make_reserve(80, 85, 0, 6, 10_000_000_000, 10_000_000_000);
reserve.liquidity.market_price_sf = 0;
let state = KaminoMarginState {
total_collateral: 10_000_000,
total_weighted_collateral: 8_000_000,
total_liabilities: 0,
total_weighted_liabilities: 0,
};
let obligation = empty_obligation();
let limits = calculate_kamino_position_limits(
&state,
&reserve,
usdc_asset_id(),
&obligation,
)
.unwrap();
assert_eq!(limits.withdraw_limit, 0);
assert_eq!(limits.borrow_limit, 0);
}
#[test]
fn invalid_asset_id_errors() {
assert!(AssetId::new(9999).is_err());
}
#[test]
fn decimal_scale_usdc() {
assert_eq!(decimal_scale(6).unwrap(), (1, 1));
}
#[test]
fn decimal_scale_sol() {
assert_eq!(decimal_scale(9).unwrap(), (1_000, 1));
}
#[test]
fn decimal_scale_small() {
assert_eq!(decimal_scale(4).unwrap(), (1, 100));
}
}