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
27 changes: 14 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rstest = { version = "0.24" }
serde = { version = "1.0", features = ["derive"] }
templar-common = { path = "./common" }
test-utils = { path = "./test-utils" }
thiserror = "2.0.11"
tokio = { version = "1.30.0", features = ["full"] }

[package]
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ edition = "2021"
[dependencies]
near-contract-standards.workspace = true
near-sdk.workspace = true
thiserror.workspace = true
29 changes: 26 additions & 3 deletions common/src/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct BorrowPosition {
borrow_asset_principal: BorrowAssetAmount,
pub borrow_asset_fees: FeeRecord<BorrowAsset>,
pub temporary_lock: BorrowAssetAmount,
pub liquidation_lock: bool,
}

impl BorrowPosition {
Expand All @@ -73,9 +74,19 @@ impl BorrowPosition {
borrow_asset_principal: 0.into(),
borrow_asset_fees: FeeRecord::new(block_height),
temporary_lock: 0.into(),
liquidation_lock: false,
}
}

pub fn full_liquidation(&mut self, block_timestamp_ms: u64) {
self.liquidation_lock = false;
self.started_at_block_timestamp_ms = None;
self.collateral_asset_deposit = 0.into();
self.borrow_asset_principal = 0.into();
self.borrow_asset_fees.total = 0.into();
self.borrow_asset_fees.last_updated_block_height = block_timestamp_ms.into();
}

pub fn get_borrow_asset_principal(&self) -> BorrowAssetAmount {
self.borrow_asset_principal
}
Expand Down Expand Up @@ -123,7 +134,11 @@ impl BorrowPosition {
pub(crate) fn reduce_borrow_asset_liability(
&mut self,
mut amount: BorrowAssetAmount,
) -> LiabilityReduction {
) -> Result<LiabilityReduction, error::LiquidationLockError> {
if self.liquidation_lock {
return Err(error::LiquidationLockError);
}

// No bounds checks necessary here: the min() call prevents underflow.

let amount_to_fees = self.borrow_asset_fees.total.min(amount);
Expand All @@ -139,11 +154,11 @@ impl BorrowPosition {
self.started_at_block_timestamp_ms = None;
}

LiabilityReduction {
Ok(LiabilityReduction {
amount_to_fees,
amount_to_principal,
amount_remaining: amount,
}
})
}
}

Expand All @@ -152,3 +167,11 @@ pub struct LiabilityReduction {
pub amount_to_principal: BorrowAssetAmount,
pub amount_remaining: BorrowAssetAmount,
}

