From a8c5bfb0deb5e00ad47e62f3f80dfa0c8e166b2c Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Wed, 19 Jan 2022 13:21:41 +0200 Subject: [PATCH] CU-1wty1zv fixed lending (#484) * oracle api clarification Signed-off-by: Dzmitry Lahoda * fixing pr comments Signed-off-by: Dzmitry Lahoda * fixed comment Signed-off-by: Dzmitry Lahoda * crazy fmt issue Signed-off-by: Dzmitry Lahoda * just something to tirgget build after fail Signed-off-by: Dzmitry Lahoda * fixed lending Signed-off-by: Dzmitry Lahoda * fixed price, added ratio test Signed-off-by: Dzmitry Lahoda * fixed comments of review Signed-off-by: Dzmitry Lahoda * fixed comments Signed-off-by: Dzmitry Lahoda --- .config/cargo_spellcheck.dic | 2 +- frame/assets/src/benchmarking.rs | 14 +- frame/composable-traits/src/auction.rs | 35 +- frame/composable-traits/src/currency.rs | 16 +- frame/composable-traits/src/defi.rs | 91 +++- frame/composable-traits/src/lending/math.rs | 47 +- frame/composable-traits/src/lending/mod.rs | 87 ++- frame/composable-traits/src/lending/tests.rs | 40 +- frame/composable-traits/src/lib.rs | 2 +- frame/composable-traits/src/liquidation.rs | 23 +- frame/composable-traits/src/loans.rs | 25 - frame/composable-traits/src/math.rs | 8 +- frame/composable-traits/src/oracle.rs | 9 +- frame/composable-traits/src/time.rs | 44 ++ frame/curve-amm/src/lib.rs | 3 +- frame/dutch-auction/src/benchmarking.rs | 45 +- frame/dutch-auction/src/lib.rs | 116 ++-- frame/dutch-auction/src/math.rs | 23 +- frame/dutch-auction/src/mock/currency.rs | 15 +- frame/dutch-auction/src/mock/mod.rs | 2 +- frame/dutch-auction/src/mock/runtime.rs | 18 +- frame/dutch-auction/src/tests.rs | 52 +- frame/lending/Cargo.toml | 4 +- frame/lending/accrue_interest.png | Bin 36565 -> 0 bytes frame/lending/proptest-regressions/tests.txt | 10 + frame/lending/src/benchmarking.rs | 12 +- frame/lending/src/lib.rs | 545 +++++++++---------- frame/lending/src/mocks/mod.rs | 28 +- frame/lending/src/models.rs | 24 +- frame/lending/src/tests.rs | 162 +++--- frame/liquidations/src/lib.rs | 186 ++++++- frame/liquidations/src/weights.rs | 9 + frame/oracle/src/lib.rs | 1 + frame/uniswap-v2/src/lib.rs | 3 +- 34 files changed, 961 insertions(+), 740 deletions(-) delete mode 100644 frame/composable-traits/src/loans.rs create mode 100644 frame/composable-traits/src/time.rs delete mode 100644 frame/lending/accrue_interest.png create mode 100644 frame/lending/proptest-regressions/tests.txt create mode 100644 frame/liquidations/src/weights.rs diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index f7d0d43bd2e..926051b4975 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -22,4 +22,4 @@ tombstoned u128 Wasm Xcm -XCM \ No newline at end of file +Dispatchable \ No newline at end of file diff --git a/frame/assets/src/benchmarking.rs b/frame/assets/src/benchmarking.rs index f26d7a59b76..1a4167aa136 100644 --- a/frame/assets/src/benchmarking.rs +++ b/frame/assets/src/benchmarking.rs @@ -34,14 +34,14 @@ benchmarks! { let asset_id: T::AssetId = ASSET_ID.into(); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap(); + T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test"); }: _(RawOrigin::Signed(caller), asset_id, dest, amount, true) transfer_native { let caller: T::AccountId = whitelisted_caller(); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::NativeCurrency::mint_into(&caller, amount).unwrap(); + T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test"); }: _(RawOrigin::Signed(caller), dest, amount, false) force_transfer { @@ -50,7 +50,7 @@ benchmarks! { let from = T::Lookup::unlookup(FROM_ACCOUNT.into()); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap(); + T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test"); }: _(RawOrigin::Root, asset_id, from, dest, amount, false) force_transfer_native { @@ -58,7 +58,7 @@ benchmarks! { let from = T::Lookup::unlookup(FROM_ACCOUNT.into()); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::NativeCurrency::mint_into(&caller, amount).unwrap(); + T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test"); }: _(RawOrigin::Root, from, dest, amount, false) transfer_all { @@ -66,14 +66,14 @@ benchmarks! { let asset_id: T::AssetId = ASSET_ID.into(); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap(); + T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test"); }: _(RawOrigin::Signed(caller), asset_id, dest, false) transfer_all_native { let caller: T::AccountId = whitelisted_caller(); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::NativeCurrency::mint_into(&caller, amount).unwrap(); + T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test"); }: _(RawOrigin::Signed(caller), dest, false) mint_initialize { @@ -98,7 +98,7 @@ benchmarks! { let asset_id: T::AssetId = ASSET_ID.into(); let dest = T::Lookup::unlookup(TO_ACCOUNT.into()); let amount: T::Balance = TRANSFER_AMOUNT.into(); - T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap(); + T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test"); }: _(RawOrigin::Root, asset_id, dest, amount) } diff --git a/frame/composable-traits/src/auction.rs b/frame/composable-traits/src/auction.rs index 69957f7f0d9..297a2673ef1 100644 --- a/frame/composable-traits/src/auction.rs +++ b/frame/composable-traits/src/auction.rs @@ -1,33 +1,2 @@ -use crate::loans::DurationSeconds; -use frame_support::pallet_prelude::*; -use scale_info::TypeInfo; -use sp_runtime::Permill; - -#[derive(Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] -pub enum AuctionStepFunction { - /// default - direct pass through to dex without steps, just to satisfy defaults and reasonably - /// for testing - LinearDecrease(LinearDecrease), - StairstepExponentialDecrease(StairstepExponentialDecrease), -} - -impl Default for AuctionStepFunction { - fn default() -> Self { - Self::LinearDecrease(Default::default()) - } -} - -#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] -pub struct LinearDecrease { - /// Seconds after auction start when the price reaches zero - pub total: DurationSeconds, -} - -#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] -pub struct StairstepExponentialDecrease { - // Length of time between price drops - pub step: DurationSeconds, - // Per-step multiplicative factor, usually more than 50%, mostly closer to 100%, but not 100%. - // Drop per unit of `step`. - pub cut: Permill, -} +// TODO: how type alias to such generics can be achieved? +// pub type DutchAuction = SellEngine; diff --git a/frame/composable-traits/src/currency.rs b/frame/composable-traits/src/currency.rs index 10b7d95f376..1d7a6c3a6f5 100644 --- a/frame/composable-traits/src/currency.rs +++ b/frame/composable-traits/src/currency.rs @@ -1,9 +1,11 @@ use codec::FullCodec; use frame_support::pallet_prelude::*; use scale_info::TypeInfo; -use sp_runtime::traits::AtLeast32BitUnsigned; +use sp_runtime::traits::{AtLeast32BitUnsigned, Zero}; use sp_std::fmt::Debug; +use crate::math::SafeArithmetic; + /// really u8, but easy to do math operations pub type Exponent = u32; @@ -77,6 +79,18 @@ impl< { } +/// limited counted number trait which maximal number is more than `u64`, but not more than `u128`, +/// so inner type is either u64 or u128 with helpers for producing `ArithmeticError`s instead of +/// `Option`s. +pub trait MathBalance: + PartialOrd + Zero + SafeArithmetic + Into + TryFrom + From + Copy +{ +} +impl + TryFrom + From + Copy> + MathBalance for T +{ +} + // hack to imitate type alias until it is in stable // named with like implying it is`like` is is necessary to be `AssetId`, but may be not enough (if // something is `AssetIdLike` than it is not always asset) diff --git a/frame/composable-traits/src/defi.rs b/frame/composable-traits/src/defi.rs index 433855bc9bc..e713cf534cb 100644 --- a/frame/composable-traits/src/defi.rs +++ b/frame/composable-traits/src/defi.rs @@ -1,37 +1,42 @@ -//! Common codes for defi pallets - +//! Common codes and conventions for DeFi pallets use codec::{Codec, Decode, Encode, FullCodec}; use frame_support::{pallet_prelude::MaybeSerializeDeserialize, Parameter}; use scale_info::TypeInfo; use sp_runtime::{ + helpers_128bit::multiply_by_rational, traits::{CheckedAdd, CheckedMul, CheckedSub, Zero}, - ArithmeticError, DispatchError, FixedPointOperand, FixedU128, + ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128, }; -use crate::{ - currency::{AssetIdLike, BalanceLike}, - math::{LiftedFixedBalance, SafeArithmetic}, -}; +use crate::currency::{AssetIdLike, BalanceLike, MathBalance}; #[derive(Encode, Decode, TypeInfo, Debug, Clone, PartialEq)] pub struct Take { /// amount of `base` pub amount: Balance, /// direction depends on referenced order type - /// either minimal or maximal amount of `quote` for given unit of `base` - pub limit: Balance, + /// either minimal or maximal amount of `quote` for given `base` + /// depending on engine configuration, `limit` can be hard or flexible (change with time) + pub limit: LiftedFixedBalance, } -impl Take { +impl Take { pub fn is_valid(&self) -> bool { - self.amount > Balance::zero() && self.limit > Balance::zero() + self.amount > Balance::zero() && self.limit > Ratio::zero() } - pub fn new(amount: Balance, limit: Balance) -> Self { + + pub fn new(amount: Balance, limit: Ratio) -> Self { Self { amount, limit } } - pub fn quote_amount(&self) -> Result { - self.amount.safe_mul(&self.limit) + pub fn quote_limit_amount(&self) -> Result { + self.quote_amount(self.amount) + } + + pub fn quote_amount(&self, amount: Balance) -> Result { + let result = multiply_by_rational(amount.into(), self.limit.into_inner(), Ratio::DIV) + .map_err(|_| ArithmeticError::Overflow)?; + result.try_into().map_err(|_| ArithmeticError::Overflow) } } @@ -42,7 +47,7 @@ pub struct Sell { pub take: Take, } -impl Sell { +impl Sell { pub fn is_valid(&self) -> bool { self.take.is_valid() } @@ -50,7 +55,7 @@ impl Sell Self { Self { take: Take { amount: base_amount, limit: minimal_base_unit_price_in_quote }, @@ -70,9 +75,11 @@ impl Sell { /// See [Base Currency](https://www.investopedia.com/terms/b/basecurrency.asp). /// Also can be named `native`(to the market) currency. + /// Usually less stable, can be used as collateral. pub base: AssetId, /// Counter currency. /// Also can be named `price` currency. + /// Usually more stable, may be `borrowable` asset. pub quote: AssetId, } @@ -90,9 +97,10 @@ impl CurrencyPair { /// assert_eq!(slice[0], pair.base); /// assert_eq!(slice[1], pair.quote); /// ``` - /// ```compile_fail + /// ```rust /// # let pair = composable_traits::defi::CurrencyPair::::new(13, 42); /// # let slice = pair.as_slice(); + /// // it is copy /// drop(pair); /// let _ = slice[0]; /// ``` @@ -185,6 +193,7 @@ pub trait DeFiComposableConfig: frame_system::Config { type MayBeAssetId: AssetIdLike + MaybeSerializeDeserialize + Default; type Balance: BalanceLike + + MathBalance + Default + Parameter + Codec @@ -197,12 +206,52 @@ pub trait DeFiComposableConfig: frame_system::Config { + From // at least 64 bit + Zero + FixedPointOperand - + Into // integer part not more than bits in this + Into; // cannot do From, until LiftedFixedBalance integer part is larger than 128 // bit } -/// The fixed point number from 0..to max. -/// Unlike `Ratio` it can be more than 1. -/// And unlike `NormalizedCollateralFactor`, it can be less than one. +/// The fixed point number from 0..to max pub type Rate = FixedU128; + +/// Is [1..MAX] +pub type OneOrMoreFixedU128 = FixedU128; + +/// The fixed point number of suggested by substrate precision +/// Must be (1.0..MAX] because applied only to price normalized values +pub type MoreThanOneFixedU128 = FixedU128; + +/// Must be [0..1] +pub type ZeroToOneFixedU128 = FixedU128; + +/// Number like of higher bits, so that amount and balance calculations are done it it with higher +/// precision via fixed point. +/// While this is 128 bit, cannot support u128 because 18 bits are for of mantissa (so maximal +/// integer is 110 bit). Can support u128 if lift upper to use FixedU256 analog. +pub type LiftedFixedBalance = FixedU128; + +/// unitless ratio of one thing to other. +pub type Ratio = FixedU128; + +#[cfg(test)] +mod tests { + use super::{Ratio, Take}; + use sp_runtime::FixedPointNumber; + + #[test] + fn take_ratio_half() { + let price = 10; + let amount = 100_u128; + let take = Take::new(amount, Ratio::saturating_from_integer(price)); + let result = take.quote_amount(amount / 2).unwrap(); + assert_eq!(result, price * amount / 2); + } + + #[test] + fn take_ratio_half_amount_half_price() { + let price_part = 50; + let amount = 100_u128; + let take = Take::new(amount, Ratio::saturating_from_rational(price_part, 100)); + let result = take.quote_amount(amount).unwrap(); + assert_eq!(result, price_part * amount / 100); + } +} diff --git a/frame/composable-traits/src/lending/math.rs b/frame/composable-traits/src/lending/math.rs index 66b6c3af81b..f5a46168fe1 100644 --- a/frame/composable-traits/src/lending/math.rs +++ b/frame/composable-traits/src/lending/math.rs @@ -12,23 +12,11 @@ use sp_runtime::{ use sp_arithmetic::per_things::Percent; use crate::{ - defi::Rate, - loans::{DurationSeconds, ONE_HOUR}, - math::{LiftedFixedBalance, SafeArithmetic}, + defi::{LiftedFixedBalance, Rate, ZeroToOneFixedU128}, + math::SafeArithmetic, + time::{DurationSeconds, SECONDS_PER_YEAR_NAIVE}, }; -/// The fixed point number of suggested by substrate precision -/// Must be (1.0.. because applied only to price normalized values -pub type NormalizedCollateralFactor = FixedU128; - -/// Must be [0..1] -/// TODO: implement Ratio as wrapper over FixedU128 -pub type Ratio = FixedU128; - -/// current notion of year will take away 1/365 from lenders and give away to borrowers (as does no -/// accounts to length of year) -pub const SECONDS_PER_YEAR: DurationSeconds = 365 * 24 * ONE_HOUR; - /// utilization_ratio = total_borrows / (total_cash + total_borrows) pub fn calc_utilization_ratio( cash: LiftedFixedBalance, @@ -110,9 +98,13 @@ impl InterestRateModel { } /// Calculates the current supply interest rate - pub fn get_supply_rate(borrow_rate: Rate, util: Ratio, reserve_factor: Ratio) -> Rate { + pub fn get_supply_rate( + borrow_rate: Rate, + util: ZeroToOneFixedU128, + reserve_factor: ZeroToOneFixedU128, + ) -> Rate { // ((1 - reserve_factor) * borrow_rate) * utilization - let one_minus_reserve_factor = Ratio::one().saturating_sub(reserve_factor); + let one_minus_reserve_factor = ZeroToOneFixedU128::one().saturating_sub(reserve_factor); let rate_to_pool = borrow_rate.saturating_mul(one_minus_reserve_factor); rate_to_pool.saturating_mul(util) @@ -151,15 +143,18 @@ pub struct JumpModel { } impl JumpModel { - pub const MAX_BASE_RATE: Ratio = Ratio::from_inner(100_000_000_000_000_000); // 10% - pub const MAX_JUMP_RATE: Ratio = Ratio::from_inner(300_000_000_000_000_000); // 30% - pub const MAX_FULL_RATE: Ratio = Ratio::from_inner(500_000_000_000_000_000); // 50% + pub const MAX_BASE_RATE: ZeroToOneFixedU128 = + ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 10 / 100); + pub const MAX_JUMP_RATE: ZeroToOneFixedU128 = + ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 30 / 100); + pub const MAX_FULL_RATE: ZeroToOneFixedU128 = + ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 50 / 100); /// Create a new rate model pub fn new( - base_rate: Ratio, - jump_rate: Ratio, - full_rate: Ratio, + base_rate: ZeroToOneFixedU128, + jump_rate: ZeroToOneFixedU128, + full_rate: ZeroToOneFixedU128, target_utilization: Percent, ) -> Option { let model = Self { base_rate, jump_rate, full_rate, target_utilization }; @@ -390,7 +385,7 @@ pub fn accrued_interest( borrow_rate .checked_mul_int(amount)? .checked_mul(delta_time.into())? - .checked_div(SECONDS_PER_YEAR.into()) + .checked_div(SECONDS_PER_YEAR_NAIVE.into()) } /// compounding increment of borrow index @@ -402,7 +397,7 @@ pub fn increment_index( borrow_rate .safe_mul(&index)? .safe_mul(&FixedU128::saturating_from_integer(delta_time))? - .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))? + .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))? .safe_add(&index) } @@ -412,5 +407,5 @@ pub fn increment_borrow_rate( ) -> Result { borrow_rate .safe_mul(&FixedU128::saturating_from_integer(delta_time))? - .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR)) + .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE)) } diff --git a/frame/composable-traits/src/lending/mod.rs b/frame/composable-traits/src/lending/mod.rs index b0261d83df0..98e74e00518 100644 --- a/frame/composable-traits/src/lending/mod.rs +++ b/frame/composable-traits/src/lending/mod.rs @@ -3,36 +3,66 @@ pub mod math; #[cfg(test)] mod tests; -use crate::loans::Timestamp; +use crate::{ + defi::{CurrencyPair, DeFiEngine, MoreThanOneFixedU128}, + time::Timestamp, +}; use frame_support::{pallet_prelude::*, sp_runtime::Perquintill, sp_std::vec::Vec}; use scale_info::TypeInfo; use sp_runtime::Percent; use self::math::*; -pub type CollateralLpAmountOf = ::Balance; +pub type CollateralLpAmountOf = ::Balance; -pub type BorrowAmountOf = ::Balance; +pub type BorrowAmountOf = ::Balance; -#[derive(Encode, Decode, Default, TypeInfo)] -pub struct MarketConfigInput { - pub reserved: Perquintill, - pub manager: AccountId, - /// can pause borrow & deposits of assets - pub collateral_factor: NormalizedCollateralFactor, +#[derive(Encode, Decode, Default, TypeInfo, Debug, Clone, PartialEq)] +pub struct UpdateInput { + /// Collateral factor of market + pub collateral_factor: MoreThanOneFixedU128, + /// warn borrower when loan's collateral/debt ratio + /// given percentage short to be under collaterized pub under_collaterized_warn_percent: Percent, - pub liquidator: Option, + /// liquidation engine id + pub liquidators: Vec, + pub interest_rate_model: InterestRateModel, +} + +/// input to create market extrinsic +#[derive(Encode, Decode, Default, TypeInfo, Debug, Clone, PartialEq)] +pub struct CreateInput { + /// the part of market which can be changed + pub updatable: UpdateInput, + /// collateral currency and borrow currency + /// in case of liquidation, collateral is base and borrow is quote + pub currency_pair: CurrencyPair, + /// Reserve factor of market borrow vault. + pub reserved_factor: Perquintill, +} + +impl CreateInput { + pub fn borrow_asset(&self) -> AssetId { + self.currency_pair.quote + } + pub fn collateral_asset(&self) -> AssetId { + self.currency_pair.base + } + + pub fn reserved_factor(&self) -> Perquintill { + self.reserved_factor + } } #[derive(Encode, Decode, Default, TypeInfo)] -pub struct MarketConfig { +pub struct MarketConfig { pub manager: AccountId, pub borrow: VaultId, pub collateral: AssetId, - pub collateral_factor: NormalizedCollateralFactor, + pub collateral_factor: MoreThanOneFixedU128, pub interest_rate_model: InterestRateModel, pub under_collaterized_warn_percent: Percent, - pub liquidator: Option, + pub liquidators: Vec, } /// Basic lending with no its own wrapper (liquidity) token. @@ -41,16 +71,14 @@ pub struct MarketConfig { /// Based on Blacksmith (Warp v2) IBSLendingPair.sol and Parallel Finance. /// Fees will be withdrawing to vault. /// Lenders with be rewarded via vault. -pub trait Lending { - type AssetId; +pub trait Lending: DeFiEngine { type VaultId; type MarketId; - /// (deposit VaultId, collateral VaultId) <-> MarketId - type AccountId; - type Balance; type BlockNumber; - type GroupId; - + /// id of dispatch used to liquidate collateral in case of undercollateralized asset + type LiquidationStrategyId; + /// returned from extrinsic is guaranteed to be existing asset id at time of block execution + //type AssetId; /// Generates the underlying owned vault that will hold borrowable asset (may be shared with /// specific set of defined collaterals). Creates market for new pair in specified vault. if /// market exists under specified manager, updates its parameters `deposit` - asset users want @@ -88,11 +116,11 @@ pub trait Lending { /// could decide to allocate a share for it, transferring from I and J to the borrow asset vault /// of M. Their allocated share could differ because of the strategies being different, /// but the lending Market would have all the lendable funds in a single vault. + /// + /// Returned `MarketId` is mapped one to one with (deposit VaultId, collateral VaultId) fn create( - borrow_asset: Self::AssetId, - collateral_asset_vault: Self::AssetId, - config: MarketConfigInput, - interest_rate_model: &InterestRateModel, + manager: Self::AccountId, + config: CreateInput, ) -> Result<(Self::MarketId, Self::VaultId), DispatchError>; /// AccountId of the market instance @@ -123,7 +151,12 @@ pub trait Lending { #[allow(clippy::type_complexity)] fn get_all_markets() -> Vec<( Self::MarketId, - MarketConfig, + MarketConfig< + Self::VaultId, + Self::MayBeAssetId, + Self::AccountId, + Self::LiquidationStrategyId, + >, )>; /// `amount_to_borrow` is the amount of the borrow asset lendings's vault shares the user wants @@ -167,8 +200,8 @@ pub trait Lending { /// utilization_ratio = total_borrows / (total_cash + total_borrows). /// utilization ratio is 0 when there are no borrows. fn calc_utilization_ratio( - cash: &Self::Balance, - borrows: &Self::Balance, + cash: Self::Balance, + borrows: Self::Balance, ) -> Result; /// Borrow asset amount account should repay to be debt free for specific market pair. diff --git a/frame/composable-traits/src/lending/tests.rs b/frame/composable-traits/src/lending/tests.rs index d0a07379131..a3440ba0f91 100644 --- a/frame/composable-traits/src/lending/tests.rs +++ b/frame/composable-traits/src/lending/tests.rs @@ -1,4 +1,4 @@ -use crate::defi::Rate; +use crate::defi::{Rate, ZeroToOneFixedU128}; use super::*; use proptest::{prop_assert, strategy::Strategy, test_runner::TestRunner}; @@ -62,12 +62,12 @@ fn get_borrow_rate_works() { #[test] fn get_supply_rate_works() { let borrow_rate = Rate::saturating_from_rational(2, 100); - let util = Ratio::saturating_from_rational(50, 100); - let reserve_factor = Ratio::zero(); + let util = ZeroToOneFixedU128::saturating_from_rational(50, 100); + let reserve_factor = ZeroToOneFixedU128::zero(); let supply_rate = InterestRateModel::get_supply_rate(borrow_rate, util, reserve_factor); assert_eq!( supply_rate, - borrow_rate.saturating_mul(Ratio::one().saturating_sub(reserve_factor) * util), + borrow_rate.saturating_mul(ZeroToOneFixedU128::one().saturating_sub(reserve_factor) * util), ); } @@ -78,24 +78,24 @@ fn curve_model_correctly_calculates_borrow_rate() { model.get_borrow_rate(Percent::from_percent(80)).unwrap(), // curve model has arbitrary power parameters leading to changes in precision of high // power - Rate::from_inner(140000000000000000) + Rate::from_inner(Rate::DIV / 100 * 14) ); } #[derive(Debug, Clone)] struct JumpModelStrategy { - pub base_rate: Ratio, - pub jump_percentage: Ratio, - pub full_percentage: Ratio, + pub base_rate: ZeroToOneFixedU128, + pub jump_percentage: ZeroToOneFixedU128, + pub full_percentage: ZeroToOneFixedU128, pub target_utilization: Percent, } fn valid_jump_model() -> impl Strategy { ( - (1..=10u32).prop_map(|x| Ratio::saturating_from_rational(x, 100)), - (11..=30u32).prop_map(|x| Ratio::saturating_from_rational(x, 100)), - (31..=50).prop_map(|x| Ratio::saturating_from_rational(x, 100)), - (0..=100u8).prop_map(Percent::from_percent), + (1..=10_u32).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), + (11..=30_u32).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), + (31..=50).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), + (0..=100_u8).prop_map(Percent::from_percent), ) .prop_filter("Jump rate model", |(base, jump, full, _)| { // tried high order strategy - failed as it tries to combine collections with not @@ -115,9 +115,9 @@ fn valid_jump_model() -> impl Strategy { #[test] fn test_empty_drained_market() { let mut jump_model = JumpModel::new( - FixedU128::from_float(0.010000000000000000), - FixedU128::from_float(0.110000000000000000), - FixedU128::from_float(0.310000000000000000), + FixedU128::from_float(0.01), + FixedU128::from_float(0.11), + FixedU128::from_float(0.31), Percent::zero(), ) .unwrap(); @@ -131,9 +131,9 @@ fn test_empty_drained_market() { #[test] fn test_slope() { let mut jump_model = JumpModel::new( - FixedU128::from_float(0.010000000000000000), - FixedU128::from_float(0.110000000000000000), - FixedU128::from_float(0.310000000000000000), + FixedU128::from_float(0.01), + FixedU128::from_float(0.11), + FixedU128::from_float(0.31), Percent::from_percent(80), ) .unwrap(); @@ -159,7 +159,7 @@ fn test_slope() { fn proptest_jump_model() { let mut runner = TestRunner::default(); runner - .run(&(valid_jump_model(), 0..=100u8), |(strategy, utilization)| { + .run(&(valid_jump_model(), 0..=100_u8), |(strategy, utilization)| { let base_rate = strategy.base_rate; let jump_rate = strategy.jump_percentage; let full_rate = strategy.full_percentage; @@ -181,7 +181,7 @@ fn proptest_jump_model_rate() { let base_rate = Rate::saturating_from_rational(2, 100); let jump_rate = Rate::saturating_from_rational(10, 100); let full_rate = Rate::saturating_from_rational(32, 100); - let strategy = (0..=100u8, 1..=99u8) + let strategy = (0..=100_u8, 1..=99_u8) .prop_map(|(optimal, utilization)| (optimal, utilization, utilization + 1)); let mut runner = TestRunner::default(); diff --git a/frame/composable-traits/src/lib.rs b/frame/composable-traits/src/lib.rs index 348fe7fae4e..41ebac0938c 100644 --- a/frame/composable-traits/src/lib.rs +++ b/frame/composable-traits/src/lib.rs @@ -22,9 +22,9 @@ pub mod dex; pub mod governance; pub mod lending; pub mod liquidation; -pub mod loans; pub mod math; pub mod oracle; pub mod privilege; +pub mod time; pub mod vault; pub mod vesting; diff --git a/frame/composable-traits/src/liquidation.rs b/frame/composable-traits/src/liquidation.rs index 76f9c915aaf..e8747f2404c 100644 --- a/frame/composable-traits/src/liquidation.rs +++ b/frame/composable-traits/src/liquidation.rs @@ -1,24 +1,19 @@ use sp_runtime::DispatchError; -use crate::loans::PriceStructure; +use crate::defi::{DeFiEngine, Sell}; /// An object from which we can initiate liquidations from. /// Does not cares if liquidation was completed or not, neither can reasonably provide that /// information. Off-chain can join relevant ids if needed. -pub trait Liquidation { - type AssetId; - type Balance; - type AccountId; - type LiquidationId; - type GroupId; +/// `configuration` - optional list of liquidations strategies +pub trait Liquidation: DeFiEngine { + type OrderId; + type LiquidationStrategyId; /// Initiate a liquidation, this operation should be executed as fast as possible. fn liquidate( - source_account: &Self::AccountId, - source_asset_id: Self::AssetId, - source_asset_price: PriceStructure, - target_asset_id: Self::AssetId, - target_account: &Self::AccountId, - total_amount: Self::Balance, - ) -> Result; + from_to: &Self::AccountId, + order: Sell, + configuration: sp_std::vec::Vec, + ) -> Result; } diff --git a/frame/composable-traits/src/loans.rs b/frame/composable-traits/src/loans.rs deleted file mode 100644 index 3df3fd6c42d..00000000000 --- a/frame/composable-traits/src/loans.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! shared types across lending/liquidation/auctions pallets -use codec::{Decode, Encode}; - -use scale_info::TypeInfo; - -/// `std::time::Duration` is not used because it is to precise with 128 bits and microseconds. -pub type DurationSeconds = u64; - -/// seconds -pub type Timestamp = u64; - -pub const ONE_HOUR: DurationSeconds = 60 * 60; - -/// allows for price to favor some group within some period of time -#[derive(Debug, Decode, Encode, Default, TypeInfo)] -pub struct PriceStructure { - pub initial_price: Balance, - pub preference: Option<(GroupId, DurationSeconds)>, -} - -impl PriceStructure { - pub fn new(initial_price: Balance) -> Self { - Self { initial_price, preference: None } - } -} diff --git a/frame/composable-traits/src/math.rs b/frame/composable-traits/src/math.rs index cad07f9d568..37b8f20ba35 100644 --- a/frame/composable-traits/src/math.rs +++ b/frame/composable-traits/src/math.rs @@ -1,14 +1,8 @@ use sp_runtime::{ traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, One, Saturating, Zero}, - ArithmeticError, FixedU128, + ArithmeticError, }; -/// Number like of higher bits, so that amount and balance calculations are done it it with higher -/// precision via fixed point. -/// While this is 128 bit, cannot support u128 because 18 bits are for of mantissa (so maximal -/// integer is 110 bit). Can support u128 if lift upper to use FixedU256 analog. -pub type LiftedFixedBalance = FixedU128; - /// little bit slower than maximizing performance by knowing constraints. /// Example, you sum to negative numbers, can get underflow, so need to check on each add; but if /// you have positive number only, you cannot have underflow. Same for other constrains, like non diff --git a/frame/composable-traits/src/oracle.rs b/frame/composable-traits/src/oracle.rs index 56f7e8b548e..fb38d00cbd4 100644 --- a/frame/composable-traits/src/oracle.rs +++ b/frame/composable-traits/src/oracle.rs @@ -1,6 +1,8 @@ -use crate::{currency::LocalAssets, defi::CurrencyPair}; +use crate::{ + currency::LocalAssets, + defi::{CurrencyPair, Ratio}, +}; use frame_support::{dispatch::DispatchError, pallet_prelude::*}; -use sp_runtime::FixedU128; use sp_std::vec::Vec; #[derive(Encode, Decode, Default, Debug, PartialEq)] @@ -66,6 +68,7 @@ pub trait Oracle { weighting: Vec, ) -> Result; + /// How much of `quote` for unit `base` Oracle suggests to take. /// Up to oracle how it decides ratio. /// If there is no direct trading pair, can estimate via common pair (to which all currencies /// are normalized). General formula @@ -76,5 +79,5 @@ pub trait Oracle { /// let base_amount = 3.0; /// let needed_base_for_quote = base_amount * ratio; // 300.0 /// ``` - fn get_ratio(pair: CurrencyPair) -> Result; + fn get_ratio(pair: CurrencyPair) -> Result; } diff --git a/frame/composable-traits/src/time.rs b/frame/composable-traits/src/time.rs new file mode 100644 index 00000000000..a8648be8496 --- /dev/null +++ b/frame/composable-traits/src/time.rs @@ -0,0 +1,44 @@ +//! Naive time things + +use frame_support::pallet_prelude::*; +use scale_info::TypeInfo; +use sp_runtime::Permill; + +/// `std::time::Duration` is not used because it is to precise with 128 bits and microseconds. +pub type DurationSeconds = u64; + +/// Unix now seconds +pub type Timestamp = u64; + +pub const ONE_HOUR: DurationSeconds = 60 * 60; + +/// current notion of year will take away 1/365 from lenders and give away to borrowers (as does no +/// accounts to length of year) +pub const SECONDS_PER_YEAR_NAIVE: DurationSeconds = 365 * 24 * ONE_HOUR; + +#[derive(Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] +pub enum TimeReleaseFunction { + LinearDecrease(LinearDecrease), + StairstepExponentialDecrease(StairstepExponentialDecrease), +} + +impl Default for TimeReleaseFunction { + fn default() -> Self { + Self::LinearDecrease(Default::default()) + } +} + +#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] +pub struct LinearDecrease { + /// Seconds after start when the amount reaches zero + pub total: DurationSeconds, +} + +#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)] +pub struct StairstepExponentialDecrease { + // Length of time between drops + pub step: DurationSeconds, + // Per-step multiplicative factor, usually more than 50%, mostly closer to 100%, but not 100%. + // Drop per unit of `step`. + pub cut: Permill, +} diff --git a/frame/curve-amm/src/lib.rs b/frame/curve-amm/src/lib.rs index 710ef62c449..93ed1765017 100644 --- a/frame/curve-amm/src/lib.rs +++ b/frame/curve-amm/src/lib.rs @@ -46,9 +46,8 @@ pub mod pallet { use codec::{Codec, FullCodec}; use composable_traits::{ currency::CurrencyFactory, - defi::CurrencyPair, + defi::{CurrencyPair, LiftedFixedBalance}, dex::{CurveAmm, StableSwapPoolInfo}, - math::LiftedFixedBalance, }; use frame_support::{ pallet_prelude::*, diff --git a/frame/dutch-auction/src/benchmarking.rs b/frame/dutch-auction/src/benchmarking.rs index 386b412dc5f..f52943453ef 100644 --- a/frame/dutch-auction/src/benchmarking.rs +++ b/frame/dutch-auction/src/benchmarking.rs @@ -1,24 +1,25 @@ use super::*; -use crate::Pallet as DutchAuction; -use codec::Decode; -use composable_traits::defi::{CurrencyPair, DeFiComposableConfig, Sell, Take}; +use crate::{mock::currency::CurrencyId, Pallet as DutchAuction}; +use codec::{Decode, Encode}; +use composable_traits::defi::{CurrencyPair, DeFiComposableConfig, Ratio, Sell, Take}; use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, whitelisted_caller}; use frame_support::traits::{fungibles::Mutate, Hooks}; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; +use sp_runtime::FixedPointNumber; use sp_std::prelude::*; // meaningless sell of 1 to 1 pub fn sell_identity( ) -> Sell<::MayBeAssetId, ::Balance> { - let one: ::Balance = 1u64.into(); + let one: ::Balance = 1_u64.into(); let pair = assets::(); - Sell::new(pair.base, pair.quote, one, one) + Sell::new(pair.base, pair.quote, one, Ratio::saturating_from_integer(one)) } // meaningless take of 1 to 1 pub fn take_identity() -> Take<::Balance> { - let one: ::Balance = 1u64.into(); - Take::new(one, one) + let one: ::Balance = 1_u64.into(); + Take::new(one, Ratio::saturating_from_integer(one)) } pub type AssetIdOf = ::MayBeAssetId; @@ -27,8 +28,8 @@ fn assets() -> CurrencyPair> where T: Config, { - let a = 0u128.to_be_bytes(); - let b = 1u128.to_be_bytes(); + let a = 0_u128.to_be_bytes(); + let b = 1_u128.to_be_bytes(); CurrencyPair::new( AssetIdOf::::decode(&mut &a[..]).unwrap(), AssetIdOf::::decode(&mut &b[..]).unwrap(), @@ -45,7 +46,7 @@ benchmarks! { let sell = sell_identity::(); let account_id : T::AccountId = whitelisted_caller(); let caller = RawOrigin::Signed(account_id.clone()); - let amount: T::Balance = 1_000_000u64.into(); + let amount: T::Balance = 1_000_000_000_000_u64.into(); ::MultiCurrency::mint_into(sell.pair.base, &account_id, amount).unwrap(); }: _( caller, @@ -57,7 +58,12 @@ benchmarks! { let sell = sell_identity::(); let account_id : T::AccountId = whitelisted_caller(); let caller = RawOrigin::Signed(account_id.clone()); - let amount: T::Balance = 1_000_000u64.into(); + let amount: T::Balance = 1_000_000_000_000_u64.into(); + + let encoded = CurrencyId::PICA.encode(); + let native_asset_id = T::MayBeAssetId::decode(&mut &encoded[..]).unwrap(); + ::MultiCurrency::mint_into(native_asset_id, &account_id, amount).unwrap(); + ::MultiCurrency::mint_into(sell.pair.base, &account_id, amount).unwrap(); ::MultiCurrency::mint_into(sell.pair.quote, &account_id, amount).unwrap(); DutchAuction::::ask(caller.clone().into(), sell, <_>::default()).unwrap(); @@ -75,7 +81,13 @@ benchmarks! { let sell = sell_identity::(); let account_id : T::AccountId = whitelisted_caller(); let caller = RawOrigin::Signed(account_id.clone()); - let amount: T::Balance = 1_000_000u64.into(); + let amount: T::Balance = 1_000_000_000_000_u64.into(); + + let encoded = CurrencyId::PICA.encode(); + let native_asset_id = T::MayBeAssetId::decode(&mut &encoded[..]).unwrap(); + ::MultiCurrency::mint_into(native_asset_id, &account_id, amount).unwrap(); + + ::MultiCurrency::mint_into(sell.pair.base, &account_id, amount).unwrap(); DutchAuction::::ask(caller.clone().into(), sell, <_>::default()).unwrap(); let order_id = OrdersIndex::::get(); @@ -87,14 +99,19 @@ benchmarks! { let sell = sell_identity::(); let account_id : T::AccountId = whitelisted_caller(); let caller = RawOrigin::Signed(account_id.clone()); - let amount: T::Balance = 1_000_000u64.into(); + let amount: T::Balance = 1_000_000_000_000_u64.into(); + + let encoded = CurrencyId::PICA.encode(); + let native_asset_id = T::MayBeAssetId::decode(&mut &encoded[..]).unwrap(); + ::MultiCurrency::mint_into(native_asset_id, &account_id, amount).unwrap(); + ::MultiCurrency::mint_into(sell.pair.base, &account_id, amount).unwrap(); ::MultiCurrency::mint_into(sell.pair.quote, &account_id, amount).unwrap(); DutchAuction::::ask(caller.clone().into(), sell, <_>::default()).unwrap(); let order_id = OrdersIndex::::get(); let take_order = take_identity::(); - DutchAuction::::take(caller.clone().into(), order_id, take_order.clone()).unwrap(); + DutchAuction::::take(caller.into(), order_id, take_order).unwrap(); } : { as Hooks>>::on_finalize(T::BlockNumber::default()) } diff --git a/frame/dutch-auction/src/lib.rs b/frame/dutch-auction/src/lib.rs index 1d5edeb82e8..3a122905955 100644 --- a/frame/dutch-auction/src/lib.rs +++ b/frame/dutch-auction/src/lib.rs @@ -19,6 +19,13 @@ //! Sell takes deposit (as for accounts), to store sells for some time. //! We have to store lock deposit value with ask as it can change within time. //! Later deposit is used by pallet as initiative to liquidate garbage. +//! +//! # Price prediction +//! Dutch action starts with configured price and than and other price value is f(t). +//! So any external observer can predict what price will be on specified block. +//! +//! # DEX +//! Currently this dutch auction does not tries to sell on external DEX. #![cfg_attr( not(test), @@ -68,10 +75,9 @@ pub mod pallet { pub use crate::weights::WeightInfo; use codec::{Decode, Encode}; use composable_traits::{ - auction::AuctionStepFunction, defi::{DeFiComposableConfig, DeFiEngine, OrderIdLike, Sell, SellEngine, Take}, - loans::DurationSeconds, - math::{SafeArithmetic, WrappingNext}, + math::WrappingNext, + time::{TimeReleaseFunction, Timestamp}, }; use frame_support::{ pallet_prelude::*, @@ -88,10 +94,7 @@ pub mod pallet { use crate::math::*; use orml_traits::{MultiCurrency, MultiReservableCurrency}; - use sp_runtime::{ - traits::{AccountIdConversion, Saturating}, - DispatchError, - }; + use sp_runtime::{traits::AccountIdConversion, DispatchError}; use sp_std::vec::Vec; #[pallet::config] @@ -121,14 +124,14 @@ pub mod pallet { pub struct SellOrder { pub from_to: AccountId, pub order: Sell, - pub configuration: AuctionStepFunction, + pub configuration: TimeReleaseFunction, /// context captured when sell started pub context: Context, } #[derive(Encode, Decode, Default, TypeInfo, Clone, Debug, PartialEq)] pub struct Context { - pub added_at: DurationSeconds, + pub added_at: Timestamp, pub deposit: Balance, } @@ -208,11 +211,11 @@ pub mod pallet { pub fn ask( origin: OriginFor, order: Sell, - configuration: AuctionStepFunction, + configuration: TimeReleaseFunction, ) -> DispatchResultWithPostInfo { let who = &(ensure_signed(origin)?); let order_id = - >::ask(who, order, configuration)?; + >::ask(who, order, configuration)?; Self::deposit_event(Event::OrderAdded { order_id, @@ -229,7 +232,7 @@ pub mod pallet { take: Take, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - >::take(&who, order_id, take)?; + >::take(&who, order_id, take)?; Ok(().into()) } @@ -255,12 +258,12 @@ pub mod pallet { } } - impl SellEngine for Pallet { + impl SellEngine for Pallet { type OrderId = T::OrderId; fn ask( from_to: &Self::AccountId, order: Sell, - configuration: AuctionStepFunction, + configuration: TimeReleaseFunction, ) -> Result { ensure!(order.is_valid(), Error::::OrderParametersIsInvalid,); let order_id = >::mutate(|x| { @@ -296,12 +299,12 @@ pub mod pallet { order.order.take.limit <= take.limit, Error::::TakeLimitDoesNotSatisfiesOrder, ); - let limit = order.order.take.limit.into(); + let limit = order.order.take.limit; // may consider storing calculation results within single block, so that finalize does // not recalculates let passed = T::UnixTime::now().as_secs() - order.context.added_at; let _limit = order.configuration.price(limit, passed)?; - let quote_amount = take.quote_amount()?; + let quote_amount = take.quote_limit_amount()?; T::MultiCurrency::reserve(order.order.pair.quote, from_to, quote_amount)?; >::append(order_id, TakeOf:: { from_to: from_to.clone(), take }); @@ -316,49 +319,58 @@ pub mod pallet { // so we stay fast and prevent attack fn on_finalize(_n: T::BlockNumber) { for (order_id, mut takes) in >::iter() { - // users payed N * WEIGHT before, we here pay N * (log N - 1) * Weight. We can - // retain pure N by first served principle so, not highest price. - takes.sort_by(|a, b| b.take.limit.cmp(&a.take.limit)); - let SellOrder { mut order, context: _, from_to: ref seller, configuration: _ } = - >::get(order_id) - .expect("takes are added only onto existing orders"); - - // calculate real price - for take in takes { - let quote_amount = take.take.amount.saturating_mul(take.take.limit); - // what to do with orders which nobody ever takes? some kind of dust orders with - if order.take.amount == T::Balance::zero() { - T::MultiCurrency::unreserve(order.pair.quote, &take.from_to, quote_amount); - } else { - let take_amount = take.take.amount.min(order.take.amount); - order.take.amount -= take_amount; - let real_quote_amount = take_amount - .safe_mul(&take.take.limit) - .expect("was checked in take call"); - - exchange_reserved::( - order.pair.base, - seller, - take_amount, - order.pair.quote, - &take.from_to, - real_quote_amount, - ) - .expect("we forced locks beforehand"); - - if real_quote_amount < quote_amount { + if let Some(SellOrder { + mut order, + context: _, + from_to: ref seller, + configuration: _, + }) = >::get(order_id) + { + // users payed N * WEIGHT before, we here pay N * (log N - 1) * Weight. We can + // retain pure N by first served principle so, not highest price. + takes.sort_by(|a, b| b.take.limit.cmp(&a.take.limit)); + // calculate real price + for take in takes { + let quote_amount = + take.take.quote_limit_amount().expect("was checked in take call"); + // what to do with orders which nobody ever takes? some kind of dust orders + // with + if order.take.amount == T::Balance::zero() { T::MultiCurrency::unreserve( order.pair.quote, &take.from_to, - quote_amount - real_quote_amount, + quote_amount, ); + } else { + let take_amount = take.take.amount.min(order.take.amount); + order.take.amount -= take_amount; + let real_quote_amount = + take.take.quote_amount(take_amount).expect("was taken via min"); + + exchange_reserved::( + order.pair.base, + seller, + take_amount, + order.pair.quote, + &take.from_to, + real_quote_amount, + ) + .expect("we forced locks beforehand"); + + if real_quote_amount < quote_amount { + T::MultiCurrency::unreserve( + order.pair.quote, + &take.from_to, + quote_amount - real_quote_amount, + ); + } } } - } - if order.take.amount == T::Balance::zero() { - >::remove(order_id); - Self::deposit_event(Event::OrderRemoved { order_id }); + if order.take.amount == T::Balance::zero() { + >::remove(order_id); + Self::deposit_event(Event::OrderRemoved { order_id }); + } } } >::remove_all(None); diff --git a/frame/dutch-auction/src/math.rs b/frame/dutch-auction/src/math.rs index 0bc9bf4bf83..53ffefd8282 100644 --- a/frame/dutch-auction/src/math.rs +++ b/frame/dutch-auction/src/math.rs @@ -3,9 +3,9 @@ //! https://github.com/makerdao/dss/blob/master/src/abaci.sol use composable_traits::{ - auction::{AuctionStepFunction, LinearDecrease, StairstepExponentialDecrease}, - loans::DurationSeconds, - math::{LiftedFixedBalance, SafeArithmetic}, + defi::LiftedFixedBalance, + math::SafeArithmetic, + time::{DurationSeconds, LinearDecrease, StairstepExponentialDecrease, TimeReleaseFunction}, }; use sp_runtime::{ @@ -22,15 +22,15 @@ pub trait AuctionTimeCurveModel { ) -> Result; } -impl AuctionTimeCurveModel for AuctionStepFunction { +impl AuctionTimeCurveModel for TimeReleaseFunction { fn price( &self, - initial_price: composable_traits::math::LiftedFixedBalance, - duration_since_start: composable_traits::loans::DurationSeconds, - ) -> Result { + initial_price: LiftedFixedBalance, + duration_since_start: DurationSeconds, + ) -> Result { match self { - AuctionStepFunction::LinearDecrease(x) => x.price(initial_price, duration_since_start), - AuctionStepFunction::StairstepExponentialDecrease(x) => + TimeReleaseFunction::LinearDecrease(x) => x.price(initial_price, duration_since_start), + TimeReleaseFunction::StairstepExponentialDecrease(x) => x.price(initial_price, duration_since_start), } } @@ -82,9 +82,8 @@ impl AuctionTimeCurveModel for StairstepExponentialDecrease { mod tests { use composable_traits::{ - auction::{LinearDecrease, StairstepExponentialDecrease}, - loans::{DurationSeconds, ONE_HOUR}, - math::LiftedFixedBalance, + defi::LiftedFixedBalance, + time::{DurationSeconds, LinearDecrease, StairstepExponentialDecrease, ONE_HOUR}, }; use sp_arithmetic::assert_eq_error_rate; diff --git a/frame/dutch-auction/src/mock/currency.rs b/frame/dutch-auction/src/mock/currency.rs index 3c985f5c649..b2d912769c7 100644 --- a/frame/dutch-auction/src/mock/currency.rs +++ b/frame/dutch-auction/src/mock/currency.rs @@ -4,19 +4,9 @@ use scale_info::TypeInfo; use sp_runtime::{ArithmeticError, DispatchError}; #[derive( - PartialOrd, - Ord, - PartialEq, - Eq, - Debug, - Copy, - Clone, - codec::Encode, - codec::Decode, - serde::Serialize, - serde::Deserialize, - TypeInfo, + PartialOrd, Ord, PartialEq, Eq, Debug, Copy, Clone, codec::Encode, codec::Decode, TypeInfo, )] +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[allow(clippy::upper_case_acronyms)] // Currencies should be in CONSTANT_CASE pub enum CurrencyId { PICA, @@ -45,6 +35,5 @@ impl DynamicCurrencyId for CurrencyId { } parameter_types! { - pub const MaxStrategies: usize = 255; pub const NativeAssetId: CurrencyId = CurrencyId::PICA; } diff --git a/frame/dutch-auction/src/mock/mod.rs b/frame/dutch-auction/src/mock/mod.rs index 3d00d2eaad7..6c2997595d7 100644 --- a/frame/dutch-auction/src/mock/mod.rs +++ b/frame/dutch-auction/src/mock/mod.rs @@ -1,4 +1,4 @@ -#[cfg(test)] +#[cfg(any(test, feature = "runtime-benchmarks"))] pub mod currency; #[cfg(test)] pub mod governance_registry; diff --git a/frame/dutch-auction/src/mock/runtime.rs b/frame/dutch-auction/src/mock/runtime.rs index 859b47f51d1..cfd27c6d461 100644 --- a/frame/dutch-auction/src/mock/runtime.rs +++ b/frame/dutch-auction/src/mock/runtime.rs @@ -29,6 +29,7 @@ use super::governance_registry::GovernanceRegistry; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; pub type Block = frame_system::mocking::MockBlock; pub type Balance = u128; +pub type OrderId = u32; pub type Amount = i64; pub type AccountId = <::Signer as IdentifyAccount>::AccountId; @@ -127,7 +128,7 @@ parameter_types! { } impl pallet_timestamp::Config for Runtime { - type Moment = u64; + type Moment = composable_traits::time::Timestamp; type OnTimestampSet = (); type MinimumPeriod = MinimumPeriod; type WeightInfo = (); @@ -193,11 +194,11 @@ impl DeFiComposableConfig for Runtime { } parameter_types! { - pub static WeightToFee: u128 = 1; + pub static WeightToFee: Balance = 1; } impl WeightToFeePolynomial for WeightToFee { - type Balance = u128; + type Balance = Balance; fn polynomial() -> WeightToFeeCoefficients { let one = WeightToFeeCoefficient { @@ -215,7 +216,7 @@ impl pallet_dutch_auction::Config for Runtime { type UnixTime = Timestamp; - type OrderId = u8; + type OrderId = OrderId; type MultiCurrency = Assets; @@ -228,14 +229,13 @@ impl pallet_dutch_auction::Config for Runtime { type NativeCurrency = Balances; } +#[allow(dead_code)] // not really dead pub fn new_test_externalities() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let balances = vec![ - (AccountId::from(ALICE), 1_000_000_000_000_000_000_000_000), - (AccountId::from(BOB), 1_000_000_000_000_000_000_000_000), - ]; + let balances = + vec![(ALICE, 1_000_000_000_000_000_000_000_000), (BOB, 1_000_000_000_000_000_000_000_000)]; - pallet_balances::GenesisConfig:: { balances: balances.clone() } + pallet_balances::GenesisConfig:: { balances } .assimilate_storage(&mut storage) .unwrap(); diff --git a/frame/dutch-auction/src/tests.rs b/frame/dutch-auction/src/tests.rs index 01c3c67dd94..deb84c74430 100644 --- a/frame/dutch-auction/src/tests.rs +++ b/frame/dutch-auction/src/tests.rs @@ -1,6 +1,6 @@ use composable_traits::{ - auction::{AuctionStepFunction, LinearDecrease}, - defi::{Sell, Take}, + defi::{LiftedFixedBalance, Sell, Take}, + time::{LinearDecrease, TimeReleaseFunction}, }; use orml_traits::MultiReservableCurrency; @@ -12,9 +12,14 @@ use frame_support::{ Hooks, }, }; +use sp_runtime::{traits::AccountIdConversion, FixedPointNumber}; use crate::mock::{currency::CurrencyId, runtime::*}; +fn fixed(n: u128) -> LiftedFixedBalance { + LiftedFixedBalance::saturating_from_integer(n) +} + pub fn new_test_externalities() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); let balances = @@ -32,6 +37,7 @@ pub fn new_test_externalities() -> sp_io::TestExternalities { externatlities } +// ensure that we take extra for sell, at least amount to remove #[test] fn setup_sell() { new_test_externalities().execute_with(|| { @@ -41,20 +47,26 @@ fn setup_sell() { .unwrap(); Tokens::mint_into(CurrencyId::BTC, &ALICE, 100000000000).unwrap(); let seller = AccountId::from_raw(ALICE.0); - let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, 1, 1000); + let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, 1, fixed(1000)); let invalid = crate::OrdersIndex::::get(); - let configuration = AuctionStepFunction::LinearDecrease(LinearDecrease { total: 42 }); + let configuration = TimeReleaseFunction::LinearDecrease(LinearDecrease { total: 42 }); let not_reserved = Assets::reserved_balance(CurrencyId::BTC, &ALICE); + let gas = Assets::balance(CurrencyId::PICA, &ALICE); + let treasury = + Assets::balance(CurrencyId::PICA, &DutchAuctionPalletId::get().into_account()); DutchAuction::ask(Origin::signed(seller), sell, configuration).unwrap(); + let treasury_added = + Assets::balance(CurrencyId::PICA, &DutchAuctionPalletId::get().into_account()) - + treasury; + assert!(treasury_added > 0); + assert!(treasury_added >= <() as crate::weights::WeightInfo>::liquidate() as u128); let reserved = Assets::reserved_balance(CurrencyId::BTC, &ALICE); assert!(not_reserved < reserved && reserved == 1); let order_id = crate::OrdersIndex::::get(); assert_ne!(invalid, order_id); - - // TODO: Fix the check below - // let initiative: u128 = Assets::reserved_balance(CurrencyId::PICA, &ALICE); - // let taken = <() as crate::weights::WeightInfo>::liquidate(); - // assert!(initiative == taken.into()); + let ask_gas = <() as crate::weights::WeightInfo>::ask() as u128; + let remaining_gas = Assets::balance(CurrencyId::PICA, &ALICE); + assert!(gas < remaining_gas + ask_gas + treasury_added); }); } @@ -68,16 +80,16 @@ fn with_immediate_exact_buy() { let seller = AccountId::from_raw(ALICE.0); let buyer = AccountId::from_raw(BOB.0); let sell_amount = 1; - let take_amount = 1000; - let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, sell_amount, take_amount); - let configuration = AuctionStepFunction::LinearDecrease(LinearDecrease { total: 42 }); + let take_amount = 1000_u128; + let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, sell_amount, fixed(take_amount)); + let configuration = TimeReleaseFunction::LinearDecrease(LinearDecrease { total: 42 }); DutchAuction::ask(Origin::signed(seller), sell, configuration).unwrap(); let order_id = crate::OrdersIndex::::get(); - let result = DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, 999)); + let result = DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, fixed(999))); assert!(!result.is_ok()); let not_reserved = >::reserved_balance(CurrencyId::USDT, &BOB); - let result = DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, 1000)); + let result = DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, fixed(1000))); assert_ok!(result); let reserved = Assets::reserved_balance(CurrencyId::USDT, &BOB); assert!(not_reserved < reserved && reserved == take_amount); @@ -100,13 +112,13 @@ fn with_two_takes_higher_than_limit_and_not_enough_for_all() { let buyer = AccountId::from_raw(BOB.0); let sell_amount = 3; let take_amount = 1000; - let configuration = AuctionStepFunction::LinearDecrease(LinearDecrease { total: 42 }); + let configuration = TimeReleaseFunction::LinearDecrease(LinearDecrease { total: 42 }); - let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, sell_amount, take_amount); + let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, sell_amount, fixed(take_amount)); DutchAuction::ask(Origin::signed(seller), sell, configuration).unwrap(); let order_id = crate::OrdersIndex::::get(); - assert_ok!(DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, 1001))); - assert_ok!(DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, 1002))); + assert_ok!(DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, fixed(1001)))); + assert_ok!(DutchAuction::take(Origin::signed(buyer), order_id, Take::new(1, fixed(1002)))); DutchAuction::on_finalize(42); @@ -120,8 +132,8 @@ fn liquidation() { new_test_externalities().execute_with(|| { Tokens::mint_into(CurrencyId::BTC, &ALICE, 10).unwrap(); let seller = AccountId::from_raw(ALICE.0); - let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, 1, 1000); - let configuration = AuctionStepFunction::LinearDecrease(LinearDecrease { total: 42 }); + let sell = Sell::new(CurrencyId::BTC, CurrencyId::USDT, 1, fixed(1000)); + let configuration = TimeReleaseFunction::LinearDecrease(LinearDecrease { total: 42 }); DutchAuction::ask(Origin::signed(seller), sell, configuration).unwrap(); let order_id = crate::OrdersIndex::::get(); let _balance_before = >::balance(&ALICE); diff --git a/frame/lending/Cargo.toml b/frame/lending/Cargo.toml index b73946ec3ad..afeada82071 100644 --- a/frame/lending/Cargo.toml +++ b/frame/lending/Cargo.toml @@ -37,13 +37,12 @@ log = { version = "0.4.14", default-features = false } num-traits = { version = "0.2.14", default-features = false } plotters = { version = "0.3.1", optional = true } scale-info = { version = "1.0", default-features = false, features = ["derive"] } -serde = { version = '1', optional = true } +serde = { version = '1.0.119' } [dev-dependencies] hex-literal = "0.3.3" once_cell = "1.8.0" proptest = "0.9.6" -serde = "1.0.119" orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "17a791edf431d7d7aee1ea3dfaeeb7bc21944301" } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "17a791edf431d7d7aee1ea3dfaeeb7bc21944301", default-features = false } @@ -59,7 +58,6 @@ pallet-assets = { path = '../assets', default-features = false} [features] default = ["std"] std = [ - "serde/std", "codec/std", "log/std", "frame-support/std", diff --git a/frame/lending/accrue_interest.png b/frame/lending/accrue_interest.png deleted file mode 100644 index e8e2f1063420a697d8458f18eda130090eb72466..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36565 zcmeI5e@qis0LKri2x`~qHkoeoaG4<+W;Ii(f2gORLv$0jX;6YG(`9Z!s3s$*%sXUE zH2#2uD2qY6#W^%_>S7`eWXD8dg3e7Gr~I&pDAW$2)I!^P_uj4w+y2=f{<-8!E`i>K z_pa~WyMEudpYOX}%aM^75fahx%6@xu2xZiN1)feR1Sf6cm?m%?%TN)s0@Sh2aZ zr@8I-v|Fn)KJ6SC8@{JIW*Yb5h_}P9+^GxwbNeNnURaQ>v|*H-j|=`XkL+8I(b`;*e5>MZm^wU}gkh$IIU}ana3tWZ zz>0#Y1`l#*egv}sW&z9sm<2EkU>3kEfLQ>uARm$lk_eIrk_eIrk_eIrk_eIrk_d<* zV9Q~?3T!G+UO>tJ;N2h#Ko)>309gRC0AvBk0+0nD3*sS(Ac-J}Ac-J}Ac-J}Ac-J} zAc?SM0hV~cVij1d0*h5(u?j3!G5lpJU`xQ3fGq)A0=5Kf3D^>_C16XymVhk*TMo|; z09yjK1Z)Y|^6{#`FbiN7z$}1S0J8vQ0n7rJ1uzR>7QifkSpc&D4_U$EV(@ewJSS)f zxkvs4`4i+%kUv5G1o;!>Pmn)B{sj3Gg<}wRjKuxGQ|_*piF&tZigK% zZV*S(v@wa1w+NDIQ&N6?aNh@;VQsW?39eweg zicEi?&LmbQZx`;cF^@_pIr=bUW7S0Vklo`$NOZNJw(yP_*LDQUOHl9ett8Lx<7r=k zm2LDFTiHO}wLom&JLff@EsLbnQ`9$588WtZ?_R#!oNN()o-ArLRmM5IutcnvTbupf zQ_>0IKP9PUl0mv)?$+CAX`7*a>-2fanVg_iGUi_=hz-&d+QYNLt-`i0fs(*l62r13 z(lsw~0bR`sTB=u(R)0*1Kp;fi-nC*OdnvN0A2E zB$5Nt%)T*!p(5^sjjQAOQ&kCyp zZoSE;J-S=qYb^98NdZAKl892hx5~k8pWX3m9XqtoO2)TPceG3#Bg8;Yp-C+hx*`?j z?)rK|E)#3a`gPO0Qu+45fcr?N!(qw2>PtFHUiM$gNISy|ABdV>cA2Apx0OXm+R`HH zoTmRAQbQx{8@*=@4gYxkl0O-g6+841Ff>+;Pdipo@Z( borrow_asset: u128, collateral_asset: u128, ) -> (crate::MarketIndex, ::VaultId) { - let market_config = MarketConfigInput { + let market_config = CreateInput { liquidator: None, manager, reserved: Perquintill::from_percent(10), - collateral_factor: NormalizedCollateralFactor::saturating_from_rational(200, 100), + collateral_factor: MoreThanOneFixedU128::saturating_from_rational(200, 100), under_collaterized_warn_percent: Percent::from_percent(10), }; Lending::::create( @@ -67,7 +67,7 @@ benchmarks! { let borrow_asset_id = ::AssetId::from(BTC); let collateral_asset_id = ::AssetId::from(USDT); let reserved_factor = Perquintill::from_percent(10); - let collateral_factor = NormalizedCollateralFactor::saturating_from_rational(200, 100); + let collateral_factor = MoreThanOneFixedU128::saturating_from_rational(200, 100); let under_collaterized_warn_percent = Percent::from_percent(10); let market_id = MarketIndex::new(1); let vault_id = 1u64.into(); @@ -120,7 +120,7 @@ benchmarks! { Lending::::deposit_collateral_internal(&market, &caller, amount).unwrap(); }: _(RawOrigin::Signed(caller.clone()), market, amount) verify { - assert_last_event::(Event::CollateralWithdrawed { + assert_last_event::(Event::CollateralWithdrawn { sender: caller, market_id: market, amount diff --git a/frame/lending/src/lib.rs b/frame/lending/src/lib.rs index 8dae973b6ae..2193ce85095 100644 --- a/frame/lending/src/lib.rs +++ b/frame/lending/src/lib.rs @@ -52,17 +52,18 @@ pub use crate::weights::WeightInfo; #[frame_support::pallet] pub mod pallet { use crate::{models::BorrowerData, weights::WeightInfo}; - use codec::{Codec, FullCodec}; + use codec::Codec; use composable_traits::{ currency::CurrencyFactory, - defi::Rate, + defi::*, lending::{ - math::*, BorrowAmountOf, CollateralLpAmountOf, Lending, MarketConfig, MarketConfigInput, + math::{self, *}, + BorrowAmountOf, CollateralLpAmountOf, CreateInput, Lending, MarketConfig, UpdateInput, }, liquidation::Liquidation, - loans::{DurationSeconds, Timestamp}, - math::{LiftedFixedBalance, SafeArithmetic}, + math::SafeArithmetic, oracle::Oracle, + time::{DurationSeconds, Timestamp, SECONDS_PER_YEAR_NAIVE}, vault::{Deposit, FundsAvailability, StrategicVault, Vault, VaultConfig}, }; use frame_support::{ @@ -79,23 +80,20 @@ pub mod pallet { offchain::{AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer}, pallet_prelude::*, }; - use num_traits::{CheckedDiv, SaturatingSub}; + use num_traits::CheckedDiv; use sp_core::crypto::KeyTypeId; use sp_runtime::{ - traits::{ - AccountIdConversion, AtLeast32BitUnsigned, CheckedAdd, CheckedMul, CheckedSub, One, - Saturating, Zero, - }, - ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128, - KeyTypeId as CryptoKeyTypeId, Percent, Perquintill, + traits::{AccountIdConversion, CheckedAdd, CheckedMul, CheckedSub, One, Saturating, Zero}, + ArithmeticError, DispatchError, FixedPointNumber, FixedU128, KeyTypeId as CryptoKeyTypeId, + Percent, Perquintill, }; use sp_std::{fmt::Debug, vec, vec::Vec}; type MarketConfiguration = MarketConfig< ::VaultId, - ::AssetId, + ::MayBeAssetId, ::AccountId, - ::GroupId, + ::LiquidationStrategyId, >; #[derive(Default, Debug, Copy, Clone, Encode, Decode, PartialEq, TypeInfo)] @@ -157,130 +155,67 @@ pub mod pallet { } #[pallet::config] - #[cfg(not(feature = "runtime-benchmarks"))] - pub trait Config: CreateSignedTransaction> + frame_system::Config { - type Event: From> + IsType<::Event>; - type Oracle: Oracle::AssetId, Balance = Self::Balance>; - type VaultId: Clone + Codec + Debug + PartialEq + Default + Parameter; - type Vault: StrategicVault< - VaultId = Self::VaultId, - AssetId = ::AssetId, - Balance = Self::Balance, - AccountId = Self::AccountId, - >; - - type CurrencyFactory: CurrencyFactory<::AssetId>; - type AssetId: FullCodec - + Eq - + PartialEq - + Copy - + MaybeSerializeDeserialize - + Debug - + Default - + TypeInfo; - - type Balance: Default - + Parameter - + Codec - + Copy - + Ord - + CheckedAdd - + CheckedSub - + CheckedMul - + SaturatingSub - + AtLeast32BitUnsigned - + From // at least 64 bit - + Zero - + FixedPointOperand - + Into // integer part not more than bits in this - + Into; // cannot do From, until LiftedFixedBalance integer part is larger than 128 - // bit - - /// vault owned - can transfer, cannot mint - type Currency: Transfer::AssetId> - + Mutate::AssetId>; - - /// market owned - debt token can be minted - type MarketDebtCurrency: Transfer::AssetId> - + Mutate::AssetId> - + MutateHold::AssetId> - + InspectHold::AssetId>; - - type Liquidation: Liquidation< - AssetId = Self::AssetId, - Balance = Self::Balance, - AccountId = Self::AccountId, - GroupId = Self::GroupId, - >; - type UnixTime: UnixTime; - type MaxLendingCount: Get; - type AuthorityId: AppCrypto; - type WeightInfo: WeightInfo; - type GroupId: FullCodec + Default + PartialEq + Clone + Debug + TypeInfo; - } - #[cfg(feature = "runtime-benchmarks")] pub trait Config: - CreateSignedTransaction> + frame_system::Config + pallet_oracle::Config + CreateSignedTransaction> + frame_system::Config + DeFiComposableConfig { type Event: From> + IsType<::Event>; - type Oracle: Oracle::AssetId, Balance = Self::Balance>; - type VaultId: Clone + Codec + Debug + PartialEq + Default + Parameter + From; + type Oracle: Oracle< + AssetId = ::MayBeAssetId, + Balance = ::Balance, + >; + type VaultId: Clone + Codec + Debug + PartialEq + Default + Parameter; type Vault: StrategicVault< VaultId = Self::VaultId, - AssetId = ::AssetId, + AssetId = ::MayBeAssetId, Balance = Self::Balance, AccountId = Self::AccountId, >; - type CurrencyFactory: CurrencyFactory<::AssetId>; - type AssetId: FullCodec - + Eq - + PartialEq - + Copy - + MaybeSerializeDeserialize - + From - + Debug - + Default - + TypeInfo; - - type Balance: Default - + Parameter - + Codec - + Copy - + Ord - + CheckedAdd - + CheckedSub - + CheckedMul - + SaturatingSub - + AtLeast32BitUnsigned - + From // at least 64 bit - + Zero - + FixedPointOperand - + Into // integer part not more than bits in this - + Into; // cannot do From, until LiftedFixedBalance integer part is larger than 128 - // bit + type CurrencyFactory: CurrencyFactory<::MayBeAssetId>; /// vault owned - can transfer, cannot mint - type Currency: Transfer::AssetId> - + Mutate::AssetId>; + type Currency: Transfer< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + > + Mutate< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + >; /// market owned - debt token can be minted - type MarketDebtCurrency: Transfer::AssetId> - + Mutate::AssetId> - + MutateHold::AssetId> - + InspectHold::AssetId>; + type MarketDebtCurrency: Transfer< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + > + Mutate< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + > + MutateHold< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + > + InspectHold< + Self::AccountId, + Balance = Self::Balance, + AssetId = ::MayBeAssetId, + >; type Liquidation: Liquidation< - AssetId = ::AssetId, + MayBeAssetId = Self::MayBeAssetId, Balance = Self::Balance, AccountId = Self::AccountId, + LiquidationStrategyId = Self::LiquidationStrategyId, >; type UnixTime: UnixTime; type MaxLendingCount: Get; type AuthorityId: AppCrypto; type WeightInfo: WeightInfo; - type GroupId: FullCodec + Default + PartialEq + Clone + Debug + TypeInfo; + /// Id of proxy to liquidate + type LiquidationStrategyId: Parameter + Default + PartialEq + Clone + Debug + TypeInfo; } #[pallet::pallet] @@ -380,27 +315,41 @@ pub mod pallet { ExceedLendingCount, LiquidationFailed, BorrowerDataCalculationFailed, + Unauthorized, } #[pallet::event] #[pallet::generate_deposit(pub (crate) fn deposit_event)] pub enum Event { /// Event emitted when new lending market is created. - NewMarketCreated { + MarketCreated { market_id: MarketIndex, vault_id: T::VaultId, manager: T::AccountId, - borrow_asset_id: ::AssetId, - collateral_asset_id: ::AssetId, - reserved_factor: Perquintill, - collateral_factor: NormalizedCollateralFactor, + input: CreateInput, + }, + MarketUpdated { + market_id: MarketIndex, + input: UpdateInput, }, /// Event emitted when collateral is deposited. - CollateralDeposited { sender: T::AccountId, market_id: MarketIndex, amount: T::Balance }, + CollateralDeposited { + sender: T::AccountId, + market_id: MarketIndex, + amount: T::Balance, + }, /// Event emitted when collateral is withdrawed. - CollateralWithdrawed { sender: T::AccountId, market_id: MarketIndex, amount: T::Balance }, + CollateralWithdrawn { + sender: T::AccountId, + market_id: MarketIndex, + amount: T::Balance, + }, /// Event emitted when user borrows from given market. - Borrowed { sender: T::AccountId, market_id: MarketIndex, amount: T::Balance }, + Borrowed { + sender: T::AccountId, + market_id: MarketIndex, + amount: T::Balance, + }, /// Event emitted when user repays borrow of beneficiary in given market. RepaidBorrow { sender: T::AccountId, @@ -409,9 +358,15 @@ pub mod pallet { amount: T::Balance, }, /// Event emitted when a liquidation is initiated for a loan. - LiquidationInitiated { market_id: MarketIndex, account: T::AccountId }, + LiquidationInitiated { + market_id: MarketIndex, + account: T::AccountId, + }, /// Event emitted to warn that loan may go under collaterized soon. - SoonMayUnderCollaterized { market_id: MarketIndex, account: T::AccountId }, + SoonMayUnderCollaterized { + market_id: MarketIndex, + account: T::AccountId, + }, } /// Lending instances counter @@ -427,7 +382,12 @@ pub mod pallet { _, Twox64Concat, MarketIndex, - MarketConfig::AssetId, T::AccountId, T::GroupId>, + MarketConfig< + T::VaultId, + ::MayBeAssetId, + T::AccountId, + T::LiquidationStrategyId, + >, >; /// Original debt values are on balances. @@ -435,8 +395,13 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn debt_currencies)] #[allow(clippy::disallowed_type)] // AssetId implements default, so ValueQuery is ok here. - pub type DebtMarkets = - StorageMap<_, Twox64Concat, MarketIndex, ::AssetId, ValueQuery>; + pub type DebtMarkets = StorageMap< + _, + Twox64Concat, + MarketIndex, + ::MayBeAssetId, + ValueQuery, + >; /// at which lending index account did borrowed. #[pallet::storage] @@ -447,7 +412,7 @@ pub mod pallet { MarketIndex, Twox64Concat, T::AccountId, - Ratio, + ZeroToOneFixedU128, OptionQuery, >; @@ -468,7 +433,8 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn borrow_index)] #[allow(clippy::disallowed_type)] // MarketIndex implements default, so ValueQuery is ok here. - pub type BorrowIndex = StorageMap<_, Twox64Concat, MarketIndex, Ratio, ValueQuery>; + pub type BorrowIndex = + StorageMap<_, Twox64Concat, MarketIndex, ZeroToOneFixedU128, ValueQuery>; /// (Market, Account) -> Collateral #[pallet::storage] @@ -521,54 +487,57 @@ pub mod pallet { } } + #[allow(type_alias_bounds)] // false positive + pub type CreateInputOf = CreateInput; + #[pallet::call] impl Pallet { /// Create a new lending market. - /// - `origin` : Sender of this extrinsic. (Also manager for new market to be created.) - /// - `collateral_asset_id` : AssetId for collateral. - /// - `reserved_factor` : Reserve factor of market to be created. - /// - `collateral_factor` : Collateral factor of market to be created. - /// - `under_collaterized_warn_percent` : warn borrower when loan's collateral/debt ratio - /// given percentage short to be under collaterized + /// - `origin` : Sender of this extrinsic. Manager for new market to be created. Can pause + /// borrow & deposits of assets. #[pallet::weight(::WeightInfo::create_new_market())] #[transactional] - #[allow(clippy::too_many_arguments)] - pub fn create_new_market( + pub fn create_market( origin: OriginFor, - borrow_asset_id: ::AssetId, - collateral_asset_id: ::AssetId, - reserved_factor: Perquintill, - collateral_factor: NormalizedCollateralFactor, - under_collaterized_warn_percent: Percent, - interest_rate_model: InterestRateModel, - liquidator: Option, + input: CreateInput, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let market_config = MarketConfigInput { - reserved: reserved_factor, - manager: who.clone(), - collateral_factor, - under_collaterized_warn_percent, - liquidator, - }; - let (market_id, vault_id) = Self::create( - borrow_asset_id, - collateral_asset_id, - market_config, - &interest_rate_model, - )?; - Self::deposit_event(Event::::NewMarketCreated { + let (market_id, vault_id) = Self::create(who.clone(), input.clone())?; + Self::deposit_event(Event::::MarketCreated { market_id, vault_id, manager: who, - borrow_asset_id, - collateral_asset_id, - reserved_factor, - collateral_factor, + input, }); Ok(().into()) } + /// owner must be very careful calling this + #[pallet::weight(::WeightInfo::create_new_market())] + #[transactional] + pub fn update_market( + origin: OriginFor, + market_id: MarketIndex, + input: UpdateInput, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Markets::::mutate(&market_id, |market| { + if let Some(market) = market { + ensure!(who == market.manager, Error::::Unauthorized); + + market.collateral_factor = input.collateral_factor; + market.interest_rate_model = input.interest_rate_model; + market.under_collaterized_warn_percent = input.under_collaterized_warn_percent; + market.liquidators = input.liquidators.clone(); + Ok(()) + } else { + Err(Error::::MarketDoesNotExist) + } + })?; + Self::deposit_event(Event::::MarketUpdated { market_id, input }); + Ok(().into()) + } + /// Deposit collateral to market. /// - `origin` : Sender of this extrinsic. /// - `market` : Market index to which collateral will be deposited. @@ -599,7 +568,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; Self::withdraw_collateral_internal(&market_id, &sender, amount)?; - Self::deposit_event(Event::::CollateralWithdrawed { sender, market_id, amount }); + Self::deposit_event(Event::::CollateralWithdrawn { sender, market_id, amount }); Ok(().into()) } @@ -627,7 +596,8 @@ pub mod pallet { /// Repay borrow for beneficiary account. /// - `origin` : Sender of this extrinsic. (Also the user who repays beneficiary's borrow.) /// - `market_id` : Market index to which user wants to repay borrow. - /// - `beneficiary` : AccountId which has borrowed asset. (This can be same or differnt than + /// - `beneficiary` : AccountId which has borrowed asset. (This can be same or different + /// than /// origin). /// - `repay_amount` : Amount which user wants to borrow. #[pallet::weight(::WeightInfo::repay_borrow())] @@ -671,51 +641,41 @@ pub mod pallet { impl Pallet { pub fn total_interest_accurate( market_id: &::MarketId, - ) -> Result { + ) -> Result { let debt_asset_id = DebtMarkets::::get(market_id); let total_interest = T::MarketDebtCurrency::balance(debt_asset_id, &Self::account_id(market_id)); Ok(total_interest) } - pub fn account_id(market_id: &::MarketId) -> ::AccountId { + pub fn account_id( + market_id: &::MarketId, + ) -> ::AccountId { ::account_id(market_id) } pub fn calc_utilization_ratio( - cash: &::Balance, - borrows: &::Balance, + cash: ::Balance, + borrows: ::Balance, ) -> Result { ::calc_utilization_ratio(cash, borrows) } - pub fn create( - borrow_asset: ::AssetId, - collateral_asset: ::AssetId, - config_input: MarketConfigInput<::AccountId, T::GroupId>, - interest_rate_model: &InterestRateModel, - ) -> Result<(::MarketId, ::VaultId), DispatchError> { - ::create( - borrow_asset, - collateral_asset, - config_input, - interest_rate_model, - ) - } + pub fn deposit_collateral_internal( market_id: &::MarketId, - account_id: &::AccountId, + account_id: &::AccountId, amount: CollateralLpAmountOf, ) -> Result<(), DispatchError> { ::deposit_collateral(market_id, account_id, amount) } pub fn collateral_of_account( market_id: &::MarketId, - account: &::AccountId, - ) -> Result<::Balance, DispatchError> { + account: &::AccountId, + ) -> Result<::Balance, DispatchError> { ::collateral_of_account(market_id, account) } pub fn withdraw_collateral_internal( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, amount: CollateralLpAmountOf, ) -> Result<(), DispatchError> { ::withdraw_collateral(market_id, account, amount) @@ -723,48 +683,48 @@ pub mod pallet { pub fn get_borrow_limit( market_id: &::MarketId, - account: &::AccountId, - ) -> Result<::Balance, DispatchError> { + account: &::AccountId, + ) -> Result<::Balance, DispatchError> { ::get_borrow_limit(market_id, account) } pub fn borrow_internal( market_id: &::MarketId, - debt_owner: &::AccountId, - amount_to_borrow: ::Balance, + debt_owner: &::AccountId, + amount_to_borrow: ::Balance, ) -> Result<(), DispatchError> { ::borrow(market_id, debt_owner, amount_to_borrow) } pub fn borrow_balance_current( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, ) -> Result>, DispatchError> { ::borrow_balance_current(market_id, account) } pub fn total_borrows( market_id: &::MarketId, - ) -> Result<::Balance, DispatchError> { + ) -> Result<::Balance, DispatchError> { ::total_borrows(market_id) } pub fn total_cash( market_id: &::MarketId, - ) -> Result<::Balance, DispatchError> { + ) -> Result<::Balance, DispatchError> { ::total_cash(market_id) } pub fn total_interest( market_id: &::MarketId, - ) -> Result<::Balance, DispatchError> { + ) -> Result<::Balance, DispatchError> { ::total_interest(market_id) } pub fn repay_borrow_internal( market_id: &::MarketId, - from: &::AccountId, - beneficiary: &::AccountId, + from: &::AccountId, + beneficiary: &::AccountId, repay_amount: Option>, ) -> Result<(), DispatchError> { ::repay_borrow(market_id, from, beneficiary, repay_amount) @@ -772,7 +732,7 @@ pub mod pallet { pub fn create_borrower_data( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, ) -> Result { let market = Self::get_market(market_id)?; let collateral_balance = Self::collateral_of_account(market_id, account)?; @@ -793,7 +753,7 @@ pub mod pallet { pub fn should_liquidate( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, ) -> Result { let borrower = Self::create_borrower_data(market_id, account)?; let should_liquidate = borrower.should_liquidate()?; @@ -802,7 +762,7 @@ pub mod pallet { pub fn soon_under_collaterized( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, ) -> Result { let borrower = Self::create_borrower_data(market_id, account)?; let should_warn = borrower.should_warn()?; @@ -814,13 +774,20 @@ pub mod pallet { /// if there is any error then propagate that error. pub fn liquidate_internal( market_id: &::MarketId, - account: &::AccountId, + account: &::AccountId, ) -> Result<(), DispatchError> { if Self::should_liquidate(market_id, account)? { - Err(DispatchError::Other("TODO: work happens in other branch")) - } else { - Ok(()) + let market = Self::get_market(market_id)?; + let borrow_asset = T::Vault::asset_id(&market.borrow)?; + let collateral_to_liquidate = Self::collateral_of_account(market_id, account)?; + let source_target_account = Self::account_id(market_id); + let unit_price = + T::Oracle::get_ratio(CurrencyPair::new(market.collateral, borrow_asset))?; + let sell = + Sell::new(market.collateral, borrow_asset, collateral_to_liquidate, unit_price); + T::Liquidation::liquidate(&source_target_account, sell, market.liquidators)?; } + Ok(()) } pub(crate) fn initialize_block( @@ -880,15 +847,11 @@ pub mod pallet { } else { errors.iter().for_each(|e| { if let Err(e) = e { - #[cfg(test)] - { - panic!("test failed with {:?}", e); - } log::error!( - "This should never happen, could not initialize block!!! {:#?} {:#?}", - block_number, - e - ) + "This should never happen, could not initialize block!!! {:#?} {:#?}", + block_number, + e + ) } }); TransactionOutcome::Rollback(0) @@ -947,7 +910,7 @@ pub mod pallet { } fn get_price( - asset_id: ::AssetId, + asset_id: ::MayBeAssetId, amount: T::Balance, ) -> Result { ::get_price(asset_id, amount) @@ -958,23 +921,18 @@ pub mod pallet { fn updated_account_interest_index( market_id: &MarketIndex, debt_owner: &T::AccountId, - amount: T::Balance, + amount_to_borrow: T::Balance, ) -> Result { let market_index = BorrowIndex::::try_get(market_id).map_err(|_| Error::::MarketDoesNotExist)?; let account_interest_index = - DebtIndex::::get(market_id, debt_owner).unwrap_or_else(Ratio::zero); + DebtIndex::::get(market_id, debt_owner).unwrap_or_else(ZeroToOneFixedU128::zero); let debt_asset_id = DebtMarkets::::get(market_id); let existing_borrow_amount = T::MarketDebtCurrency::balance(debt_asset_id, debt_owner); - let amount_to_borrow: u128 = amount.into(); - let amount_to_borrow = amount_to_borrow - .checked_mul(LiftedFixedBalance::accuracy()) - .ok_or(Error::::Overflow)?; + T::MarketDebtCurrency::mint_into(debt_asset_id, debt_owner, amount_to_borrow)?; T::MarketDebtCurrency::hold(debt_asset_id, debt_owner, amount_to_borrow)?; - let total_borrow_amount = existing_borrow_amount - .checked_add(amount_to_borrow) - .ok_or(Error::::Overflow)?; + let total_borrow_amount = existing_borrow_amount.safe_add(&amount_to_borrow)?; let existing_borrow_share = Percent::from_rational(existing_borrow_amount, total_borrow_amount); let new_borrow_share = Percent::from_rational(amount_to_borrow, total_borrow_amount); @@ -986,7 +944,6 @@ pub mod pallet { market_id: &MarketIndex, debt_owner: &T::AccountId, amount_to_borrow: BorrowAmountOf, - asset_id: ::AssetId, market: MarketConfiguration, market_account: &T::AccountId, ) -> Result<(), DispatchError> { @@ -1032,7 +989,7 @@ pub mod pallet { beneficiary: &T::AccountId, repay_amount: BorrowAmountOf, owed: BorrowAmountOf, - borrow_asset_id: ::AssetId, + borrow_asset_id: ::MayBeAssetId, market_account: &T::AccountId, ) -> Result<(), DispatchError> { let latest_borrow_timestamp = BorrowTimestamp::::get(market_id, beneficiary); @@ -1044,7 +1001,7 @@ pub mod pallet { ); } ensure!( - repay_amount > ::Balance::zero(), + repay_amount > ::Balance::zero(), Error::::RepayAmountMustBeGraterThanZero ); ensure!(repay_amount <= owed, Error::::CannotRepayMoreThanBorrowAmount); @@ -1065,31 +1022,33 @@ pub mod pallet { } } + impl DeFiEngine for Pallet { + type MayBeAssetId = ::MayBeAssetId; + + type Balance = ::Balance; + + type AccountId = ::AccountId; + } + impl Lending for Pallet { - /// we are operating only on vault types, so restricted by these - type AssetId = ::AssetId; type VaultId = ::VaultId; - type AccountId = ::AccountId; - type Balance = T::Balance; - type MarketId = MarketIndex; - type BlockNumber = T::BlockNumber; - type GroupId = T::GroupId; + type LiquidationStrategyId = ::LiquidationStrategyId; fn create( - borrow_asset: Self::AssetId, - collateral_asset: Self::AssetId, - config_input: MarketConfigInput, - interest_rate_model: &InterestRateModel, + manager: Self::AccountId, + config_input: CreateInput, ) -> Result<(Self::MarketId, Self::VaultId), DispatchError> { ensure!( - config_input.collateral_factor > 1.into(), + config_input.updatable.collateral_factor > 1.into(), Error::::CollateralFactorIsLessOrEqualOne ); - let collateral_asset_supported = ::is_supported(collateral_asset)?; - let borrow_asset_supported = ::is_supported(borrow_asset)?; + let collateral_asset_supported = + ::is_supported(config_input.collateral_asset())?; + let borrow_asset_supported = + ::is_supported(config_input.borrow_asset())?; ensure!( collateral_asset_supported && borrow_asset_supported, Error::::AssetNotSupportedByOracle @@ -1108,13 +1067,13 @@ pub mod pallet { let borrow_asset_vault = T::Vault::create( Deposit::Existential, VaultConfig { - asset_id: borrow_asset, - reserved: config_input.reserved, - manager: config_input.manager.clone(), + asset_id: config_input.borrow_asset(), + reserved: config_input.reserved_factor(), + manager: manager.clone(), strategies: [( Self::account_id(&market_id), // Borrowable = 100% - reserved - Perquintill::one().saturating_sub(config_input.reserved), + Perquintill::one().saturating_sub(config_input.reserved_factor()), )] .iter() .cloned() @@ -1123,19 +1082,21 @@ pub mod pallet { )?; let config = MarketConfig { - manager: config_input.manager, + manager, borrow: borrow_asset_vault.clone(), - collateral: collateral_asset, - collateral_factor: config_input.collateral_factor, - interest_rate_model: *interest_rate_model, - under_collaterized_warn_percent: config_input.under_collaterized_warn_percent, - liquidator: config_input.liquidator, + collateral: config_input.collateral_asset(), + collateral_factor: config_input.updatable.collateral_factor, + interest_rate_model: config_input.updatable.interest_rate_model, + under_collaterized_warn_percent: config_input + .updatable + .under_collaterized_warn_percent, + liquidators: config_input.updatable.liquidators, }; let debt_asset_id = T::CurrencyFactory::create()?; DebtMarkets::::insert(market_id, debt_asset_id); Markets::::insert(market_id, config); - BorrowIndex::::insert(market_id, Ratio::one()); + BorrowIndex::::insert(market_id, ZeroToOneFixedU128::one()); Ok((market_id, borrow_asset_vault)) }) @@ -1170,14 +1131,7 @@ pub mod pallet { let borrow_asset = T::Vault::asset_id(&market.borrow)?; let market_account = Self::account_id(market_id); - Self::can_borrow( - market_id, - debt_owner, - amount_to_borrow, - borrow_asset, - market, - &market_account, - )?; + Self::can_borrow(market_id, debt_owner, amount_to_borrow, market, &market_account)?; let new_account_interest_index = Self::updated_account_interest_index(market_id, debt_owner, amount_to_borrow)?; @@ -1199,11 +1153,11 @@ pub mod pallet { market_id: &Self::MarketId, from: &Self::AccountId, beneficiary: &Self::AccountId, - repay_amount: Option>, + total_repay_amount: Option>, ) -> Result<(), DispatchError> { let market = Self::get_market(market_id)?; if let Some(owed) = Self::borrow_balance_current(market_id, beneficiary)? { - let repay_amount = repay_amount.unwrap_or(owed); + let total_repay_amount = total_repay_amount.unwrap_or(owed); let borrow_asset_id = T::Vault::asset_id(&market.borrow)?; let market_account = Self::account_id(market_id); @@ -1211,7 +1165,7 @@ pub mod pallet { market_id, from, beneficiary, - repay_amount, + total_repay_amount, owed, borrow_asset_id, &market_account, @@ -1219,9 +1173,8 @@ pub mod pallet { let debt_asset_id = DebtMarkets::::get(market_id); - let burn_amount: u128 = - ::Currency::balance(debt_asset_id, beneficiary).into(); - let total_repay_amount: u128 = repay_amount.into(); + let burn_amount = ::Currency::balance(debt_asset_id, beneficiary); + let mut remaining_borrow_amount = T::MarketDebtCurrency::balance(debt_asset_id, &market_account); if total_repay_amount <= burn_amount { @@ -1256,12 +1209,12 @@ pub mod pallet { borrow_asset_id, from, &market_account, - repay_amount, + total_repay_amount, false, ) .expect("must be able to transfer because of above checks"); - if remaining_borrow_amount == 0 { + if remaining_borrow_amount == T::Balance::zero() { BorrowTimestamp::::remove(market_id, beneficiary); DebtIndex::::remove(market_id, beneficiary); } @@ -1275,15 +1228,15 @@ pub mod pallet { let accrued_debt = T::MarketDebtCurrency::balance(debt_asset_id, &Self::account_id(market_id)); let total_issued = T::MarketDebtCurrency::total_issuance(debt_asset_id); - let total_borrows = (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(); - Ok((total_borrows as u64).into()) + let total_borrows = total_issued - accrued_debt; + Ok(total_borrows) } fn total_interest(market_id: &Self::MarketId) -> Result { let debt_asset_id = DebtMarkets::::get(market_id); let total_interest = T::MarketDebtCurrency::balance(debt_asset_id, &Self::account_id(market_id)); - Ok(((total_interest / LiftedFixedBalance::accuracy()) as u64).into()) + Ok(total_interest) } fn total_cash(market_id: &Self::MarketId) -> Result { @@ -1293,12 +1246,12 @@ pub mod pallet { } fn calc_utilization_ratio( - cash: &Self::Balance, - borrows: &Self::Balance, + cash: Self::Balance, + borrows: Self::Balance, ) -> Result { - Ok(composable_traits::lending::math::calc_utilization_ratio( - (*cash).into(), - (*borrows).into(), + Ok(math::calc_utilization_ratio( + LiftedFixedBalance::saturating_from_integer(cash.into()), + LiftedFixedBalance::saturating_from_integer(borrows.into()), )?) } @@ -1313,7 +1266,7 @@ pub mod pallet { let total_borrows = Self::total_borrows(market_id)?; let total_cash = Self::total_cash(market_id)?; - let utilization_ratio = Self::calc_utilization_ratio(&total_cash, &total_borrows)?; + let utilization_ratio = Self::calc_utilization_ratio(total_cash, total_borrows)?; let mut market = Self::get_market(market_id)?; let delta_time = now.checked_sub(Self::last_block_timestamp()).ok_or(Error::::Underflow)?; @@ -1325,7 +1278,7 @@ pub mod pallet { &mut market.interest_rate_model, borrow_index, delta_time, - total_borrows.into(), + total_borrows, )?; BorrowIndex::::insert(market_id, borrow_index_new); @@ -1348,7 +1301,7 @@ pub mod pallet { let market_interest_index = Self::get_borrow_index(market_id)?; let balance = borrow_from_principal::( - ((principal / LiftedFixedBalance::accuracy()) as u64).into(), + principal, market_interest_index, account_interest_index, )?; @@ -1375,7 +1328,8 @@ pub mod pallet { let market = Self::get_market(market_id)?; let borrow_asset = T::Vault::asset_id(&market.borrow)?; let borrow_amount_value = Self::get_price(borrow_asset, borrow_amount)?; - Ok(swap_back(borrow_amount_value.into(), &market.collateral_factor)? + Ok(LiftedFixedBalance::saturating_from_integer(borrow_amount_value.into()) + .safe_mul(&market.collateral_factor)? .checked_mul_int(1_u64) .ok_or(ArithmeticError::Overflow)? .into()) @@ -1510,14 +1464,14 @@ pub mod pallet { /// Rather than failing the calculation with a division by 0, we immediately return 0 in /// this case. fn borrow_from_principal( - principal: ::Balance, - market_interest_index: Ratio, - account_interest_index: Ratio, + principal: ::Balance, + market_interest_index: ZeroToOneFixedU128, + account_interest_index: ZeroToOneFixedU128, ) -> Result, DispatchError> { if principal.is_zero() { return Ok(None) } - let principal: LiftedFixedBalance = principal.into(); + let principal = LiftedFixedBalance::saturating_from_integer(principal.into()); let balance = principal .checked_mul(&market_interest_index) .and_then(|from_start_total| from_start_total.checked_div(&account_interest_index)) @@ -1530,37 +1484,32 @@ pub mod pallet { pub fn swap( collateral_balance: &LiftedFixedBalance, collateral_price: &LiftedFixedBalance, - collateral_factor: &NormalizedCollateralFactor, + collateral_factor: &MoreThanOneFixedU128, ) -> Result { collateral_balance.safe_mul(collateral_price)?.safe_div(collateral_factor) } - pub fn swap_back( - borrow_balance_value: LiftedFixedBalance, - collateral_factor: &NormalizedCollateralFactor, - ) -> Result { - borrow_balance_value.safe_mul(collateral_factor) - } - pub fn accrue_interest_internal( utilization_ratio: Percent, interest_rate_model: &mut I, - borrow_index: Rate, + borrow_index: OneOrMoreFixedU128, delta_time: DurationSeconds, - total_borrows: u128, - ) -> Result<(u128, Rate), DispatchError> { + total_borrows: T::Balance, + ) -> Result<(T::Balance, Rate), DispatchError> { let borrow_rate = interest_rate_model .get_borrow_rate(utilization_ratio) .ok_or(Error::::BorrowRateDoesNotExist)?; let borrow_index_new = increment_index(borrow_rate, borrow_index, delta_time)?; let delta_interest_rate = borrow_rate .safe_mul(&FixedU128::saturating_from_integer(delta_time))? - .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))?; - - let total_borrows = total_borrows.safe_mul(&LiftedFixedBalance::accuracy())?; - let accrue_increment = LiftedFixedBalance::from_inner(total_borrows) - .safe_mul(&delta_interest_rate)? - .into_inner(); + .safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))?; + let total_borrows: FixedU128 = + FixedU128::checked_from_integer(Into::::into(total_borrows)) + .ok_or(ArithmeticError::Overflow)?; + let accrue_increment = + total_borrows.safe_mul(&delta_interest_rate)?.into_inner() / LiftedFixedBalance::DIV; + let accrue_increment = + accrue_increment.try_into().map_err(|_| ArithmeticError::Overflow)?; Ok((accrue_increment, borrow_index_new)) } } diff --git a/frame/lending/src/mocks/mod.rs b/frame/lending/src/mocks/mod.rs index 5700c5eaa6a..26baa6a9c90 100644 --- a/frame/lending/src/mocks/mod.rs +++ b/frame/lending/src/mocks/mod.rs @@ -1,6 +1,6 @@ use crate::{self as pallet_lending, *}; use composable_traits::{ - currency::{DynamicCurrencyId, Exponent}, + currency::DynamicCurrencyId, defi::DeFiComposableConfig, governance::{GovernanceRegistry, SignedRawOrigin}, }; @@ -31,9 +31,17 @@ type Block = frame_system::mocking::MockBlock; pub type Balance = u128; pub type Amount = i128; pub type BlockNumber = u64; - pub type VaultId = u64; +pub type LiquidationStrategyId = u32; +pub type OrderId = u32; + +parameter_types! { + pub const LiquidationsPalletId : PalletId = PalletId(*b"liqd_tns"); +} + +pub type ParachainId = u32; + pub const MINIMUM_BALANCE: Balance = 1000; pub static ALICE: Lazy = Lazy::new(|| { @@ -318,7 +326,7 @@ impl pallet_dutch_auction::weights::WeightInfo for DutchAuctionsMocks { } impl frame_support::weights::WeightToFeePolynomial for DutchAuctionsMocks { - type Balance = u128; + type Balance = Balance; fn polynomial() -> frame_support::weights::WeightToFeeCoefficients { todo!("will replace with mocks from relevant pallet") @@ -327,7 +335,7 @@ impl frame_support::weights::WeightToFeePolynomial for DutchAuctionsMocks { impl pallet_dutch_auction::Config for Test { type Event = Event; - type OrderId = u128; + type OrderId = OrderId; type UnixTime = Timestamp; type MultiCurrency = Assets; type WeightInfo = DutchAuctionsMocks; @@ -339,8 +347,12 @@ impl pallet_dutch_auction::Config for Test { impl pallet_liquidations::Config for Test { type Event = Event; type UnixTime = Timestamp; - type Lending = Lending; - type GroupId = AccountId; + type DutchAuction = DutchAuction; + type LiquidationStrategyId = LiquidationStrategyId; + type OrderId = OrderId; + type PalletId = LiquidationsPalletId; + type WeightInfo = (); + type ParachainId = ParachainId; } pub type Extrinsic = TestXt; @@ -382,8 +394,6 @@ impl pallet_lending::Config for Test { type VaultId = VaultId; type Vault = Vault; type Event = Event; - type AssetId = MockCurrencyId; - type Balance = Balance; type Currency = Tokens; type CurrencyFactory = LpTokenFactory; type MarketDebtCurrency = Tokens; @@ -392,7 +402,7 @@ impl pallet_lending::Config for Test { type MaxLendingCount = MaxLendingCount; type AuthorityId = crypto::TestAuthId; type WeightInfo = (); - type GroupId = AccountId; + type LiquidationStrategyId = LiquidationStrategyId; } // Build genesis storage according to the mock runtime. diff --git a/frame/lending/src/models.rs b/frame/lending/src/models.rs index 72cc19ea912..c6fff257bdc 100644 --- a/frame/lending/src/models.rs +++ b/frame/lending/src/models.rs @@ -1,28 +1,32 @@ use composable_traits::{ - defi::Rate, - lending::math::NormalizedCollateralFactor, - math::{LiftedFixedBalance, SafeArithmetic}, + currency::MathBalance, + defi::{LiftedFixedBalance, MoreThanOneFixedU128, Rate}, + math::SafeArithmetic, }; -use sp_runtime::{traits::Saturating, ArithmeticError, Percent}; +use sp_runtime::{traits::Saturating, ArithmeticError, FixedPointNumber, Percent}; pub struct BorrowerData { pub collateral_balance_value: LiftedFixedBalance, pub borrow_balance_value: LiftedFixedBalance, - pub collateral_factor: NormalizedCollateralFactor, + pub collateral_factor: MoreThanOneFixedU128, pub under_collaterized_warn_percent: Percent, } impl BorrowerData { #[inline(always)] - pub fn new>( + pub fn new( collateral_balance_value: T, borrow_balance_value: T, - collateral_factor: NormalizedCollateralFactor, + collateral_factor: MoreThanOneFixedU128, under_collaterized_warn_percent: Percent, ) -> Self { Self { - collateral_balance_value: collateral_balance_value.into(), - borrow_balance_value: borrow_balance_value.into(), + collateral_balance_value: LiftedFixedBalance::saturating_from_integer( + collateral_balance_value.into(), + ), + borrow_balance_value: LiftedFixedBalance::saturating_from_integer( + borrow_balance_value.into(), + ), collateral_factor, under_collaterized_warn_percent, } @@ -52,7 +56,7 @@ impl BorrowerData { } #[inline(always)] - pub fn safe_collateral_factor(&self) -> Result { + pub fn safe_collateral_factor(&self) -> Result { self.collateral_factor.safe_add( &self.collateral_factor.safe_mul(&self.under_collaterized_warn_percent.into())?, ) diff --git a/frame/lending/src/tests.rs b/frame/lending/src/tests.rs index a062561f054..402a066bbce 100644 --- a/frame/lending/src/tests.rs +++ b/frame/lending/src/tests.rs @@ -3,29 +3,29 @@ use std::ops::Mul; use crate::{ accrue_interest_internal, mocks::{ - new_test_ext, process_block, AccountId, Balance, BlockNumber, Lending, LpTokenFactory, - MockCurrencyId, Oracle, Origin, Test, Tokens, Vault, VaultId, ALICE, BOB, CHARLIE, - MILLISECS_PER_BLOCK, MINIMUM_BALANCE, UNRESERVED, + new_test_ext, process_block, AccountId, Balance, BlockNumber, Lending, MockCurrencyId, + Oracle, Origin, Test, Tokens, Vault, VaultId, ALICE, BOB, CHARLIE, MILLISECS_PER_BLOCK, + MINIMUM_BALANCE, UNRESERVED, }, models::BorrowerData, Error, MarketIndex, }; use composable_tests_helpers::{prop_assert_acceptable_computation_error, prop_assert_ok}; use composable_traits::{ - currency::{CurrencyFactory, LocalAssets}, - defi::Rate, - lending::{math::*, MarketConfigInput}, - math::LiftedFixedBalance, + defi::{CurrencyPair, LiftedFixedBalance, MoreThanOneFixedU128, Rate, ZeroToOneFixedU128}, + lending::{math::*, CreateInput, UpdateInput}, + time::SECONDS_PER_YEAR_NAIVE, vault::{Deposit, VaultConfig}, }; use frame_support::{ - assert_noop, assert_ok, + assert_err, assert_noop, assert_ok, traits::fungibles::{Inspect, Mutate}, }; use pallet_vault::models::VaultInfo; use proptest::{prelude::*, test_runner::TestRunner}; +use sp_arithmetic::assert_eq_error_rate; use sp_core::U256; -use sp_runtime::{FixedPointNumber, Percent, Perquintill}; +use sp_runtime::{ArithmeticError, FixedPointNumber, Percent, Perquintill}; type BorrowAssetVault = VaultId; @@ -57,20 +57,19 @@ fn create_market( collateral_asset: MockCurrencyId, manager: AccountId, reserved: Perquintill, - collateral_factor: NormalizedCollateralFactor, + collateral_factor: MoreThanOneFixedU128, ) -> (MarketIndex, BorrowAssetVault) { - let market_config = MarketConfigInput { - liquidator: None, - manager, - reserved, - collateral_factor, - under_collaterized_warn_percent: Percent::from_float(0.10), + let config = CreateInput { + updatable: UpdateInput { + collateral_factor, + under_collaterized_warn_percent: Percent::from_float(0.10), + liquidators: vec![], + interest_rate_model: InterestRateModel::default(), + }, + reserved_factor: reserved, + currency_pair: CurrencyPair::new(collateral_asset, borrow_asset), }; - let interest_rate_model = InterestRateModel::default(); - let market = - Lending::create(borrow_asset, collateral_asset, market_config, &interest_rate_model); - assert_ok!(market); - market.expect("unreachable; qed;") + ::create(manager, config).unwrap() } /// Create a market with a USDT vault LP token as collateral @@ -83,7 +82,7 @@ fn create_simple_vaulted_market() -> ((MarketIndex, BorrowAssetVault), Collatera collateral_asset, *ALICE, DEFAULT_MARKET_VAULT_RESERVE, - NormalizedCollateralFactor::saturating_from_rational(200, 100), + MoreThanOneFixedU128::saturating_from_rational(200, 100), ), collateral_asset, ) @@ -96,20 +95,31 @@ fn create_simple_market() -> (MarketIndex, BorrowAssetVault) { MockCurrencyId::USDT, *ALICE, DEFAULT_MARKET_VAULT_RESERVE, - NormalizedCollateralFactor::saturating_from_rational(DEFAULT_COLLATERAL_FACTOR * 100, 100), + MoreThanOneFixedU128::saturating_from_rational(DEFAULT_COLLATERAL_FACTOR * 100, 100), ) } +/// some model with sane parameter +fn new_jump_model() -> (Percent, InterestRateModel) { + let base_rate = Rate::saturating_from_rational(2, 100); + let jump_rate = Rate::saturating_from_rational(10, 100); + let full_rate = Rate::saturating_from_rational(32, 100); + let optimal = Percent::from_percent(80); + let interest_rate_model = + InterestRateModel::Jump(JumpModel::new(base_rate, jump_rate, full_rate, optimal).unwrap()); + (optimal, interest_rate_model) +} + #[test] fn accrue_interest_base_cases() { let (optimal, ref mut interest_rate_model) = new_jump_model(); let stable_rate = interest_rate_model.get_borrow_rate(optimal).unwrap(); - assert_eq!(stable_rate, Ratio::saturating_from_rational(10, 100)); - let borrow_index = Rate::saturating_from_integer(1); - let delta_time = SECONDS_PER_YEAR; + assert_eq!(stable_rate, ZeroToOneFixedU128::saturating_from_rational(10_u128, 100_u128)); + let borrow_index = Rate::saturating_from_integer(1_u128); + let delta_time = SECONDS_PER_YEAR_NAIVE; let total_issued = 100_000_000_000_000_000_000; let accrued_debt = 0; - let total_borrows = (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(); + let total_borrows = total_issued - accrued_debt; let (accrued_increase, _) = accrue_interest_internal::( optimal, interest_rate_model, @@ -134,51 +144,55 @@ fn accrue_interest_base_cases() { let error = 25; assert_eq!( accrued_increase, - 10_000_000_000_000_000_000 * MILLISECS_PER_BLOCK as u128 / SECONDS_PER_YEAR as u128 + error + 10_000_000_000_000_000_000 * MILLISECS_PER_BLOCK as u128 / SECONDS_PER_YEAR_NAIVE as u128 + + error ); } #[test] -fn accrue_interest_edge_cases() { +fn apr_for_zero() { let (_, ref mut interest_rate_model) = new_jump_model(); let utilization = Percent::from_percent(100); - let borrow_index = Rate::saturating_from_integer(1); - let delta_time = SECONDS_PER_YEAR; - let total_issued = u128::MAX; - let accrued_debt = 0; - let total_borrows = (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(); + let borrow_index = Rate::saturating_from_integer(1_u128); + let (accrued_increase, _) = accrue_interest_internal::( utilization, interest_rate_model, borrow_index, - delta_time, - total_borrows, + SECONDS_PER_YEAR_NAIVE, + 0, ) .unwrap(); - assert_eq!(accrued_increase, 108890357414700308308160000000000000000); + assert_eq!(accrued_increase, 0); +} - let (accrued_increase, _) = accrue_interest_internal::( +#[test] +fn apr_for_year_for_max() { + let (_, ref mut interest_rate_model) = new_jump_model(); + let utilization = Percent::from_percent(80); + let borrow_index = Rate::saturating_from_integer(1_u128); + let total_borrows = u128::MAX; + let result = accrue_interest_internal::( utilization, interest_rate_model, borrow_index, - delta_time, - 0, - ) - .unwrap(); - assert_eq!(accrued_increase, 0); + SECONDS_PER_YEAR_NAIVE, + total_borrows, + ); + assert_err!(result, ArithmeticError::Overflow); } #[test] fn accrue_interest_induction() { - let borrow_index = Rate::saturating_from_integer(1); - let minimal = 18; // current precision and minimal time delta do not allow to accrue on less than this power of 10 + let borrow_index = Rate::saturating_from_integer(1_u128); + let minimal: u128 = 100; let mut runner = TestRunner::default(); - let accrued_debt = 0; + let accrued_debt: u128 = 0; runner .run( &( - 0..=2 * SECONDS_PER_YEAR / MILLISECS_PER_BLOCK, - (minimal..=35_u32).prop_map(|i| 10_u128.pow(i)), + 0..=2 * SECONDS_PER_YEAR_NAIVE / MILLISECS_PER_BLOCK, + (minimal..=minimal * 1_000_000_000), ), |(slot, total_issued)| { let (optimal, ref mut interest_rate_model) = new_jump_model(); @@ -188,7 +202,7 @@ fn accrue_interest_induction() { interest_rate_model, borrow_index, slot * MILLISECS_PER_BLOCK, - (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(), + total_issued - accrued_debt, ) .unwrap(); let (accrued_increase_2, borrow_index_2) = @@ -197,7 +211,7 @@ fn accrue_interest_induction() { interest_rate_model, borrow_index, (slot + 1) * MILLISECS_PER_BLOCK, - (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(), + total_issued - accrued_debt, ) .unwrap(); prop_assert!(accrued_increase_1 < accrued_increase_2); @@ -211,13 +225,14 @@ fn accrue_interest_induction() { #[test] fn accrue_interest_plotter() { let (optimal, ref mut interest_rate_model) = new_jump_model(); - let borrow_index = Rate::checked_from_integer(1).unwrap(); - let total_issued = 10_000_000; + let borrow_index = MoreThanOneFixedU128::checked_from_integer(1).unwrap(); + let total_issued = 10_000_000_000; let accrued_debt = 0; - let total_borrows = (total_issued - accrued_debt) / LiftedFixedBalance::accuracy(); + let total_borrows = total_issued - accrued_debt; // no sure how handle in rust previous + next (so map has access to previous result) let mut previous = 0; - let _data: Vec<_> = (0..=1000) + const TOTAL_BLOCKS: u64 = 1000; + let _data: Vec<_> = (0..TOTAL_BLOCKS) .map(|x| { let (accrue_increment, _) = accrue_interest_internal::( optimal, @@ -236,11 +251,11 @@ fn accrue_interest_plotter() { optimal, interest_rate_model, Rate::checked_from_integer(1).unwrap(), - 1000 * MILLISECS_PER_BLOCK, + TOTAL_BLOCKS * MILLISECS_PER_BLOCK, total_borrows, ) .unwrap(); - assert_eq!(previous, total_accrued); + assert_eq_error_rate!(previous, total_accrued, 1_000); #[cfg(feature = "visualization")] { @@ -316,35 +331,23 @@ fn test_borrow_repay_in_same_block() { }); } -/// some model with sane parameter -fn new_jump_model() -> (Percent, InterestRateModel) { - let base_rate = Rate::saturating_from_rational(2, 100); - let jump_rate = Rate::saturating_from_rational(10, 100); - let full_rate = Rate::saturating_from_rational(32, 100); - let optimal = Percent::from_percent(80); - let interest_rate_model = - InterestRateModel::Jump(JumpModel::new(base_rate, jump_rate, full_rate, optimal).unwrap()); - (optimal, interest_rate_model) -} - #[test] fn test_calc_utilization_ratio() { // 50% borrow - assert_eq!(Lending::calc_utilization_ratio(&1, &1).unwrap(), Percent::from_percent(50)); - assert_eq!(Lending::calc_utilization_ratio(&1, &1).unwrap(), Percent::from_float(0.50)); - assert_eq!(Lending::calc_utilization_ratio(&100, &100).unwrap(), Percent::from_percent(50)); + assert_eq!(Lending::calc_utilization_ratio(1, 1).unwrap(), Percent::from_percent(50)); + assert_eq!(Lending::calc_utilization_ratio(100, 100).unwrap(), Percent::from_percent(50)); // no borrow - assert_eq!(Lending::calc_utilization_ratio(&1, &0).unwrap(), Percent::zero()); + assert_eq!(Lending::calc_utilization_ratio(1, 0).unwrap(), Percent::zero()); // full borrow - assert_eq!(Lending::calc_utilization_ratio(&0, &1).unwrap(), Percent::from_percent(100)); + assert_eq!(Lending::calc_utilization_ratio(0, 1).unwrap(), Percent::from_percent(100)); } #[test] fn test_borrow_math() { let borrower = BorrowerData::new( - 100, + 100_u128, 0, - NormalizedCollateralFactor::from_float(1.0), + MoreThanOneFixedU128::from_float(1.0), Percent::from_float(0.10), ); let borrow = borrower.borrow_for_collateral().unwrap(); @@ -405,6 +408,7 @@ fn borrow_flow() { process_block(i); } let interest_after = Lending::total_interest_accurate(&market).unwrap(); + assert!(interest_before < interest_after); let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); let new_limit = limit_normalized / price(MockCurrencyId::BTC, 1); @@ -582,7 +586,7 @@ fn test_liquidation() { MockCurrencyId::BTC, *ALICE, Perquintill::from_percent(10), - NormalizedCollateralFactor::saturating_from_rational(2, 1), + MoreThanOneFixedU128::saturating_from_rational(2, 1), ); Oracle::set_btc_price(100); @@ -632,7 +636,7 @@ fn test_warn_soon_under_collaterized() { MockCurrencyId::BTC, *ALICE, Perquintill::from_percent(10), - NormalizedCollateralFactor::saturating_from_rational(2, 1), + MoreThanOneFixedU128::saturating_from_rational(2, 1), ); // 1 BTC = 100 USDT @@ -778,7 +782,7 @@ proptest! { let borrower = BorrowerData::new( collateral_balance * collateral_price, borrower_balance_with_interest * borrow_price, - NormalizedCollateralFactor::from_float(1.0), + MoreThanOneFixedU128::from_float(1.0), Percent::from_float(0.10), // 10% ); let borrow = borrower.borrow_for_collateral(); @@ -842,7 +846,7 @@ proptest! { #[test] fn calc_utilization_ratio_proptest((cash, borrow) in valid_cash_borrow()) { new_test_ext().execute_with(|| { - prop_assert_eq!(Lending::calc_utilization_ratio(&cash.into(), &borrow.into()).unwrap(), Percent::from_float(borrow as f64 / (cash as f64 + borrow as f64))); + prop_assert_eq!(Lending::calc_utilization_ratio(cash.into(), borrow.into()).unwrap(), Percent::from_float(borrow as f64 / (cash as f64 + borrow as f64))); Ok(()) })?; } @@ -866,7 +870,7 @@ proptest! { lp_token_id, *ALICE, Perquintill::from_percent(10), - NormalizedCollateralFactor::saturating_from_rational(200, 100), + MoreThanOneFixedU128::saturating_from_rational(200, 100), ); // Top level lp price should be transitively resolvable to the base asset price. diff --git a/frame/liquidations/src/lib.rs b/frame/liquidations/src/lib.rs index 4a27db5b8a5..32f43d42cc0 100644 --- a/frame/liquidations/src/lib.rs +++ b/frame/liquidations/src/lib.rs @@ -32,24 +32,32 @@ unused_extern_crates )] +mod weights; + pub use pallet::*; #[frame_support::pallet] pub mod pallet { - use codec::FullCodec; + use codec::{Decode, Encode, FullCodec}; use composable_traits::{ - defi::DeFiComposableConfig, lending::Lending, liquidation::Liquidation, - loans::PriceStructure, + defi::{DeFiComposableConfig, DeFiEngine, Sell, SellEngine}, + liquidation::Liquidation, + math::WrappingNext, + time::{LinearDecrease, StairstepExponentialDecrease, TimeReleaseFunction}, }; use frame_support::{ - traits::{IsType, UnixTime}, - PalletId, + dispatch::DispatchResultWithPostInfo, + pallet_prelude::{OptionQuery, StorageMap, StorageValue}, + traits::{GenesisBuild, Get, IsType, UnixTime}, + PalletId, Parameter, Twox64Concat, }; - use sp_runtime::DispatchError; + use frame_system::pallet_prelude::OriginFor; + use scale_info::TypeInfo; + use sp_runtime::{DispatchError, Permill, Perquintill}; - pub const PALLET_ID: PalletId = PalletId(*b"Liqudati"); + use crate::weights::WeightInfo; #[pallet::config] @@ -58,9 +66,24 @@ pub mod pallet { type UnixTime: UnixTime; - type Lending: Lending; + type DutchAuction: SellEngine< + TimeReleaseFunction, + OrderId = Self::OrderId, + MayBeAssetId = ::MayBeAssetId, + Balance = Self::Balance, + AccountId = Self::AccountId, + >; + + type LiquidationStrategyId: Default + FullCodec + WrappingNext + Parameter + Copy; + + type OrderId: Default + FullCodec; - type GroupId: Default + FullCodec; + type PalletId: Get; + + // /// when called, engine pops latest order to liquidate and pushes back result + // type Liquidate: Parameter + Dispatchable + From>; + type WeightInfo: WeightInfo; + type ParachainId: FullCodec + Default + Parameter + Clone; } #[pallet::event] @@ -68,37 +91,152 @@ pub mod pallet { pub enum Event { PositionWasSentToLiquidation {}, } + #[pallet::error] - pub enum Error {} + pub enum Error { + NoLiquidationEngineFound, + } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); #[pallet::call] - impl Pallet {} + impl Pallet { + #[pallet::weight(T::WeightInfo::add_liquidation_strategy())] + pub fn add_liqudation_strategy( + _origin: OriginFor, + _configuraiton: LiquidationStrategyConfiguration, + ) -> DispatchResultWithPostInfo { + Err(DispatchError::Other("no implemented").into()) + } + } - impl Liquidation for Pallet { - type AssetId = T::MayBeAssetId; + #[pallet::storage] + #[pallet::getter(fn strategies)] + pub type Strategies = StorageMap< + _, + Twox64Concat, + T::LiquidationStrategyId, + LiquidationStrategyConfiguration, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn strategy_index)] + #[allow(clippy::disallowed_type)] + pub type StrategyIndex = + StorageValue<_, T::LiquidationStrategyId, frame_support::pallet_prelude::ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn default_strategy_index)] + #[allow(clippy::disallowed_type)] + pub type DefaultStrategyIndex = + StorageValue<_, T::LiquidationStrategyId, frame_support::pallet_prelude::ValueQuery>; + + impl DeFiEngine for Pallet { + type MayBeAssetId = T::MayBeAssetId; type Balance = T::Balance; type AccountId = T::AccountId; + } - type LiquidationId = u128; + #[pallet::genesis_config] + pub struct GenesisConfig { + _phantom: sp_std::marker::PhantomData, + } - type GroupId = T::GroupId; + impl Default for GenesisConfig { + fn default() -> Self { + Self { _phantom: <_>::default() } + } + } + + impl Pallet { + pub fn create_strategy_id() -> T::LiquidationStrategyId { + StrategyIndex::::mutate(|x| { + *x = x.next(); + *x + }) + } + } + + #[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq)] + pub enum LiquidationStrategyConfiguration { + DutchAuction(TimeReleaseFunction), + UniswapV2 { slippage: Perquintill }, + XcmDex { parachain_id: ParachainId }, + /* Building fully decoupled flow is described bellow. Will avoid that for now. + * ```plantuml + * `solves question - how pallet can invoke list of other pallets with different configuration types + * `so yet sharing some liquidation part and tracing liquidation id + * dutch_auction_strategy -> liquidation : Create new strategy id + * dutch_auction_strategy -> liquidation : Add Self Dispatchable call (baked with strategyid) + * liquidation -> liquidation: Add liquidation order + * liquidation -> liquidation: Get Dispatchable by Strategyid + * liquidation --> dutch_auction_strategy: Invoke Dispatchable + * dutch_auction_strategy -> dutch_auction_strategy: Get liquidation configuration by id previosly baked into call + * dutch_auction_strategy --> liquidation: Pop next order + * dutch_auction_strategy -> dutch_auction_strategy: Start liqudaiton + * ``` + *Dynamic { liquidate: Dispatch, minimum_price: Balance }, */ + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + let index = Pallet::::create_strategy_id(); + DefaultStrategyIndex::::set(index); + let linear_ten_minutes = LiquidationStrategyConfiguration::DutchAuction( + TimeReleaseFunction::LinearDecrease(LinearDecrease { total: 10 * 60 }), + ); + Strategies::::insert(index, linear_ten_minutes); + + let index = Pallet::::create_strategy_id(); + let exponential = + StairstepExponentialDecrease { step: 10, cut: Permill::from_rational(95_u32, 100) }; + let exponential = LiquidationStrategyConfiguration::DutchAuction( + TimeReleaseFunction::StairstepExponentialDecrease(exponential), + ); + Strategies::::insert(index, exponential); + } + } + + impl Liquidation for Pallet { + type LiquidationStrategyId = T::LiquidationStrategyId; + type OrderId = T::OrderId; fn liquidate( - _source_account: &Self::AccountId, - _source_asset_id: Self::AssetId, - _source_asset_price: PriceStructure, - _target_asset_id: Self::AssetId, - _target_account: &Self::AccountId, - _total_amount: Self::Balance, - ) -> Result { - Self::deposit_event(Event::::PositionWasSentToLiquidation {}); - Err(DispatchError::Other("todo")) + from_to: &Self::AccountId, + order: Sell, + configuration: Vec, + ) -> Result { + let mut configuration = configuration; + if configuration.is_empty() { + configuration.push(DefaultStrategyIndex::::get()) + }; + + for id in configuration { + let configuration = Strategies::::get(id); + if let Some(configuration) = configuration { + let result = match configuration { + LiquidationStrategyConfiguration::DutchAuction(configuration) => + T::DutchAuction::ask(from_to, order.clone(), configuration), + _ => + return Err(DispatchError::Other( + "as for now, only auction liquidators implemented", + )), + }; + + if result.is_ok() { + Self::deposit_event(Event::::PositionWasSentToLiquidation {}); + return result + } + } + } + + Err(Error::::NoLiquidationEngineFound.into()) } } } diff --git a/frame/liquidations/src/weights.rs b/frame/liquidations/src/weights.rs new file mode 100644 index 00000000000..d494a03f594 --- /dev/null +++ b/frame/liquidations/src/weights.rs @@ -0,0 +1,9 @@ +use frame_support::dispatch::Weight; + +pub trait WeightInfo { + fn add_liquidation_strategy() -> Weight { + 10000 + } +} + +impl WeightInfo for () {} diff --git a/frame/oracle/src/lib.rs b/frame/oracle/src/lib.rs index 326e1a858ff..79e56bc34fb 100644 --- a/frame/oracle/src/lib.rs +++ b/frame/oracle/src/lib.rs @@ -53,6 +53,7 @@ pub mod pallet { pallet_prelude::*, Config as SystemConfig, }; + use lite_json::json::JsonValue; use scale_info::TypeInfo; use sp_core::crypto::KeyTypeId; diff --git a/frame/uniswap-v2/src/lib.rs b/frame/uniswap-v2/src/lib.rs index c520d9776ac..7e678023762 100644 --- a/frame/uniswap-v2/src/lib.rs +++ b/frame/uniswap-v2/src/lib.rs @@ -48,9 +48,8 @@ pub mod pallet { use codec::{Codec, FullCodec}; use composable_traits::{ currency::CurrencyFactory, - defi::CurrencyPair, + defi::{CurrencyPair, LiftedFixedBalance}, dex::{ConstantProductPoolInfo, CurveAmm}, - math::LiftedFixedBalance, }; use frame_support::{ pallet_prelude::*,