use std::ops::{Deref, DerefMut};
use near_sdk::{env, json_types::U64, near, AccountId};
use crate::{
accumulator::{AccumulationRecord, Accumulator},
asset::{BorrowAsset, BorrowAssetAmount, CollateralAssetAmount},
asset_op,
event::MarketEvent,
market::{Market, SnapshotProof},
number::Decimal,
price::{Appraise, Convert, PricePair, Valuation},
YEAR_PER_MS,
};
#[derive(Clone, Copy)]
pub struct InterestAccumulationProof(());
#[cfg(test)]
impl InterestAccumulationProof {
pub fn test() -> Self {
Self(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[near(serializers = [borsh, json])]
pub enum BorrowStatus {
Healthy,
MaintenanceRequired,
Liquidation(LiquidationReason),
}
impl BorrowStatus {
pub fn is_healthy(&self) -> bool {
matches!(self, Self::Healthy)
}
pub fn is_liquidation(&self) -> bool {
matches!(self, Self::Liquidation(..))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[near(serializers = [borsh, json])]
pub enum LiquidationReason {
Undercollateralization,
Expiration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[near(serializers = [borsh, json])]
pub struct BorrowPosition {
pub started_at_block_timestamp_ms: Option<U64>,
pub collateral_asset_deposit: CollateralAssetAmount,
borrow_asset_principal: BorrowAssetAmount,
pub interest: Accumulator<BorrowAsset>,
pub fees: BorrowAssetAmount,
in_flight: BorrowAssetAmount,
pub liquidation_lock: CollateralAssetAmount,
}
impl BorrowPosition {
pub fn new(current_snapshot_index: u32) -> Self {
Self {
started_at_block_timestamp_ms: None,
collateral_asset_deposit: 0.into(),
borrow_asset_principal: 0.into(),
interest: Accumulator::new(current_snapshot_index),
fees: 0.into(),
in_flight: 0.into(),
liquidation_lock: 0.into(),
}
}
pub fn get_borrow_asset_principal(&self) -> BorrowAssetAmount {
let mut total = BorrowAssetAmount::zero();
asset_op! {
total += self.borrow_asset_principal;
total += self.in_flight;
};
total
}
pub fn get_total_borrow_asset_liability(&self) -> BorrowAssetAmount {
let mut total = BorrowAssetAmount::zero();
asset_op! {
total += self.borrow_asset_principal;
total += self.in_flight;
total += self.interest.get_total();
total += self.fees;
};
total
}
pub fn get_total_collateral_amount(&self) -> CollateralAssetAmount {
let mut total = CollateralAssetAmount::zero();
asset_op! {
total += self.collateral_asset_deposit;
total += self.liquidation_lock;
};
total
}
pub fn exists(&self) -> bool {
!self.get_total_collateral_amount().is_zero()
|| !self.get_total_borrow_asset_liability().is_zero()
}
pub fn collateralization_ratio(&self, price_pair: &PricePair) -> Option<Decimal> {
let borrow_liability = self.get_total_borrow_asset_liability();
if borrow_liability.is_zero() {
return None;
}
let collateral_valuation =
Valuation::pessimistic(self.get_total_collateral_amount(), &price_pair.collateral);
let borrow_valuation = Valuation::optimistic(borrow_liability, &price_pair.borrow);
collateral_valuation.ratio(borrow_valuation)
}
pub(crate) fn increase_borrow_asset_principal(
&mut self,
_proof: InterestAccumulationProof,
amount: BorrowAssetAmount,
block_timestamp_ms: u64,
) -> Option<()> {
if self.started_at_block_timestamp_ms.is_none()
|| self.get_total_borrow_asset_liability().is_zero()
{
self.started_at_block_timestamp_ms = Some(block_timestamp_ms.into());
}
self.borrow_asset_principal.join(amount)
}
pub fn liquidatable_collateral(
&self,
price_pair: &PricePair,
mcr: Decimal,
liquidator_spread: Decimal,
) -> CollateralAssetAmount {
let collateral_amount = self.get_total_collateral_amount();
let collateral_amount_dec = Decimal::from(collateral_amount);
let liability_valuation = price_pair.valuation(self.get_total_borrow_asset_liability());
#[allow(clippy::unwrap_used, reason = "not div0")]
let scaled_liability = liability_valuation
.ratio(price_pair.valuation(CollateralAssetAmount::new(1)))
.unwrap();
let scaled_collateral_amount =
collateral_amount_dec * (Decimal::ONE - liquidator_spread) / mcr;
if scaled_liability <= scaled_collateral_amount {
CollateralAssetAmount::zero()
} else {
let unscaled_amount =
(scaled_liability - scaled_collateral_amount) / (mcr - Decimal::ONE);
let mut available = if unscaled_amount >= collateral_amount_dec / mcr {
collateral_amount
} else {
let amount = mcr * unscaled_amount;
amount
.to_u128_ceil()
.map_or(collateral_amount, CollateralAssetAmount::new)
};
asset_op! {
@msg("Invariant violation: get_total_collateral_amount() >= liquidation_lock should hold")
available -= self.liquidation_lock;
};
available
}
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct LiabilityReduction {
pub to_fees: BorrowAssetAmount,
pub to_interest: BorrowAssetAmount,
pub to_principal: BorrowAssetAmount,
pub remaining: BorrowAssetAmount,
}
#[must_use]
#[derive(Debug, Clone)]
#[near(serializers = [json, borsh])]
pub struct InitialLiquidation {
pub liquidated: CollateralAssetAmount,
pub recovered: BorrowAssetAmount,
pub refund: BorrowAssetAmount,
}
#[must_use]
#[derive(Debug, Clone)]
#[near(serializers = [json, borsh])]
pub struct InitialBorrow {
pub amount: BorrowAssetAmount,
pub fees: BorrowAssetAmount,
}
pub mod error {
use thiserror::Error;
use crate::asset::{BorrowAssetAmount, CollateralAssetAmount};
#[derive(Error, Debug)]
#[error("This position is currently being liquidated.")]
pub struct LiquidationLockError;
#[derive(Error, Debug)]
pub enum InitialLiquidationError {
#[error("Borrow position is not eligible for liquidation")]
Ineligible,
#[error("Attempt to liquidate more collateral than is currently eligible: {requested} requested > {available} available")]
ExcessiveLiquidation {
requested: CollateralAssetAmount,
available: CollateralAssetAmount,
},
#[error("Failed to calculate value of collateral")]
ValueCalculationFailure,
#[error("Liquidation offer too low: {offered} offered < {minimum_acceptable} minimum acceptable")]
OfferTooLow {
offered: BorrowAssetAmount,
minimum_acceptable: BorrowAssetAmount,
},
}
#[derive(Debug, Error)]
pub enum InitialBorrowError {
#[error("Insufficient borrow asset available")]
InsufficientBorrowAssetAvailable,
#[error("Fee calculation failed")]
FeeCalculationFailure,
#[error("Borrow position must be healthy after borrow")]
Undercollateralization,
#[error("New borrow position is outside of allowable range")]
OutsideAllowableRange,
}
}
pub struct BorrowPositionRef<M> {
market: M,
account_id: AccountId,
position: BorrowPosition,
}
impl<M> BorrowPositionRef<M> {
pub fn new(market: M, account_id: AccountId, position: BorrowPosition) -> Self {
Self {
market,
account_id,
position,
}
}
pub fn account_id(&self) -> &AccountId {
&self.account_id
}
pub fn inner(&self) -> &BorrowPosition {
&self.position
}
}
impl<M: Deref<Target = Market>> BorrowPositionRef<M> {
pub fn estimate_current_snapshot_interest(&self) -> BorrowAssetAmount {
let prev_end_timestamp_ms = self.market.get_last_finalized_snapshot().end_timestamp_ms.0;
let interest_in_current_snapshot = self.market.interest_rate()
* (env::block_timestamp_ms().saturating_sub(prev_end_timestamp_ms))
* Decimal::from(self.position.get_borrow_asset_principal())
* YEAR_PER_MS;
#[allow(clippy::unwrap_used, reason = "Interest rate guaranteed <= APY_LIMIT")]
interest_in_current_snapshot.to_u128_ceil().unwrap().into()
}
pub fn with_pending_interest(&mut self) {
let mut pending_estimate = self.calculate_interest(u32::MAX).get_amount();
asset_op!(pending_estimate += self.estimate_current_snapshot_interest());
self.position.interest.pending_estimate = pending_estimate;
}
pub(crate) fn calculate_interest(
&self,
snapshot_limit: u32,
) -> AccumulationRecord<BorrowAsset> {
let principal: Decimal = self.position.get_borrow_asset_principal().into();
let mut next_snapshot_index = self.position.interest.get_next_snapshot_index();
let mut accumulated = Decimal::ZERO;
#[allow(clippy::unwrap_used, reason = "1 finalized snapshot guaranteed")]
let mut prev_end_timestamp_ms = self
.market
.finalized_snapshots
.get(next_snapshot_index.checked_sub(1).unwrap())
.unwrap()
.end_timestamp_ms
.0;
#[allow(
clippy::cast_possible_truncation,
reason = "Assume # of snapshots will never be > u32::MAX"
)]
for (i, snapshot) in self
.market
.finalized_snapshots
.iter()
.enumerate()
.skip(next_snapshot_index as usize)
.take(snapshot_limit as usize)
{
let duration_ms = Decimal::from(
snapshot
.end_timestamp_ms
.0
.checked_sub(prev_end_timestamp_ms)
.unwrap_or_else(|| {
env::panic_str(&format!(
"Invariant violation: Snapshot timestamp decrease at time chunk #{}.",
u64::from(snapshot.time_chunk.0),
))
}),
);
accumulated += principal * snapshot.interest_rate * duration_ms * YEAR_PER_MS;
prev_end_timestamp_ms = snapshot.end_timestamp_ms.0;
next_snapshot_index = i as u32 + 1;
}
AccumulationRecord {
#[allow(
clippy::unwrap_used,
reason = "Assume accumulated interest will never exceed u128::MAX"
)]
amount: accumulated.to_u128_floor().unwrap().into(),
fraction_as_u128_dividend: accumulated.fractional_part_as_u128_dividend(),
next_snapshot_index,
}
}
pub fn status(&self, price_pair: &PricePair, block_timestamp_ms: u64) -> BorrowStatus {
let collateralization_ratio = self.position.collateralization_ratio(price_pair);
self.market.configuration.borrow_status(
collateralization_ratio,
self.position.started_at_block_timestamp_ms,
block_timestamp_ms,
)
}
pub fn within_allowable_borrow_range(&self) -> bool {
self.market
.configuration
.borrow_range
.contains(self.position.get_borrow_asset_principal())
}
pub fn liquidatable_collateral(&self, price_pair: &PricePair) -> CollateralAssetAmount {
self.position.liquidatable_collateral(
price_pair,
self.market.configuration.borrow_mcr_maintenance,
self.market.configuration.liquidation_maximum_spread,
)
}
}
pub struct BorrowPositionGuard<'a>(BorrowPositionRef<&'a mut Market>);
impl Drop for BorrowPositionGuard<'_> {
fn drop(&mut self) {
self.0
.market
.borrow_positions
.insert(&self.0.account_id, &self.0.position);
}
}
impl<'a> Deref for BorrowPositionGuard<'a> {
type Target = BorrowPositionRef<&'a mut Market>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BorrowPositionGuard<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> BorrowPositionGuard<'a> {
pub fn new(market: &'a mut Market, account_id: AccountId, position: BorrowPosition) -> Self {
Self(BorrowPositionRef::new(market, account_id, position))
}
pub(crate) fn reduce_borrow_asset_liability(
&mut self,
_proof: InterestAccumulationProof,
mut amount: BorrowAssetAmount,
) -> LiabilityReduction {
let to_fees = self.position.fees.min(amount);
asset_op! {
@msg("Invariant violation: min() precludes underflow")
amount -= to_fees;
self.position.fees -= to_fees;
};
let to_interest = self.position.interest.get_total().min(amount);
asset_op! {
@msg("Invariant violation: min() precludes underflow")
amount -= to_interest;
};
self.position.interest.remove(to_interest);
let to_principal = {
let minimum_amount = u128::from(self.market.configuration.borrow_range.minimum);
let amount_remaining =
u128::from(self.position.borrow_asset_principal).saturating_sub(u128::from(amount));
if amount_remaining > 0 && amount_remaining < minimum_amount {
u128::from(self.position.borrow_asset_principal)
.saturating_sub(minimum_amount)
.into()
} else {
self.position.borrow_asset_principal.min(amount)
}
};
asset_op! {
@msg("Invariant violation: amount_to_principal > amount")
amount -= to_principal;
@msg("Invariant violation: amount_to_principal > borrow_asset_principal")
self.position.borrow_asset_principal -= to_principal;
@msg("Invariant violation: amount_to_principal > market.borrow_asset_borrowed")
self.market.borrow_asset_borrowed -= to_principal;
};
if self.position.borrow_asset_principal.is_zero() {
self.position.started_at_block_timestamp_ms = None;
}
LiabilityReduction {
to_fees,
to_interest,
to_principal,
remaining: amount,
}
}
pub fn record_collateral_asset_deposit(
&mut self,
_proof: InterestAccumulationProof,
amount: CollateralAssetAmount,
) {
asset_op! {
self.position.collateral_asset_deposit += amount;
self.market.collateral_asset_deposited += amount;
};
MarketEvent::CollateralDeposited {
account_id: self.account_id.clone(),
collateral_asset_amount: amount,
}
.emit();
}
pub fn record_collateral_asset_withdrawal(
&mut self,
_proof: InterestAccumulationProof,
amount: CollateralAssetAmount,
) {
asset_op! {
self.position.collateral_asset_deposit -= amount;
self.market.collateral_asset_deposited -= amount;
};
MarketEvent::CollateralWithdrawn {
account_id: self.account_id.clone(),
collateral_asset_amount: amount,
}
.emit();
}
pub fn record_borrow_initial(
&mut self,
_proof: SnapshotProof,
_interest: InterestAccumulationProof,
amount: BorrowAssetAmount,
price_pair: &PricePair,
block_timestamp_ms: u64,
) -> Result<InitialBorrow, error::InitialBorrowError> {
let available_to_borrow = self.market.get_borrow_asset_available_to_borrow();
if amount > available_to_borrow {
return Err(error::InitialBorrowError::InsufficientBorrowAssetAvailable);
}
let origination_fee = self
.market
.configuration
.borrow_origination_fee
.of(amount)
.ok_or(error::InitialBorrowError::FeeCalculationFailure)?;
let single_snapshot_fee = self
.market
.single_snapshot_fee(amount)
.ok_or(error::InitialBorrowError::FeeCalculationFailure)?;
let mut fees = origination_fee;
fees.join(single_snapshot_fee)
.ok_or(error::InitialBorrowError::FeeCalculationFailure)?;
asset_op! {
self.market.borrow_asset_borrowed_in_flight += amount;
self.position.in_flight += amount;
self.position.fees += fees;
};
if !self.status(price_pair, block_timestamp_ms).is_healthy() {
asset_op! {
self.market.borrow_asset_borrowed_in_flight -= amount;
self.position.in_flight -= amount;
self.position.fees -= fees;
};
return Err(error::InitialBorrowError::Undercollateralization);
}
if !self.within_allowable_borrow_range() {
asset_op! {
self.market.borrow_asset_borrowed_in_flight -= amount;
self.position.in_flight -= amount;
self.position.fees -= fees;
};
return Err(error::InitialBorrowError::OutsideAllowableRange);
}
self.market.record_borrow_asset_yield_distribution(fees);
Ok(InitialBorrow { amount, fees })
}
pub fn record_borrow_final(
&mut self,
_snapshot: SnapshotProof,
interest: InterestAccumulationProof,
borrow: &InitialBorrow,
success: bool,
block_timestamp_ms: u64,
) {
asset_op! {
self.market.borrow_asset_borrowed_in_flight -= borrow.amount;
self.position.in_flight -= borrow.amount;
};
if success {
self.position
.increase_borrow_asset_principal(interest, borrow.amount, block_timestamp_ms)
.unwrap_or_else(|| env::panic_str("Increase borrow asset principal overflow"));
asset_op!(self.market.borrow_asset_borrowed += borrow.amount);
MarketEvent::BorrowWithdrawn {
account_id: self.account_id.clone(),
borrow_asset_amount: borrow.amount,
}
.emit();
} else {
}
}
pub fn record_repay(
&mut self,
proof: InterestAccumulationProof,
amount: BorrowAssetAmount,
) -> Result<BorrowAssetAmount, error::LiquidationLockError> {
if !self.position.liquidation_lock.is_zero() {
return Err(error::LiquidationLockError);
}
let liability_reduction = self.reduce_borrow_asset_liability(proof, amount);
MarketEvent::BorrowRepaid {
account_id: self.account_id.clone(),
borrow_asset_fees_repaid: liability_reduction.to_fees,
borrow_asset_principal_repaid: liability_reduction.to_principal,
borrow_asset_principal_remaining: self.position.get_borrow_asset_principal(),
}
.emit();
Ok(liability_reduction.remaining)
}
pub fn accumulate_interest_partial(&mut self, snapshot_limit: u32) {
let accumulation_record = self.calculate_interest(snapshot_limit);
if !accumulation_record.amount.is_zero() {
MarketEvent::InterestAccumulated {
account_id: self.account_id.clone(),
borrow_asset_amount: accumulation_record.amount,
}
.emit();
}
self.position.interest.accumulate(accumulation_record);
}
pub fn accumulate_interest(&mut self) -> InterestAccumulationProof {
self.accumulate_interest_partial(u32::MAX);
InterestAccumulationProof(())
}
pub fn liquidation_lock(&mut self, amount: CollateralAssetAmount) {
asset_op!(
@msg("Attempt to liquidate more collateral than position has deposited")
self.position.collateral_asset_deposit -= amount;
self.position.liquidation_lock += amount;
);
}
pub fn liquidation_unlock(&mut self, amount: CollateralAssetAmount) {
asset_op!(
@msg("Invariant violation: Liquidation unlock of more collateral that was locked")
self.position.liquidation_lock -= amount;
self.position.collateral_asset_deposit += amount;
);
}
pub fn record_liquidation_initial(
&mut self,
_proof: InterestAccumulationProof,
liquidator_sent: BorrowAssetAmount,
liquidator_request: Option<CollateralAssetAmount>,
price_pair: &PricePair,
block_timestamp_ms: u64,
) -> Result<InitialLiquidation, error::InitialLiquidationError> {
let BorrowStatus::Liquidation(reason) = self.status(price_pair, block_timestamp_ms) else {
return Err(error::InitialLiquidationError::Ineligible);
};
let liquidatable_collateral = match reason {
LiquidationReason::Undercollateralization => self.liquidatable_collateral(price_pair),
LiquidationReason::Expiration => self.position.collateral_asset_deposit,
};
let liquidator_request = liquidator_request.unwrap_or(liquidatable_collateral);
if liquidator_request > liquidatable_collateral {
return Err(error::InitialLiquidationError::ExcessiveLiquidation {
requested: liquidator_request,
available: liquidatable_collateral,
});
}
let collateral_value = price_pair.convert(liquidator_request);
let maximum_acceptable: BorrowAssetAmount = collateral_value
.to_u128_ceil()
.ok_or(error::InitialLiquidationError::ValueCalculationFailure)?
.max(1)
.into();
#[allow(
clippy::unwrap_used,
reason = "Previous line guarantees this will not panic"
)]
let minimum_acceptable: BorrowAssetAmount = (collateral_value
* (Decimal::ONE - self.market.configuration.liquidation_maximum_spread))
.to_u128_ceil()
.unwrap()
.max(1)
.into();
if liquidator_sent < minimum_acceptable {
return Err(error::InitialLiquidationError::OfferTooLow {
offered: liquidator_sent,
minimum_acceptable,
});
}
self.liquidation_lock(liquidator_request);
let mut refund = BorrowAssetAmount::zero();
let mut recovered = liquidator_sent;
if liquidator_sent > maximum_acceptable {
recovered = maximum_acceptable;
refund = liquidator_sent;
asset_op!(refund -= recovered);
}
Ok(InitialLiquidation {
liquidated: liquidator_request,
recovered,
refund,
})
}
pub fn record_liquidation_final(
&mut self,
proof: InterestAccumulationProof,
liquidator_id: AccountId,
initial_liquidation: &InitialLiquidation,
) {
let liability_reduction =
self.reduce_borrow_asset_liability(proof, initial_liquidation.recovered);
self.market
.record_borrow_asset_yield_distribution(liability_reduction.remaining);
self.liquidation_unlock(initial_liquidation.liquidated);
self.record_collateral_asset_withdrawal(proof, initial_liquidation.liquidated);
MarketEvent::Liquidation {
liquidator_id,
account_id: self.account_id.clone(),
borrow_asset_recovered: initial_liquidation.recovered,
collateral_asset_liquidated: initial_liquidation.liquidated,
}
.emit();
}
}