pub mod error {
use thiserror::Error;

#[derive(Error, Debug)]
#[error("This position is currently being liquidated.")]
pub struct LiquidationLockError;
}
6 changes: 2 additions & 4 deletions common/src/market/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub struct MarketConfiguration {
pub borrow_asset: FungibleAsset<BorrowAsset>,
pub collateral_asset: FungibleAsset<CollateralAsset>,
pub balance_oracle_account_id: AccountId,
pub liquidator_account_id: AccountId,
pub minimum_collateral_ratio_per_borrow: Rational<u16>,
/// How much of the deposited principal may be lent out (up to 100%)?
/// This is a matter of protection for supply providers.
Expand All @@ -38,7 +37,7 @@ pub struct MarketConfiguration {
/// NEAR, a "maximum liquidator spread" of 10% would mean that a liquidator
/// could liquidate this borrow by sending 109USDC, netting the liquidator
/// ($110 - $100) * 10% = $1 of NEAR.
pub maximum_liquidator_spread: Rational<u16>,
pub maximum_liquidator_spread: Fraction<u16>,
}

impl MarketConfiguration {
Expand Down Expand Up @@ -108,7 +107,6 @@ mod tests {
borrow_asset: FungibleAsset::nep141("usdt.fakes.testnet".parse().unwrap()),
collateral_asset: FungibleAsset::nep141("wrap.testnet".parse().unwrap()),
balance_oracle_account_id: "root.testnet".parse().unwrap(),
liquidator_account_id: "templar-in-training.testnet".parse().unwrap(),
minimum_collateral_ratio_per_borrow: Rational::new(120, 100),
maximum_borrow_asset_usage_ratio: Fraction::new(99, 100).unwrap(),
borrow_origination_fee: Fee::Proportional(Rational::new(1, 100)),
Expand All @@ -120,7 +118,7 @@ mod tests {
yield_weights: YieldWeights::new_with_supply_weight(8)
.with_static("protocol".parse().unwrap(), 1)
.with_static("insurance".parse().unwrap(), 1),
maximum_liquidator_spread: Rational::new(5, 100),
maximum_liquidator_spread: Fraction::new(5, 100).unwrap(),
})
.unwrap()
);
Expand Down
21 changes: 16 additions & 5 deletions common/src/market/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ impl Market {
borrow_position: &mut BorrowPosition,
amount: BorrowAssetAmount,
) {
let liability_reduction = borrow_position.reduce_borrow_asset_liability(amount);
let liability_reduction = borrow_position
.reduce_borrow_asset_liability(amount)
.unwrap_or_else(|e| env::panic_str(&e.to_string()));

require!(
liability_reduction.amount_remaining.is_zero(),
Expand Down Expand Up @@ -416,15 +418,24 @@ impl Market {
.is_liquidation()
}

pub fn record_liquidation_lock(&mut self, borrow_position: &mut BorrowPosition) {
borrow_position.liquidation_lock = true;
}

pub fn record_liquidation_unlock(&mut self, borrow_position: &mut BorrowPosition) {
borrow_position.liquidation_lock = false;
}

pub fn record_full_liquidation(
&mut self,
borrow_position: &mut BorrowPosition,
mut recovered_amount: BorrowAssetAmount,
) {
if recovered_amount
.split(borrow_position.get_borrow_asset_principal())
.is_some()
{
let principal = borrow_position.get_borrow_asset_principal();
borrow_position.full_liquidation(env::block_timestamp_ms());

// TODO: Is it correct to only care about the original principal here?
if recovered_amount.split(principal).is_some() {
// distribute yield
self.record_borrow_asset_yield_distribution(recovered_amount);
} else {
Expand Down
4 changes: 4 additions & 0 deletions common/src/rational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl<T: Div<Output = T> + BitXor<Output = T> + Sub<Output = T> + Copy + Eq + Ord
Self(a, b).simplify()
}

pub const fn new_const(a: T, b: T) -> Self {
Self(a, b)
}

pub fn simplify(self) -> Self {
let Self(mut n, mut d) = self;

Expand Down
78 changes: 66 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@ impl FungibleTokenReceiver for Contract {
}) => {
let amount = use_borrow_asset();

require!(
sender_id == self.configuration.liquidator_account_id,
"Account not authorized to perform liquidations",
);

let mut borrow_position = self
.borrow_positions
.get(&account_id)
Expand All @@ -171,16 +166,39 @@ impl FungibleTokenReceiver for Contract {
"Borrow position cannot be liquidated",
);

// TODO: Implement `maximum_liquidator_spread` here, since
// we have the price data available in `oracle_price_proof`.
self.record_full_liquidation(&mut borrow_position, amount);
// minimum_acceptable_amount = collateral_amount * (1 - maximum_liquidator_spread) * collateral_price / borrow_price
let minimum_acceptable_amount: BorrowAssetAmount = self
.configuration
.maximum_liquidator_spread
.complement()
.upcast::<u128>()
.checked_mul(oracle_price_proof.collateral_asset_price)
.and_then(|x| x.checked_div(oracle_price_proof.borrow_asset_price))
.and_then(|x| {
x.checked_scalar_mul(borrow_position.collateral_asset_deposit.as_u128())
})
.and_then(|x| x.ceil())
.unwrap() // TODO: Eliminate .unwrap()
.into();

require!(
amount >= minimum_acceptable_amount,
"Too little attached to liquidate",
);

self.record_liquidation_lock(&mut borrow_position);

self.borrow_positions.insert(&account_id, &borrow_position);

// TODO: (cont'd from above) This would allow us to calculate
// the amount that "should" be recovered and refund the
// liquidator any excess.
PromiseOrValue::Value(U128(0))
PromiseOrValue::Promise(
self.configuration
.collateral_asset
.transfer(sender_id, borrow_position.collateral_asset_deposit)
.then(
Self::ext(env::current_account_id())
.after_liquidate(account_id, amount),
),
)
}
}
}
Expand Down Expand Up @@ -658,4 +676,40 @@ impl Contract {
}
}
}

/// Called during liquidation process; checks whether the transfer of
/// collateral to the liquidator was successful.
#[private]
pub fn after_liquidate(
&mut self,
account_id: AccountId,
borrow_asset_amount: BorrowAssetAmount,
) -> U128 {
require!(env::promise_results_count() == 1);

let mut borrow_position = self.borrow_positions.get(&account_id).unwrap_or_else(|| {
env::panic_str("Invariant violation: Liquidation of nonexistent position.")
});

match env::promise_result(0) {
PromiseResult::Successful(_) => {
self.record_full_liquidation(&mut borrow_position, borrow_asset_amount);
U128(0)
}
PromiseResult::Failed => {
// Somehow transfer of collateral failed. This could mean:
//
// 1. Somehow the contract does not have enough collateral
// available. This would be indicative of a *fundamental flaw*
// in the contract (i.e. this should never happen).
//
// 2. More likely, in a multichain context, communication
// broke down somewhere between the signer and the remote RPC.
// Could be as simple as a nonce sync issue. Should just wait
// and try again later.
self.record_liquidation_unlock(&mut borrow_position);
U128(borrow_asset_amount.as_u128())
}
}
}
}
Loading
Loading