diff --git a/contracts/lease/src/contract/cmd/liquidation_status.rs b/contracts/lease/src/contract/cmd/liquidation_status.rs index 28f3ad035..5466a7b2d 100644 --- a/contracts/lease/src/contract/cmd/liquidation_status.rs +++ b/contracts/lease/src/contract/cmd/liquidation_status.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use finance::{currency::Currency, liability::Zone}; +use finance::{ + currency::Currency, + liability::{Cause, Liquidation, Status, Zone}, +}; use lpp::stub::loan::LppLoan as LppLoanTrait; use oracle::stub::{Oracle as OracleTrait, OracleRef}; use platform::batch::Batch; @@ -10,7 +13,7 @@ use timealarms::stub::TimeAlarmsRef; use crate::{ api::LeaseCoin, error::{ContractError, ContractResult}, - lease::{with_lease::WithLease, Cause, Lease, LeaseDTO, Liquidation, Status}, + lease::{with_lease::WithLease, Lease, LeaseDTO}, }; pub(crate) fn status_and_schedule( diff --git a/contracts/lease/src/contract/state/opened/event.rs b/contracts/lease/src/contract/state/opened/event.rs index b551af271..a4205a533 100644 --- a/contracts/lease/src/contract/state/opened/event.rs +++ b/contracts/lease/src/contract/state/opened/event.rs @@ -1,4 +1,4 @@ -use finance::liability::Level; +use finance::liability::{Cause, Level}; use platform::batch::{Emit, Emitter}; use sdk::cosmwasm_std::Env; @@ -6,7 +6,7 @@ use crate::{ api::DownpaymentCoin, contract::cmd::{LiquidationDTO, OpenLoanRespResult, ReceiptDTO}, event::Type, - lease::{Cause, LeaseDTO}, + lease::LeaseDTO, }; pub(super) fn emit_lease_opened( diff --git a/contracts/lease/src/lease/liquidation.rs b/contracts/lease/src/lease/liquidation.rs index a05a4bb21..bf8e36630 100644 --- a/contracts/lease/src/lease/liquidation.rs +++ b/contracts/lease/src/lease/liquidation.rs @@ -1,10 +1,9 @@ -use serde::{Deserialize, Serialize}; +use serde::Serialize; use finance::{ coin::Coin, currency::Currency, - liability::{Liability, Zone}, - percent::Percent, + liability::{check_liability, Status}, price::{self, Price}, zero::Zero, }; @@ -16,58 +15,6 @@ use crate::{error::ContractResult, loan::LiabilityStatus}; use super::Lease; -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(test, derive(Debug))] -pub(crate) enum Status -where - Asset: Currency, -{ - NoDebt, - No(Zone), - Liquidation(Liquidation), -} - -impl Status -where - Asset: Currency, -{ - fn partial(amount: Coin, cause: Cause) -> Self { - Self::Liquidation(Liquidation::Partial { amount, cause }) - } - - fn full(cause: Cause) -> Self { - Self::Liquidation(Liquidation::Full(cause)) - } - - #[cfg(debug_assertion)] - fn amount( - &self, - lease: &Lease, - ) -> Coin { - match self { - Self::No(_) => Default::default(), - Self::Liquidation(liq) => liq.amount(lease), - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(test, derive(Debug))] -pub(crate) enum Liquidation -where - Asset: Currency, -{ - Partial { amount: Coin, cause: Cause }, - Full(Cause), -} - -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -#[cfg_attr(test, derive(Debug))] -pub(crate) enum Cause { - Overdue(), - Liability { ltv: Percent, healthy_ltv: Percent }, -} - impl Lease where Lpn: Currency + Serialize, @@ -107,570 +54,3 @@ where Ok(self.oracle.price_of::()?) } } - -impl Liquidation -where - Asset: Currency, -{ - #[cfg(debug_assertion)] - pub(crate) fn amount( - &self, - lease: &Lease, - ) -> Coin { - match self { - Self::Partial { amount, cause: _ } => *amount, - Self::Full(_) => lease.amount, - } - } -} - -fn check_liability( - spec: &Liability, - asset: Coin, - total_due: Coin, - overdue: Coin, - liquidation_threshold: Coin, -) -> Status -where - Asset: Currency, -{ - may_ask_liquidation_liability(spec, asset, total_due, liquidation_threshold) - .max(may_ask_liquidation_overdue( - asset, - overdue, - liquidation_threshold, - )) - .unwrap_or_else(|| no_liquidation(spec, asset, total_due)) -} - -fn no_liquidation( - spec: &Liability, - asset: Coin, - total_due: Coin, -) -> Status -where - Asset: Currency, -{ - if total_due.is_zero() { - Status::NoDebt - } else { - let ltv = Percent::from_ratio(total_due, asset); - debug_assert!(ltv < spec.max()); - - Status::No(spec.zone_of(ltv)) - } -} - -fn may_ask_liquidation_liability( - spec: &Liability, - asset: Coin, - total_due: Coin, - liquidation_threshold: Coin, -) -> Option> -where - Asset: Currency, -{ - may_ask_liquidation( - asset, - Cause::Liability { - ltv: spec.max(), - healthy_ltv: spec.healthy_percent(), - }, - spec.amount_to_liquidate(asset, total_due), - liquidation_threshold, - ) -} - -fn may_ask_liquidation_overdue( - asset: Coin, - overdue: Coin, - liquidation_threshold: Coin, -) -> Option> -where - Asset: Currency, -{ - may_ask_liquidation(asset, Cause::Overdue(), overdue, liquidation_threshold) -} - -fn may_ask_liquidation( - asset: Coin, - cause: Cause, - liquidation: Coin, - liquidation_threshold: Coin, -) -> Option> -where - Asset: Currency, -{ - if liquidation.is_zero() { - None - } else if asset.saturating_sub(liquidation) <= liquidation_threshold { - Some(Status::full(cause)) - } else { - Some(Status::partial(liquidation, cause)) - } -} - -#[cfg(test)] -mod tests { - use currency::lease::Atom; - use finance::{ - coin::Amount, - duration::Duration, - liability::{Liability, Zone}, - percent::Percent, - }; - - use super::{super::Status, check_liability, Cause}; - - #[test] - fn no_debt() { - let warn_ltv = Percent::from_permille(11); - let spec = liability_with_first(warn_ltv); - assert_eq!( - check_liability::(&spec, 100.into(), 0.into(), 0.into(), 0.into()), - Status::NoDebt, - ); - } - - #[test] - fn warnings_none() { - let warn_ltv = Percent::from_percent(51); - let spec = liability_with_first(warn_ltv); - assert_eq!( - check_liability::(&spec, 100.into(), 1.into(), 0.into(), 0.into()), - Status::No(Zone::no_warnings(spec.first_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 100.into(), 49.into(), 0.into(), 0.into()), - Status::No(Zone::no_warnings(spec.first_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 100.into(), 50.into(), 0.into(), 0.into()), - Status::No(Zone::no_warnings(spec.first_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 505.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 509.into(), 0.into(), 0.into()), - Status::No(Zone::no_warnings(spec.first_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 510.into(), 0.into(), 0.into()), - Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 510.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()), - ); - } - - #[test] - fn warnings_first() { - let spec = liability_with_first(Percent::from_permille(712)); - - assert_eq!( - check_liability::(&spec, 1000.into(), 711.into(), 0.into(), 0.into()), - Status::No(Zone::no_warnings(spec.first_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 712.into(), 0.into(), 0.into()), - Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 712.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()) - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 715.into(), 0.into(), 0.into()), - Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 721.into(), 0.into(), 0.into()), - Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 722.into(), 0.into(), 0.into()), - Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), - ); - } - - #[test] - fn warnings_second() { - let spec = liability_with_second(Percent::from_permille(123)); - - assert_eq!( - check_liability::(&spec, 1000.into(), 122.into(), 0.into(), 0.into()), - Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 123.into(), 0.into(), 0.into()), - Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 124.into(), 0.into(), 0.into()), - Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 128.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()) - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 132.into(), 0.into(), 0.into()), - Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 133.into(), 0.into(), 0.into()), - Status::No(Zone::third(spec.third_liq_warn(), spec.max())), - ); - } - - #[test] - fn warnings_third() { - let warn_third_ltv = Percent::from_permille(381); - let max_ltv = warn_third_ltv + STEP; - let spec = liability_with_third(warn_third_ltv); - - assert_eq!( - check_liability::(&spec, 1000.into(), 380.into(), 0.into(), 0.into()), - Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 381.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()) - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 381.into(), 0.into(), 0.into()), - Status::No(Zone::third(spec.third_liq_warn(), spec.max())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 382.into(), 0.into(), 0.into()), - Status::No(Zone::third(spec.third_liq_warn(), spec.max())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 390.into(), 0.into(), 0.into()), - Status::No(Zone::third(spec.third_liq_warn(), spec.max())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 391.into(), 0.into(), 0.into()), - Status::partial( - 384.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - } - - #[test] - fn liquidate_partial() { - let max_ltv = Percent::from_permille(881); - let spec = liability_with_max(max_ltv); - - assert_eq!( - check_liability::(&spec, 1000.into(), 880.into(), 0.into(), 0.into()), - Status::No(Zone::third(spec.third_liq_warn(), spec.max())), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 880.into(), 1.into(), 0.into()), - Status::partial(1.into(), Cause::Overdue()), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 881.into(), 0.into(), 0.into()), - Status::partial( - 879.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 881.into(), 878.into(), 0.into()), - Status::partial( - 879.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 881.into(), 879.into(), 0.into()), - Status::partial( - 879.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 881.into(), 880.into(), 0.into()), - Status::partial(880.into(), Cause::Overdue()), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 999.into(), 997.into(), 0.into()), - Status::partial( - 998.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 1000.into(), 1.into(), 0.into()), - Status::full(Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - }), - ); - } - - #[test] - fn liquidate_full() { - let max_ltv = Percent::from_permille(768); - let spec = liability_with_max(max_ltv); - - assert_eq!( - check_liability::(&spec, 1000.into(), 768.into(), 765.into(), 0.into()), - Status::partial( - 765.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 768.into(), 766.into(), 0.into()), - Status::partial(766.into(), Cause::Overdue()), - ); - assert_eq!( - check_liability::(&spec, 1000.into(), 1000.into(), 1.into(), 0.into()), - Status::full(Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - }), - ); - let back_to_healthy: Amount = spec.amount_to_liquidate(1000, 900); - assert_eq!( - check_liability::( - &spec, - 1000.into(), - 900.into(), - back_to_healthy.into(), - (1000 - back_to_healthy - 1).into() - ), - Status::partial( - back_to_healthy.into(), - Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - } - ), - ); - assert_eq!( - check_liability::( - &spec, - 1000.into(), - 900.into(), - (back_to_healthy + 1).into(), - (1000 - back_to_healthy - 2).into() - ), - Status::partial((back_to_healthy + 1).into(), Cause::Overdue()), - ); - assert_eq!( - check_liability::( - &spec, - 1000.into(), - 900.into(), - back_to_healthy.into(), - (1000 - back_to_healthy).into() - ), - Status::full(Cause::Liability { - ltv: max_ltv, - healthy_ltv: STEP - }), - ); - assert_eq!( - check_liability::( - &spec, - 1000.into(), - 900.into(), - (back_to_healthy + 1).into(), - (1000 - back_to_healthy - 1).into() - ), - Status::full(Cause::Overdue()), - ); - } - - const STEP: Percent = Percent::from_permille(10); - - fn liability_with_first(warn: Percent) -> Liability { - liability_with_max(warn + STEP + STEP + STEP) - } - - fn liability_with_second(warn: Percent) -> Liability { - liability_with_max(warn + STEP + STEP) - } - - fn liability_with_third(warn: Percent) -> Liability { - liability_with_max(warn + STEP) - } - - // init = 1%, healthy = 1%, first = max - 3, second = max - 2, third = max - 1 - fn liability_with_max(max: Percent) -> Liability { - let initial = STEP; - assert!(initial < max - STEP - STEP - STEP); - - Liability::new( - initial, - Percent::ZERO, - max - initial, - STEP, - STEP, - STEP, - Duration::from_hours(1), - ) - } -} - -#[cfg(test)] -mod test_status { - use currency::lease::Cro; - use finance::{liability::Zone, percent::Percent}; - - use crate::lease::{liquidation::Liquidation, Cause}; - - use super::Status; - - #[test] - fn ord() { - assert!( - Status::::No(Zone::no_warnings(Percent::from_permille(1))) - < Status::No(Zone::first( - Percent::from_permille(1), - Percent::from_permille(2) - )) - ); - assert!( - Status::::No(Zone::first( - Percent::from_permille(1), - Percent::from_permille(2) - )) < Status::No(Zone::second( - Percent::from_permille(1), - Percent::from_permille(2) - )) - ); - assert!( - Status::::No(Zone::first( - Percent::from_permille(1), - Percent::from_permille(2) - )) < Status::No(Zone::first( - Percent::from_permille(1), - Percent::from_permille(3) - )) - ); - assert!( - Status::No(Zone::first( - Percent::from_permille(2), - Percent::from_permille(3) - )) < Status::::No(Zone::second( - Percent::from_permille(1), - Percent::from_permille(2) - )) - ); - assert!( - Status::No(Zone::third( - Percent::from_permille(991), - Percent::from_permille(1000) - )) < Status::::Liquidation(Liquidation::Partial { - amount: 1.into(), - cause: Cause::Overdue() - }) - ); - assert!( - Status::::Liquidation(Liquidation::Partial { - amount: 1.into(), - cause: Cause::Overdue() - }) < Status::::Liquidation(Liquidation::Partial { - amount: 1.into(), - cause: Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - } - }) - ); - assert!( - Status::::Liquidation(Liquidation::Partial { - amount: 1.into(), - cause: Cause::Overdue() - }) < Status::::Liquidation(Liquidation::Partial { - amount: 2.into(), - cause: Cause::Overdue() - }) - ); - assert!( - Status::::Liquidation(Liquidation::Partial { - amount: 1.into(), - cause: Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - } - }) < Status::::Liquidation(Liquidation::Partial { - amount: 2.into(), - cause: Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - } - }) - ); - assert!( - Status::::partial( - 1.into(), - Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - } - ) < Status::::partial( - 1.into(), - Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(2) - } - ) - ); - assert!( - Status::::partial( - 1.into(), - Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - } - ) < Status::::full(Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(2) - }) - ); - assert!( - Status::::full(Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - }) < Status::::full(Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(2) - }) - ); - assert!( - Status::::full(Cause::Overdue()) - < Status::::full(Cause::Liability { - ltv: Percent::from_permille(1), - healthy_ltv: Percent::from_permille(1) - }) - ); - } -} diff --git a/contracts/lease/src/lease/mod.rs b/contracts/lease/src/lease/mod.rs index 88a85215b..ac629b77f 100644 --- a/contracts/lease/src/lease/mod.rs +++ b/contracts/lease/src/lease/mod.rs @@ -17,12 +17,7 @@ use crate::{ loan::Loan, }; -pub(super) use self::{ - dto::LeaseDTO, - liquidation::{Cause, Liquidation, Status}, - paid::Lease as LeasePaid, - state::State, -}; +pub(super) use self::{dto::LeaseDTO, paid::Lease as LeasePaid, state::State}; mod alarm; mod dto; diff --git a/packages/finance/src/liability/liquidation.rs b/packages/finance/src/liability/liquidation.rs new file mode 100644 index 000000000..561b784e8 --- /dev/null +++ b/packages/finance/src/liability/liquidation.rs @@ -0,0 +1,615 @@ +use serde::{Deserialize, Serialize}; + +use crate::{coin::Coin, currency::Currency, percent::Percent}; + +use super::{Liability, Zone}; + +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +#[cfg_attr(test, derive(Debug))] +pub enum Cause { + Overdue(), + Liability { ltv: Percent, healthy_ltv: Percent }, +} + +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(test, derive(Debug))] +pub enum Status +where + Asset: Currency, +{ + NoDebt, + No(Zone), + Liquidation(Liquidation), +} + +impl Status +where + Asset: Currency, +{ + fn partial(amount: Coin, cause: Cause) -> Self { + Self::Liquidation(Liquidation::Partial { amount, cause }) + } + + fn full(cause: Cause) -> Self { + Self::Liquidation(Liquidation::Full(cause)) + } + + #[cfg(debug_assertion)] + fn amount( + &self, + lease: &Lease, + ) -> Coin { + match self { + Self::No(_) => Default::default(), + Self::Liquidation(liq) => liq.amount(lease), + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(test, derive(Debug))] +pub enum Liquidation +where + Asset: Currency, +{ + Partial { amount: Coin, cause: Cause }, + Full(Cause), +} + +impl Liquidation +where + Asset: Currency, +{ + #[cfg(debug_assertion)] + pub(crate) fn amount( + &self, + lease: &Lease, + ) -> Coin { + match self { + Self::Partial { amount, cause: _ } => *amount, + Self::Full(_) => lease.amount, + } + } +} + +pub fn check_liability( + spec: &Liability, + asset: Coin, + total_due: Coin, + overdue: Coin, + liquidation_threshold: Coin, +) -> Status +where + Asset: Currency, +{ + may_ask_liquidation_liability(spec, asset, total_due, liquidation_threshold) + .max(may_ask_liquidation_overdue( + asset, + overdue, + liquidation_threshold, + )) + .unwrap_or_else(|| no_liquidation(spec, asset, total_due)) +} + +fn no_liquidation( + spec: &Liability, + asset: Coin, + total_due: Coin, +) -> Status +where + Asset: Currency, +{ + if total_due.is_zero() { + Status::NoDebt + } else { + let ltv = Percent::from_ratio(total_due, asset); + debug_assert!(ltv < spec.max()); + + Status::No(spec.zone_of(ltv)) + } +} + +fn may_ask_liquidation_liability( + spec: &Liability, + asset: Coin, + total_due: Coin, + liquidation_threshold: Coin, +) -> Option> +where + Asset: Currency, +{ + may_ask_liquidation( + asset, + Cause::Liability { + ltv: spec.max(), + healthy_ltv: spec.healthy_percent(), + }, + spec.amount_to_liquidate(asset, total_due), + liquidation_threshold, + ) +} + +fn may_ask_liquidation_overdue( + asset: Coin, + overdue: Coin, + liquidation_threshold: Coin, +) -> Option> +where + Asset: Currency, +{ + may_ask_liquidation(asset, Cause::Overdue(), overdue, liquidation_threshold) +} + +fn may_ask_liquidation( + asset: Coin, + cause: Cause, + liquidation: Coin, + liquidation_threshold: Coin, +) -> Option> +where + Asset: Currency, +{ + if liquidation.is_zero() { + None + } else if asset.saturating_sub(liquidation) <= liquidation_threshold { + Some(Status::full(cause)) + } else { + Some(Status::partial(liquidation, cause)) + } +} + +#[cfg(test)] +mod tests { + use crate::{coin::Amount, duration::Duration, percent::Percent, test::currency::Nls}; + + use super::{check_liability, Cause, Liability, Status, Zone}; + + #[test] + fn no_debt() { + let warn_ltv = Percent::from_permille(11); + let spec = liability_with_first(warn_ltv); + assert_eq!( + check_liability::(&spec, 100.into(), 0.into(), 0.into(), 0.into()), + Status::NoDebt, + ); + } + + #[test] + fn warnings_none() { + let warn_ltv = Percent::from_percent(51); + let spec = liability_with_first(warn_ltv); + assert_eq!( + check_liability::(&spec, 100.into(), 1.into(), 0.into(), 0.into()), + Status::No(Zone::no_warnings(spec.first_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 100.into(), 49.into(), 0.into(), 0.into()), + Status::No(Zone::no_warnings(spec.first_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 100.into(), 50.into(), 0.into(), 0.into()), + Status::No(Zone::no_warnings(spec.first_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 505.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 509.into(), 0.into(), 0.into()), + Status::No(Zone::no_warnings(spec.first_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 510.into(), 0.into(), 0.into()), + Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 510.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()), + ); + } + + #[test] + fn warnings_first() { + let spec = liability_with_first(Percent::from_permille(712)); + + assert_eq!( + check_liability::(&spec, 1000.into(), 711.into(), 0.into(), 0.into()), + Status::No(Zone::no_warnings(spec.first_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 712.into(), 0.into(), 0.into()), + Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 712.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()) + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 715.into(), 0.into(), 0.into()), + Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 721.into(), 0.into(), 0.into()), + Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 722.into(), 0.into(), 0.into()), + Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), + ); + } + + #[test] + fn warnings_second() { + let spec = liability_with_second(Percent::from_permille(123)); + + assert_eq!( + check_liability::(&spec, 1000.into(), 122.into(), 0.into(), 0.into()), + Status::No(Zone::first(spec.first_liq_warn(), spec.second_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 123.into(), 0.into(), 0.into()), + Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 124.into(), 0.into(), 0.into()), + Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 128.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()) + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 132.into(), 0.into(), 0.into()), + Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 133.into(), 0.into(), 0.into()), + Status::No(Zone::third(spec.third_liq_warn(), spec.max())), + ); + } + + #[test] + fn warnings_third() { + let warn_third_ltv = Percent::from_permille(381); + let max_ltv = warn_third_ltv + STEP; + let spec = liability_with_third(warn_third_ltv); + + assert_eq!( + check_liability::(&spec, 1000.into(), 380.into(), 0.into(), 0.into()), + Status::No(Zone::second(spec.second_liq_warn(), spec.third_liq_warn())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 381.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()) + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 381.into(), 0.into(), 0.into()), + Status::No(Zone::third(spec.third_liq_warn(), spec.max())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 382.into(), 0.into(), 0.into()), + Status::No(Zone::third(spec.third_liq_warn(), spec.max())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 390.into(), 0.into(), 0.into()), + Status::No(Zone::third(spec.third_liq_warn(), spec.max())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 391.into(), 0.into(), 0.into()), + Status::partial( + 384.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + } + + #[test] + fn liquidate_partial() { + let max_ltv = Percent::from_permille(881); + let spec = liability_with_max(max_ltv); + + assert_eq!( + check_liability::(&spec, 1000.into(), 880.into(), 0.into(), 0.into()), + Status::No(Zone::third(spec.third_liq_warn(), spec.max())), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 880.into(), 1.into(), 0.into()), + Status::partial(1.into(), Cause::Overdue()), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 881.into(), 0.into(), 0.into()), + Status::partial( + 879.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 881.into(), 878.into(), 0.into()), + Status::partial( + 879.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 881.into(), 879.into(), 0.into()), + Status::partial( + 879.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 881.into(), 880.into(), 0.into()), + Status::partial(880.into(), Cause::Overdue()), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 999.into(), 997.into(), 0.into()), + Status::partial( + 998.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 1000.into(), 1.into(), 0.into()), + Status::full(Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + }), + ); + } + + #[test] + fn liquidate_full() { + let max_ltv = Percent::from_permille(768); + let spec = liability_with_max(max_ltv); + + assert_eq!( + check_liability::(&spec, 1000.into(), 768.into(), 765.into(), 0.into()), + Status::partial( + 765.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 768.into(), 766.into(), 0.into()), + Status::partial(766.into(), Cause::Overdue()), + ); + assert_eq!( + check_liability::(&spec, 1000.into(), 1000.into(), 1.into(), 0.into()), + Status::full(Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + }), + ); + let back_to_healthy: Amount = spec.amount_to_liquidate(1000, 900); + assert_eq!( + check_liability::( + &spec, + 1000.into(), + 900.into(), + back_to_healthy.into(), + (1000 - back_to_healthy - 1).into() + ), + Status::partial( + back_to_healthy.into(), + Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + } + ), + ); + assert_eq!( + check_liability::( + &spec, + 1000.into(), + 900.into(), + (back_to_healthy + 1).into(), + (1000 - back_to_healthy - 2).into() + ), + Status::partial((back_to_healthy + 1).into(), Cause::Overdue()), + ); + assert_eq!( + check_liability::( + &spec, + 1000.into(), + 900.into(), + back_to_healthy.into(), + (1000 - back_to_healthy).into() + ), + Status::full(Cause::Liability { + ltv: max_ltv, + healthy_ltv: STEP + }), + ); + assert_eq!( + check_liability::( + &spec, + 1000.into(), + 900.into(), + (back_to_healthy + 1).into(), + (1000 - back_to_healthy - 1).into() + ), + Status::full(Cause::Overdue()), + ); + } + + const STEP: Percent = Percent::from_permille(10); + + fn liability_with_first(warn: Percent) -> Liability { + liability_with_max(warn + STEP + STEP + STEP) + } + + fn liability_with_second(warn: Percent) -> Liability { + liability_with_max(warn + STEP + STEP) + } + + fn liability_with_third(warn: Percent) -> Liability { + liability_with_max(warn + STEP) + } + + // init = 1%, healthy = 1%, first = max - 3, second = max - 2, third = max - 1 + fn liability_with_max(max: Percent) -> Liability { + let initial = STEP; + assert!(initial < max - STEP - STEP - STEP); + + Liability::new( + initial, + Percent::ZERO, + max - initial, + STEP, + STEP, + STEP, + Duration::from_hours(1), + ) + } +} + +#[cfg(test)] +mod test_status { + use crate::{percent::Percent, test::currency::Usdc}; + + use super::{Cause, Liquidation, Status, Zone}; + + #[test] + fn ord() { + assert!( + Status::::No(Zone::no_warnings(Percent::from_permille(1))) + < Status::No(Zone::first( + Percent::from_permille(1), + Percent::from_permille(2) + )) + ); + assert!( + Status::::No(Zone::first( + Percent::from_permille(1), + Percent::from_permille(2) + )) < Status::No(Zone::second( + Percent::from_permille(1), + Percent::from_permille(2) + )) + ); + assert!( + Status::::No(Zone::first( + Percent::from_permille(1), + Percent::from_permille(2) + )) < Status::No(Zone::first( + Percent::from_permille(1), + Percent::from_permille(3) + )) + ); + assert!( + Status::No(Zone::first( + Percent::from_permille(2), + Percent::from_permille(3) + )) < Status::::No(Zone::second( + Percent::from_permille(1), + Percent::from_permille(2) + )) + ); + assert!( + Status::No(Zone::third( + Percent::from_permille(991), + Percent::from_permille(1000) + )) < Status::::Liquidation(Liquidation::Partial { + amount: 1.into(), + cause: Cause::Overdue() + }) + ); + assert!( + Status::::Liquidation(Liquidation::Partial { + amount: 1.into(), + cause: Cause::Overdue() + }) < Status::::Liquidation(Liquidation::Partial { + amount: 1.into(), + cause: Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + } + }) + ); + assert!( + Status::::Liquidation(Liquidation::Partial { + amount: 1.into(), + cause: Cause::Overdue() + }) < Status::::Liquidation(Liquidation::Partial { + amount: 2.into(), + cause: Cause::Overdue() + }) + ); + assert!( + Status::::Liquidation(Liquidation::Partial { + amount: 1.into(), + cause: Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + } + }) < Status::::Liquidation(Liquidation::Partial { + amount: 2.into(), + cause: Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + } + }) + ); + assert!( + Status::::partial( + 1.into(), + Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + } + ) < Status::::partial( + 1.into(), + Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(2) + } + ) + ); + assert!( + Status::::partial( + 1.into(), + Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + } + ) < Status::::full(Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(2) + }) + ); + assert!( + Status::::full(Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + }) < Status::::full(Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(2) + }) + ); + assert!( + Status::::full(Cause::Overdue()) + < Status::::full(Cause::Liability { + ltv: Percent::from_permille(1), + healthy_ltv: Percent::from_permille(1) + }) + ); + } +} diff --git a/packages/finance/src/liability/mod.rs b/packages/finance/src/liability/mod.rs index 676b513f9..2418e10fc 100644 --- a/packages/finance/src/liability/mod.rs +++ b/packages/finance/src/liability/mod.rs @@ -15,9 +15,14 @@ use crate::{ }; pub use self::level::Level; +pub use self::liquidation::check_liability; +pub use self::liquidation::Cause; +pub use self::liquidation::Liquidation; +pub use self::liquidation::Status; pub use self::zone::Zone; mod level; +mod liquidation; mod unchecked; mod zone;