Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions common/src/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@ use crate::asset::{
#[near(serializers = [borsh, json])]
pub enum BorrowStatus {
Healthy,
Liquidation,
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(Default, Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -41,6 +58,7 @@ impl<T: AssetClass> FeeRecord<T> {
#[derive(Clone, Copy, 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 borrow_asset_fees: FeeRecord<BorrowAsset>,
Expand All @@ -50,6 +68,7 @@ pub struct BorrowPosition {
impl BorrowPosition {
pub fn new(block_height: u64) -> Self {
Self {
started_at_block_timestamp_ms: None,
collateral_asset_deposit: 0.into(),
borrow_asset_principal: 0.into(),
borrow_asset_fees: FeeRecord::new(block_height),
Expand Down Expand Up @@ -88,7 +107,16 @@ impl BorrowPosition {
self.collateral_asset_deposit.split(amount)
}

pub fn increase_borrow_asset_principal(&mut self, amount: BorrowAssetAmount) -> Option<()> {
pub fn increase_borrow_asset_principal(
&mut self,
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)
}

Expand All @@ -106,6 +134,11 @@ impl BorrowPosition {
amount.split(amount_to_principal);
self.borrow_asset_principal.split(amount_to_principal);

if self.borrow_asset_principal.is_zero() {
// fully paid off
self.started_at_block_timestamp_ms = None;
}

LiabilityReduction {
amount_to_fees,
amount_to_principal,
Expand Down
36 changes: 34 additions & 2 deletions common/src/market/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use near_sdk::{json_types::U64, near, AccountId};

use crate::{
asset::{BorrowAsset, BorrowAssetAmount, CollateralAsset, FungibleAsset},
borrow::BorrowPosition,
borrow::{BorrowPosition, BorrowStatus, LiquidationReason},
fee::{Fee, TimeBasedFee},
rational::{Fraction, Rational},
};
Expand Down Expand Up @@ -42,7 +42,39 @@ pub struct MarketConfiguration {
}

impl MarketConfiguration {
pub fn is_healthy(
pub fn borrow_status(
&self,
borrow_position: &BorrowPosition,
oracle_price_proof: OraclePriceProof,
block_timestamp_ms: u64,
) -> BorrowStatus {
if !self.is_within_minimum_collateral_ratio(borrow_position, oracle_price_proof) {
return BorrowStatus::Liquidation(LiquidationReason::Undercollateralization);
}

if !self.is_within_maximum_borrow_duration(borrow_position, block_timestamp_ms) {
return BorrowStatus::Liquidation(LiquidationReason::Expiration);
}

BorrowStatus::Healthy
}

fn is_within_maximum_borrow_duration(
&self,
borrow_position: &BorrowPosition,
block_timestamp_ms: u64,
) -> bool {
if let Some(U64(maximum_duration_ms)) = self.maximum_borrow_duration_ms {
borrow_position
.started_at_block_timestamp_ms
.and_then(|U64(started_at_ms)| block_timestamp_ms.checked_sub(started_at_ms))
.map_or(true, |duration_ms| duration_ms <= maximum_duration_ms)
} else {
true
}
}

pub fn is_within_minimum_collateral_ratio(
&self,
borrow_position: &BorrowPosition,
OraclePriceProof {
Expand Down
12 changes: 8 additions & 4 deletions common/src/market/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ impl Market {
.borrow_asset_fees
.accumulate_fees(fees, env::block_height());
borrow_position
.increase_borrow_asset_principal(amount)
.increase_borrow_asset_principal(amount, env::block_timestamp_ms())
.unwrap_or_else(|| env::panic_str("Increase borrow asset principal overflow"));
}

Expand Down Expand Up @@ -407,9 +407,13 @@ impl Market {
return false;
};

!self
.configuration
.is_healthy(&borrow_position, oracle_price_proof)
self.configuration
.borrow_status(
&borrow_position,
oracle_price_proof,
env::block_timestamp_ms(),
)
.is_liquidation()
}

pub fn record_full_liquidation(
Expand Down
38 changes: 22 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ impl FungibleTokenReceiver for Contract {
.unwrap_or_else(|| BorrowPosition::new(env::block_height()));

require!(
!self
.configuration
.is_healthy(&borrow_position, oracle_price_proof),
"Borrow position cannot be liquidated at this price",
self.configuration
.borrow_status(
&borrow_position,
oracle_price_proof,
env::block_timestamp_ms(),
)
.is_liquidation(),
"Borrow position cannot be liquidated",
);

// TODO: Implement `maximum_liquidator_spread` here, since
Expand Down Expand Up @@ -235,14 +239,11 @@ impl MarketExternalInterface for Contract {
return None;
};

if self
.configuration
.is_healthy(&borrow_position, oracle_price_proof)
{
Some(BorrowStatus::Healthy)
} else {
Some(BorrowStatus::Liquidation)
}
Some(self.configuration.borrow_status(
&borrow_position,
oracle_price_proof,
env::block_timestamp_ms(),
))
}

fn get_collateral_asset_deposit_address_for(
Expand Down Expand Up @@ -298,9 +299,9 @@ impl MarketExternalInterface for Contract {

if !borrow_position.get_total_borrow_asset_liability().is_zero() {
require!(
self.configuration.is_healthy(
self.configuration.is_within_minimum_collateral_ratio(
&borrow_position,
oracle_price_proof.unwrap_or_else(|| env::panic_str("Must provide price"))
oracle_price_proof.unwrap_or_else(|| env::panic_str("Must provide price")),
),
"Borrow must still be above MCR after collateral withdrawal.",
)
Expand Down Expand Up @@ -534,8 +535,13 @@ impl Contract {

require!(
self.configuration
.is_healthy(&borrow_position, oracle_price_proof),
"Cannot borrow beyond MCR",
.borrow_status(
&borrow_position,
oracle_price_proof,
env::block_timestamp_ms(),
)
.is_healthy(),
"New position would be in liquidation",
);

self.borrow_positions.insert(&account_id, &borrow_position);
Expand Down
2 changes: 2 additions & 0 deletions test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub const EQUAL_PRICE: OraclePriceProof = OraclePriceProof {
};

pub struct TestController {
pub worker: Worker<Sandbox>,
pub contract: Contract,
pub borrow_asset: Contract,
pub collateral_asset: Contract,
Expand Down Expand Up @@ -555,6 +556,7 @@ pub async fn setup_everything(
.await;

let c = TestController {
worker,
contract,
collateral_asset,
borrow_asset,
Expand Down
40 changes: 40 additions & 0 deletions tests/maximum_borrow_duration_ms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use near_sdk::json_types::U64;
use templar_common::borrow::{BorrowStatus, LiquidationReason};
use test_utils::*;

#[tokio::test]
async fn liquidation_after_expiration() {
let SetupEverything {
c,
supply_user,
borrow_user,
..
} = setup_everything(|c| {
c.maximum_borrow_duration_ms = Some(U64(100));
})
.await;

c.supply(&supply_user, 1000).await;
c.collateralize(&borrow_user, 2000).await;
c.borrow(&borrow_user, 100, EQUAL_PRICE).await;

let status = c
.get_borrow_status(borrow_user.id(), EQUAL_PRICE)
.await
.unwrap();

assert!(status.is_healthy());

c.worker.fast_forward(10).await.unwrap();

let status = c
.get_borrow_status(borrow_user.id(), EQUAL_PRICE)
.await
.unwrap();

assert_eq!(
status,
BorrowStatus::Liquidation(LiquidationReason::Expiration),
"Borrow should be in liquidation after expiration",
);
}
Loading