From d3bd194b5152ffbae3ce7d36a1a79577c29f8ee4 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Tue, 11 Jan 2022 23:03:33 +0200 Subject: [PATCH 1/9] oracle api clarification Signed-off-by: Dzmitry Lahoda --- Cargo.lock | 34 +++-- frame/composable-traits/src/currency.rs | 42 +++--- frame/composable-traits/src/lending/mod.rs | 2 +- frame/composable-traits/src/oracle.rs | 57 ++++--- frame/currency-factory/README.md | 0 frame/currency-factory/src/lib.rs | 11 +- frame/dutch-auction/src/mock/currency.rs | 15 +- frame/lending/src/lib.rs | 57 ++++--- frame/lending/src/mocks/mod.rs | 15 +- frame/lending/src/mocks/oracle.rs | 15 +- frame/lending/src/tests.rs | 166 ++++++++++----------- frame/oracle/src/lib.rs | 29 +++- frame/oracle/src/mock.rs | 1 + runtime/common/src/impls.rs | 1 - runtime/composable/src/lib.rs | 1 - runtime/dali/src/lib.rs | 2 +- runtime/picasso/src/lib.rs | 1 - runtime/primitives/src/currency.rs | 77 ++-------- 18 files changed, 245 insertions(+), 281 deletions(-) delete mode 100644 frame/currency-factory/README.md diff --git a/Cargo.lock b/Cargo.lock index 838c66f2f1d..019faf7b661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1276,9 +1276,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.1" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1121e32687f7f90b905d4775273305baa4f32cd418923e9b0fa726533221857" +checksum = "1957aa4a5fb388f0a0a73ce7556c5b42025b874e5cdc2c670775e346e97adec0" dependencies = [ "atty", "bitflags", @@ -1293,11 +1293,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.0.0" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" +checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2 1.0.36", "quote 1.0.14", @@ -2701,7 +2701,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2 1.0.36", "quote 1.0.14", "syn 1.0.84", @@ -3714,6 +3714,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -9253,7 +9259,7 @@ version = "0.1.0" dependencies = [ "binance", "chrono", - "clap 3.0.1", + "clap 3.0.6", "custom_derive", "enum_derive", "env_logger 0.9.0", @@ -9447,7 +9453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes 1.1.0", - "heck", + "heck 0.3.3", "itertools", "lazy_static", "log 0.4.14", @@ -11631,9 +11637,9 @@ dependencies = [ [[package]] name = "signal-hook-tokio" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" dependencies = [ "futures-core", "libc", @@ -12659,7 +12665,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro-error", "proc-macro2 1.0.36", "quote 1.0.14", @@ -12681,7 +12687,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2 1.0.36", "quote 1.0.14", "syn 1.0.84", @@ -12860,7 +12866,7 @@ dependencies = [ "async-trait", "darling", "frame-metadata", - "heck", + "heck 0.3.3", "parity-scale-codec", "proc-macro-crate 0.1.5", "proc-macro-error", @@ -12878,7 +12884,7 @@ dependencies = [ "async-trait", "darling", "frame-metadata", - "heck", + "heck 0.3.3", "parity-scale-codec", "proc-macro-crate 0.1.5", "proc-macro-error", diff --git a/frame/composable-traits/src/currency.rs b/frame/composable-traits/src/currency.rs index 7ff9b6d271e..fd1a838e52f 100644 --- a/frame/composable-traits/src/currency.rs +++ b/frame/composable-traits/src/currency.rs @@ -1,33 +1,12 @@ -use core::ops::Div; - use codec::FullCodec; use frame_support::pallet_prelude::*; use scale_info::TypeInfo; use sp_runtime::traits::AtLeast32BitUnsigned; use sp_std::fmt::Debug; +/// really u8, but easy to do math operations pub type Exponent = u32; -/// A asset that can be priced. -pub trait PriceableAsset -where - Self: Copy, -{ - fn decimals(&self) -> Exponent; - fn unit>(&self) -> T { - T::from(10_u64.pow(self.decimals())) - } - fn milli + Div>(&self) -> T { - self.unit::() / T::from(1000_u64) - } -} - -impl PriceableAsset for u128 { - fn decimals(&self) -> Exponent { - 0 - } -} - /* NOTE(hussein-aitlahcen): I initially added a generic type to index into the generatable sub-range but realised it was overkill. Perhaps it will be required later if we want to differentiate multiple sub-ranges @@ -46,10 +25,29 @@ where /// Creates a new asset, compatible with [`MultiCurrency`](https://docs.rs/orml-traits/0.4.0/orml_traits/currency/trait.MultiCurrency.html). /// The implementor should ensure that a new `CurrencyId` is created and collisions are avoided. +/// Is about Local assets representations. These may differ remotely. pub trait CurrencyFactory { fn create() -> Result; } +pub trait LocalAssets { + /// decimals of of big unit over minimal unit + /// ORML also has separate trait on Balances to inspect decimals, that is not on type it self + fn decimals(currency_id: MayBeAssetId) -> Result; + + fn unit>(currency_id: MayBeAssetId) -> Result { + let exponent = Self::decimals(currency_id)?; + Ok(10_u64.pow(exponent).into()) + } +} + +/// when we store assets in native form to chain in smallest units or for mock in tests +impl LocalAssets for () { + fn decimals(_currency_id: MayBeAssetId) -> Result { + Ok(0) + } +} + pub trait BalanceLike: AtLeast32BitUnsigned + FullCodec diff --git a/frame/composable-traits/src/lending/mod.rs b/frame/composable-traits/src/lending/mod.rs index 7c7a06c7969..0a26afa2960 100644 --- a/frame/composable-traits/src/lending/mod.rs +++ b/frame/composable-traits/src/lending/mod.rs @@ -192,7 +192,7 @@ pub trait Lending { borrow_amount: Self::Balance, ) -> Result; - /// Returns the borrow limit for an account. + /// Returns the borrow limit for an account in normalized price. /// Calculation uses indexes from start of block time. /// Depends on overall collateral put by user into vault. /// This borrow limit of specific user, depends only on prices and users collateral, not on diff --git a/frame/composable-traits/src/oracle.rs b/frame/composable-traits/src/oracle.rs index e75197b8839..af2c9e9f2e4 100644 --- a/frame/composable-traits/src/oracle.rs +++ b/frame/composable-traits/src/oracle.rs @@ -1,8 +1,8 @@ +use crate::{currency::LocalAssets, defi::CurrencyPair}; use frame_support::{dispatch::DispatchError, pallet_prelude::*}; +use sp_runtime::FixedU128; use sp_std::vec::Vec; -use crate::currency::PriceableAsset; - #[derive(Encode, Decode, Default, Debug, PartialEq)] pub struct Price { pub price: PriceValue, @@ -12,21 +12,30 @@ pub struct Price { /// An object that is able to provide an asset price. /// Important: the current price-feed is providing prices in USDT only. pub trait Oracle { - type AssetId: PriceableAsset; + type AssetId: Copy; type Balance: From; type Timestamp; - - /// Quote the `amount` of `asset` in USDT cent. - /// Error is returned if `asset` not supported or price information not available. - - /// Assuming we have a price `p` for an unit (not smallest) of `asset` in USDT cents. - /// Let `k` be the number of decimals for `asset`. - /// The price of an amount `a` of the smallest possible unit of `asset` is: - /// p * a / 10^k - /// e.g. for BTC, the price is expressed for 1 BTC, but the amount is in sats: + type LocalAssets: LocalAssets; + /// Quote the `amount` of `asset_id` in normalized currency unit cent. Default is USDT Cent. + /// Which is 0.01 of USDT. `Result::Err` is returned if `asset_id` not supported or price + /// information not available. + /// + /// # Normal assets + /// + /// Assuming we have a price `price` for an unit (not smallest) of `asset_id` in USDT cents. + /// Let `decimals` be the number of decimals for `asset_id` as given by + /// `CurrencyFactory::decimals` The price of an amount `amount` of the smallest possible unit of + /// `asset_id` is: `price * amount / 10^decimals` + /// + /// + /// E.g. for BTC, the price is expressed for 1 BTC, but the amount is in sats: /// 1 BTC = 10^8 sats - /// get_price(BTC, 1_00000000) = price(1BTC) * 1_00000000 / 10^8 = $50000 - + /// So that: + /// `get_price(BTC, 1_00000000) = price(1BTC) * 1_00000000 / 10^8 = $50_000 = 5_000_000 USDT + /// cents` + /// + /// # Diluted assets + /// /// Implementation ensure that a LP token price can be resolved as long as the base asset price /// is resolvable. ///```haskell @@ -39,18 +48,32 @@ pub trait Oracle { /// price (Vaulted base stock_dilution_rate) = price base * stock_dilution_rate /// ``` fn get_price( - asset: Self::AssetId, + asset_id: Self::AssetId, amount: Self::Balance, ) -> Result, DispatchError>; - /// Check whether the provided `asset` is supported (a.k.a. a price can be computed) by the + /// Check whether the provided `asset_id` is supported (a.k.a. a price can be computed) by the /// oracle. fn is_supported(asset: Self::AssetId) -> Result { - Self::get_price(asset, asset.unit()).map(|_| true) + let exponent = Self::LocalAssets::decimals(asset)?; + let unit: Self::Balance = 10_u64.pow(exponent).into(); + Self::get_price(asset, unit).map(|_| true) } fn get_twap( of: Self::AssetId, weighting: Vec, ) -> Result; + + /// 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 + /// ```rust + /// let base_in_common = 1000.0; + /// let quote_in_common = 100.0; + /// let ratio = base_in_common / quote_in_common; // 10.0 + /// let base_amount = 3.0; + /// let needed_base_for_quote = base_amount * ratio; // 300.0 + /// ``` + fn get_ratio(pair: CurrencyPair) -> Result; } diff --git a/frame/currency-factory/README.md b/frame/currency-factory/README.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/frame/currency-factory/src/lib.rs b/frame/currency-factory/src/lib.rs index 5d52b4aa32a..95628952a21 100644 --- a/frame/currency-factory/src/lib.rs +++ b/frame/currency-factory/src/lib.rs @@ -1,3 +1,5 @@ +//! Overview +//! Allows to add new assets internally. User facing mutating API is provided by other pallets. #![cfg_attr( not(test), warn( @@ -37,7 +39,7 @@ pub use pallet::*; #[frame_support::pallet] pub mod pallet { use codec::FullCodec; - use composable_traits::currency::{CurrencyFactory, DynamicCurrencyId}; + use composable_traits::currency::{CurrencyFactory, DynamicCurrencyId, Exponent, LocalAssets}; use frame_support::{pallet_prelude::*, PalletId}; use scale_info::TypeInfo; @@ -85,4 +87,11 @@ pub mod pallet { }) } } + + impl LocalAssets for Pallet { + fn decimals(_currency_id: T::DynamicCurrencyId) -> Result { + // All assets are normalized to 12 decimals. + Ok(12) + } + } } diff --git a/frame/dutch-auction/src/mock/currency.rs b/frame/dutch-auction/src/mock/currency.rs index 139e405b087..3c985f5c649 100644 --- a/frame/dutch-auction/src/mock/currency.rs +++ b/frame/dutch-auction/src/mock/currency.rs @@ -1,4 +1,4 @@ -use composable_traits::currency::{DynamicCurrencyId, PriceableAsset}; +use composable_traits::currency::DynamicCurrencyId; use frame_support::parameter_types; use scale_info::TypeInfo; use sp_runtime::{ArithmeticError, DispatchError}; @@ -33,19 +33,6 @@ impl Default for CurrencyId { } } -impl PriceableAsset for CurrencyId { - fn decimals(&self) -> composable_traits::currency::Exponent { - match self { - CurrencyId::PICA => 0, - CurrencyId::BTC => 8, - CurrencyId::ETH => 18, - CurrencyId::LTC => 8, - CurrencyId::USDT => 2, - CurrencyId::LpToken(_) => 0, - } - } -} - impl DynamicCurrencyId for CurrencyId { fn next(self) -> Result { match self { diff --git a/frame/lending/src/lib.rs b/frame/lending/src/lib.rs index 1e8962ff822..b98bd0e5ef4 100644 --- a/frame/lending/src/lib.rs +++ b/frame/lending/src/lib.rs @@ -54,13 +54,13 @@ pub mod pallet { use crate::{models::BorrowerData, weights::WeightInfo}; use codec::{Codec, FullCodec}; use composable_traits::{ - currency::{CurrencyFactory, PriceableAsset}, + currency::CurrencyFactory, defi::Rate, lending::{ math::*, BorrowAmountOf, CollateralLpAmountOf, Lending, MarketConfig, MarketConfigInput, }, liquidation::Liquidation, - loans::{DurationSeconds, PriceStructure, Timestamp}, + loans::{DurationSeconds, Timestamp}, math::{LiftedFixedBalance, SafeArithmetic}, oracle::Oracle, vault::{Deposit, FundsAvailability, StrategicVault, Vault, VaultConfig}, @@ -86,7 +86,7 @@ pub mod pallet { AccountIdConversion, AtLeast32BitUnsigned, CheckedAdd, CheckedMul, CheckedSub, One, Saturating, Zero, }, - ArithmeticError, FixedPointNumber, FixedPointOperand, FixedU128, + ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128, KeyTypeId as CryptoKeyTypeId, Percent, Perquintill, }; use sp_std::{fmt::Debug, vec, vec::Vec}; @@ -177,8 +177,7 @@ pub mod pallet { + MaybeSerializeDeserialize + Debug + Default - + TypeInfo - + PriceableAsset; + + TypeInfo; type Balance: Default + Parameter @@ -242,8 +241,7 @@ pub mod pallet { + From + Debug + Default - + TypeInfo - + PriceableAsset; + + TypeInfo; type Balance: Default + Parameter @@ -381,6 +379,7 @@ pub mod pallet { RepayAmountMustBeGraterThanZero, ExceedLendingCount, LiquidationFailed, + BorrowerDataCalculationFailed, } #[pallet::event] @@ -818,21 +817,7 @@ pub mod pallet { account: &::AccountId, ) -> Result<(), DispatchError> { if Self::should_liquidate(market_id, account)? { - 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 collateral_price = - Self::get_price(market.collateral, market.collateral.unit())?; - let source_target_account = Self::account_id(market_id); - T::Liquidation::liquidate( - &source_target_account, - market.collateral, - PriceStructure::new(collateral_price), - borrow_asset, - &source_target_account, - collateral_to_liquidate, - ) - .map(|_| ()) + Err(DispatchError::Other("TODO: work happens in other branch")) } else { Ok(()) } @@ -895,6 +880,10 @@ 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, @@ -1009,17 +998,25 @@ pub mod pallet { } let borrow_asset = T::Vault::asset_id(&market.borrow)?; - let borrow_limit_value = Self::get_borrow_limit(market_id, debt_owner)?; + + let borrow_limit = Self::get_borrow_limit(market_id, debt_owner)?; let borrow_amount_value = Self::get_price(borrow_asset, amount_to_borrow)?; + dbg!("{:?}", borrow_limit); + dbg!("{:?}", amount_to_borrow); + dbg!("{:?}", amount_to_borrow); ensure!( - borrow_limit_value >= borrow_amount_value, + borrow_limit >= borrow_amount_value, Error::::NotEnoughCollateralToBorrowAmount ); - + dbg!("{:?}", ::Currency::balance(borrow_asset, market_account)); ensure!( - ::Currency::can_withdraw(asset_id, market_account, amount_to_borrow) - .into_result() - .is_ok(), + ::Currency::can_withdraw( + borrow_asset, + market_account, + amount_to_borrow + ) + .into_result() + .is_ok(), Error::::NotEnoughBorrowAsset, ); ensure!( @@ -1349,7 +1346,7 @@ pub mod pallet { DebtMarkets::::try_get(market_id).map_err(|_| Error::::MarketDoesNotExist)?; let account_debt = DebtIndex::::get(market_id, account); - + dbg!("{:?}", account_debt); match account_debt { Some(account_interest_index) => { let principal = T::MarketDebtCurrency::balance_on_hold(debt_asset_id, account); @@ -1400,7 +1397,7 @@ pub mod pallet { let borrower = Self::create_borrower_data(market_id, account)?; Ok(borrower .borrow_for_collateral() - .map_err(|_| Error::::NotEnoughCollateralToBorrowAmount)? + .map_err(|_| Error::::BorrowerDataCalculationFailed)? .checked_mul_int(1_u64) .ok_or(ArithmeticError::Overflow)? .into()) diff --git a/frame/lending/src/mocks/mod.rs b/frame/lending/src/mocks/mod.rs index c7bb6ac7783..f636b1aef22 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, PriceableAsset}, + currency::{DynamicCurrencyId, Exponent}, defi::DeFiComposableConfig, governance::{GovernanceRegistry, SignedRawOrigin}, }; @@ -94,19 +94,6 @@ impl Default for MockCurrencyId { } } -impl PriceableAsset for MockCurrencyId { - fn decimals(&self) -> Exponent { - match self { - MockCurrencyId::PICA => 0, - MockCurrencyId::BTC => 8, - MockCurrencyId::ETH => 18, - MockCurrencyId::LTC => 8, - MockCurrencyId::USDT => 2, - MockCurrencyId::LpToken(_) => 0, - } - } -} - impl DynamicCurrencyId for MockCurrencyId { fn next(self) -> Result { match self { diff --git a/frame/lending/src/mocks/oracle.rs b/frame/lending/src/mocks/oracle.rs index a277ccb3d5b..05c16d04f26 100644 --- a/frame/lending/src/mocks/oracle.rs +++ b/frame/lending/src/mocks/oracle.rs @@ -4,12 +4,14 @@ pub use pallet::*; pub mod pallet { use codec::Codec; use composable_traits::{ - currency::PriceableAsset, + currency::LocalAssets, oracle::{Oracle, Price}, vault::Vault, }; use frame_support::pallet_prelude::*; - use sp_runtime::{helpers_128bit::multiply_by_rational, ArithmeticError, FixedPointNumber}; + use sp_runtime::{ + helpers_128bit::multiply_by_rational, ArithmeticError, DispatchError, FixedPointNumber, + }; use sp_std::fmt::Debug; use crate::mocks::{Balance, MockCurrencyId}; @@ -47,6 +49,7 @@ pub mod pallet { type AssetId = MockCurrencyId; type Balance = Balance; type Timestamp = (); + type LocalAssets = (); fn get_price( asset: Self::AssetId, @@ -54,7 +57,7 @@ pub mod pallet { ) -> Result, DispatchError> { let derive_price = |p: u128, a: u128| { let e = 10_u128 - .checked_pow(asset.decimals()) + .checked_pow(Self::LocalAssets::decimals(asset)?) .ok_or(DispatchError::Arithmetic(ArithmeticError::Overflow))?; let price = multiply_by_rational(p, a, e) .map_err(|_| DispatchError::Arithmetic(ArithmeticError::Overflow))?; @@ -103,5 +106,11 @@ pub mod pallet { ) -> Result { Ok(0_u32.into()) } + + fn get_ratio( + _pair: composable_traits::defi::CurrencyPair, + ) -> Result { + Err(DispatchError::Other("No implemented")) + } } } diff --git a/frame/lending/src/tests.rs b/frame/lending/src/tests.rs index 050b93e9d7f..9b95d961a2e 100644 --- a/frame/lending/src/tests.rs +++ b/frame/lending/src/tests.rs @@ -3,16 +3,16 @@ use std::ops::Mul; use crate::{ accrue_interest_internal, mocks::{ - 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, + 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, }, models::BorrowerData, Error, MarketIndex, }; use composable_tests_helpers::{prop_assert_acceptable_computation_error, prop_assert_ok}; use composable_traits::{ - currency::PriceableAsset, + currency::{CurrencyFactory, LocalAssets}, defi::Rate, lending::{math::*, MarketConfigInput}, math::LiftedFixedBalance, @@ -33,6 +33,7 @@ type CollateralAsset = MockCurrencyId; const DEFAULT_MARKET_VAULT_RESERVE: Perquintill = Perquintill::from_percent(10); const DEFAULT_MARKET_VAULT_STRATEGY_SHARE: Perquintill = Perquintill::from_percent(90); +const DEFAULT_COLLATERAL_FACTOR: u128 = 2; /// Create a very simple vault for the given currency, 100% is reserved. fn create_simple_vault( @@ -95,7 +96,7 @@ fn create_simple_market() -> (MarketIndex, BorrowAssetVault) { MockCurrencyId::USDT, *ALICE, DEFAULT_MARKET_VAULT_RESERVE, - NormalizedCollateralFactor::saturating_from_rational(200, 100), + NormalizedCollateralFactor::saturating_from_rational(DEFAULT_COLLATERAL_FACTOR * 100, 100), ) } @@ -269,22 +270,15 @@ fn accrue_interest_plotter() { #[test] fn test_borrow_repay_in_same_block() { new_test_ext().execute_with(|| { - let collateral_amount = 900000; + let collateral_amount = 900000000; let (market, vault) = create_simple_market(); - // Balance for ALICE - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), collateral_amount); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); - // Balance of BTC for CHARLIE - // CHARLIE is only lender of BTC - let borrow_asset_deposit = collateral_amount; - assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), 0); + let borrow_asset_deposit = 900000; assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); - assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), borrow_asset_deposit); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, borrow_asset_deposit)); let mut total_cash = DEFAULT_MARKET_VAULT_STRATEGY_SHARE.mul(borrow_asset_deposit); @@ -295,7 +289,7 @@ fn test_borrow_repay_in_same_block() { } assert_eq!(Lending::borrow_balance_current(&market, &ALICE), Ok(Some(0))); - let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let alice_limit = 100; assert_eq!(Lending::total_cash(&market), Ok(total_cash)); process_block(1); assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_limit / 4)); @@ -352,37 +346,34 @@ fn test_borrow_math() { } #[test] -fn test_borrow() { +fn borrow_flow() { new_test_ext().execute_with(|| { let (market, vault) = create_simple_market(); + let unit = 1_000_000_000; + Oracle::set_btc_price(50000); - Oracle::set_btc_price(50_000 * MockCurrencyId::USDT.unit::()); - - let btc_price = |x| { - Oracle::get_price(MockCurrencyId::BTC, x * MockCurrencyId::BTC.unit::()) - .expect("impossible") - .price - }; + let price = + |currency_id, amount| Oracle::get_price(currency_id, amount).expect("impossible").price; - let collateral_amount = 1_000_000 * MockCurrencyId::USDT.unit::(); + let alice_capable_btc = 100 * unit; + let collateral_amount = alice_capable_btc * price(MockCurrencyId::BTC, 1000) / + price(MockCurrencyId::USDT, 1000); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral_amount)); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), collateral_amount); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); + let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let limit = limit_normalized / price(MockCurrencyId::BTC, 1); + assert_eq!(limit, alice_capable_btc / DEFAULT_COLLATERAL_FACTOR); - let borrow_asset_deposit = 100 * MockCurrencyId::BTC.unit::(); + let borrow_asset_deposit = 100000 * unit; assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), borrow_asset_deposit); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, borrow_asset_deposit)); assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), 0); - let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - - // Can only borrow $500_000 worth of BTC - assert_eq!(alice_limit, btc_price(10)); - // Allow the market to initialize it's account by withdrawing // from the vault for i in 1..2 { @@ -392,30 +383,40 @@ fn test_borrow() { let expected_cash = DEFAULT_MARKET_VAULT_STRATEGY_SHARE.mul(borrow_asset_deposit); assert_eq!(Lending::total_cash(&market), Ok(expected_cash)); - // Borrow a single BTC - let alice_borrow = MockCurrencyId::BTC.unit::(); + let alice_borrow = alice_capable_btc / DEFAULT_COLLATERAL_FACTOR / 10; assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_borrow)); assert_eq!(Lending::total_cash(&market), Ok(expected_cash - alice_borrow)); assert_eq!(Lending::total_borrows(&market), Ok(alice_borrow)); + assert_eq!(Lending::total_interest_accurate(&market), Ok(0)); - for i in 2..10000 { + let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let original_limit = limit_normalized / price(MockCurrencyId::BTC, 1); + + assert_eq!(original_limit, alice_capable_btc / DEFAULT_COLLATERAL_FACTOR - alice_borrow); + + let borrow = Lending::borrow_balance_current(&market, &ALICE).unwrap().unwrap(); + assert_eq!(borrow, alice_borrow); + let interest_before = Lending::total_interest_accurate(&market).unwrap(); + for i in 2..50 { process_block(i); } + let interest_after = Lending::total_interest_accurate(&market).unwrap(); - assert_eq!(Lending::total_interest_accurate(&market), Ok(3994235158863300000000)); + let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let new_limit = limit_normalized / price(MockCurrencyId::BTC, 1); - // Interests mean that we cannot borrow the remaining 9 BTCs. + assert!(new_limit < original_limit); + + let borrow = Lending::borrow_balance_current(&market, &ALICE).unwrap().unwrap(); + + assert!(borrow > alice_borrow); assert_noop!( - Lending::borrow_internal(&market, &ALICE, 9 * MockCurrencyId::BTC.unit::()), + Lending::borrow_internal(&market, &ALICE, original_limit), Error::::NotEnoughCollateralToBorrowAmount ); // Borrow less because of interests. - assert_ok!(Lending::borrow_internal( - &market, - &ALICE, - 8 * MockCurrencyId::BTC.unit::() - )); + assert_ok!(Lending::borrow_internal(&market, &ALICE, new_limit,)); // More than one borrow request in same block is invalid. assert_noop!( @@ -429,22 +430,16 @@ fn test_borrow() { assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + dbg!("{:?}", alice_limit); + assert!(price(MockCurrencyId::BTC, alice_capable_btc) > alice_limit); + assert!(alice_limit > price(MockCurrencyId::BTC, alice_borrow)); - // We didn't borrowed 9 because of interests, we added $ worth of 10 BTC, - // let x being our limit, it should be: 11>x>10 - assert!(btc_price(11) > alice_limit && alice_limit > btc_price(10)); - - // Try to borrow more than limit assert_noop!( - Lending::borrow_internal(&market, &ALICE, 11 * MockCurrencyId::BTC.unit::()), + Lending::borrow_internal(&market, &ALICE, alice_limit), Error::::NotEnoughCollateralToBorrowAmount ); - assert_ok!(Lending::borrow_internal( - &market, - &ALICE, - 10 * MockCurrencyId::BTC.unit::() - )); + assert_ok!(Lending::borrow_internal(&market, &ALICE, 10)); }); } @@ -453,8 +448,8 @@ fn test_vault_market_cannot_withdraw() { new_test_ext().execute_with(|| { let (market, vault_id) = create_simple_market(); - let deposit_usdt = 1_000_000 * MockCurrencyId::USDT.unit::(); - let deposit_btc = 10 * MockCurrencyId::BTC.unit::(); + let deposit_usdt = 1_000_000; + let deposit_btc = 10; assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, deposit_usdt)); assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, deposit_btc)); @@ -463,7 +458,7 @@ fn test_vault_market_cannot_withdraw() { // We don't even wait 1 block, which mean the market couldn't withdraw funds. assert_noop!( - Lending::borrow_internal(&market, &ALICE, MockCurrencyId::BTC.unit::()), + Lending::borrow_internal(&market, &ALICE, deposit_btc), Error::::NotEnoughBorrowAsset ); }); @@ -474,13 +469,13 @@ fn test_vault_market_can_withdraw() { new_test_ext().execute_with(|| { let (market, vault_id) = create_simple_market(); - let deposit_usdt = 1_000_000 * MockCurrencyId::USDT.unit::(); - let deposit_btc = 10 * MockCurrencyId::BTC.unit::(); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, deposit_usdt)); - assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, deposit_btc)); + let collateral = 1_000_000_000_000; + let borrow = 10; + assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral)); + assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, borrow)); - assert_ok!(Vault::deposit(Origin::signed(*ALICE), vault_id, deposit_btc)); - assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, deposit_usdt)); + assert_ok!(Vault::deposit(Origin::signed(*ALICE), vault_id, borrow)); + assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral)); for i in 1..2 { process_block(i); @@ -490,7 +485,7 @@ fn test_vault_market_can_withdraw() { assert_ok!(Lending::borrow_internal( &market, &ALICE, - MockCurrencyId::BTC.unit::() + borrow - 1 // DEFAULT_MARKET_VAULT_RESERVE ),); }); } @@ -498,8 +493,8 @@ fn test_vault_market_can_withdraw() { #[test] fn borrow_repay() { new_test_ext().execute_with(|| { - let alice_balance = 1_000_000 * MockCurrencyId::USDT.unit::(); - let bob_balance = 1_000_000 * MockCurrencyId::USDT.unit::(); + let alice_balance = 1_000_000; + let bob_balance = 1_000_000; let (market, vault) = create_simple_market(); @@ -517,7 +512,7 @@ fn borrow_repay() { assert_ok!(Lending::deposit_collateral_internal(&market, &BOB, bob_balance)); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &BOB), 0); - let borrow_asset_deposit = 10 * MockCurrencyId::BTC.unit::(); + let borrow_asset_deposit = 10_000_000_000; assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), borrow_asset_deposit); @@ -531,7 +526,9 @@ fn borrow_repay() { // ALICE borrows assert_eq!(Lending::borrow_balance_current(&market, &ALICE), Ok(Some(0))); - let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let alice_limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let alice_limit = + alice_limit_normalized / Oracle::get_price(MockCurrencyId::BTC, 1).unwrap().price; assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_limit)); for i in 2..10000 { @@ -540,11 +537,9 @@ fn borrow_repay() { // BOB borrows assert_eq!(Lending::borrow_balance_current(&market, &BOB), Ok(Some(0))); - assert_ok!(Lending::borrow_internal( - &market, - &BOB, - Lending::get_borrow_limit(&market, &BOB).expect("impossible") - )); + let limit_normalized = Lending::get_borrow_limit(&market, &BOB).unwrap(); + let limit = limit_normalized / Oracle::get_price(MockCurrencyId::BTC, 1).unwrap().price; + assert_ok!(Lending::borrow_internal(&market, &BOB, limit,)); let bob_limit = Lending::get_borrow_limit(&market, &BOB).unwrap(); @@ -587,9 +582,9 @@ fn test_liquidation() { NormalizedCollateralFactor::saturating_from_rational(2, 1), ); - Oracle::set_btc_price(100 * MockCurrencyId::USDT.unit::()); + Oracle::set_btc_price(100); - let two_btc_amount = 2 * MockCurrencyId::BTC.unit::(); + let two_btc_amount = 2; assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, two_btc_amount)); assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), two_btc_amount); @@ -620,7 +615,7 @@ fn test_liquidation() { } // Collateral going down imply liquidation - Oracle::set_btc_price(99 * MockCurrencyId::USDT.unit::()); + Oracle::set_btc_price(99); assert_ok!(Lending::liquidate_internal(&market, &ALICE)); }); @@ -638,10 +633,10 @@ fn test_warn_soon_under_collaterized() { ); // 1 BTC = 100 USDT - Oracle::set_btc_price(100 * MockCurrencyId::USDT.unit::()); + Oracle::set_btc_price(100); // Deposit 2 BTC - let two_btc_amount = 2 * MockCurrencyId::BTC.unit::(); + let two_btc_amount = 2; assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, two_btc_amount)); assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), two_btc_amount); @@ -671,20 +666,17 @@ fn test_warn_soon_under_collaterized() { = 10000 cents */ - assert_eq!( - Lending::get_borrow_limit(&market, &ALICE), - Ok(100 * MockCurrencyId::USDT.unit::()) - ); + assert_eq!(Lending::get_borrow_limit(&market, &ALICE), Ok(100)); // Borrow 80 USDT - let borrow_amount = 80 * MockCurrencyId::USDT.unit::(); + let borrow_amount = 80; assert_ok!(Lending::borrow_internal(&market, &ALICE, borrow_amount)); for i in 2..10000 { process_block(i); } - Oracle::set_btc_price(90 * MockCurrencyId::USDT.unit::()); + Oracle::set_btc_price(90); /* Collateral = 2*90 = 180 @@ -693,7 +685,7 @@ fn test_warn_soon_under_collaterized() { */ assert_eq!(Lending::soon_under_collaterized(&market, &ALICE), Ok(false)); - Oracle::set_btc_price(87 * MockCurrencyId::USDT.unit::()); + Oracle::set_btc_price(87); /* Collateral = 2*87 = 174 @@ -805,13 +797,13 @@ proptest! { new_test_ext().execute_with(|| { let (market, _vault) = create_simple_market(); prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); - prop_assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, amount * MockCurrencyId::USDT.unit::())); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), amount * MockCurrencyId::USDT.unit::()); + prop_assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, amount )); + prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), amount ); - prop_assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, amount * MockCurrencyId::USDT.unit::())); + prop_assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, amount )); prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); prop_assert_eq!( - Lending::withdraw_collateral_internal(&market, &ALICE, amount * MockCurrencyId::USDT.unit::() + 1), + Lending::withdraw_collateral_internal(&market, &ALICE, amount + 1), Err(Error::::NotEnoughCollateral.into()) ); diff --git a/frame/oracle/src/lib.rs b/frame/oracle/src/lib.rs index d39cebd890f..5b40eedb6f4 100644 --- a/frame/oracle/src/lib.rs +++ b/frame/oracle/src/lib.rs @@ -30,7 +30,8 @@ pub mod pallet { pub use crate::weights::WeightInfo; use codec::{Codec, FullCodec}; use composable_traits::{ - currency::PriceableAsset, + currency::LocalAssets, + math::SafeArithmetic, oracle::{Oracle, Price as LastPrice}, }; use core::ops::{Div, Mul}; @@ -58,7 +59,8 @@ pub mod pallet { use sp_runtime::{ offchain::{http, Duration}, traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedMul, CheckedSub, Saturating, Zero}, - AccountId32, KeyTypeId as CryptoKeyTypeId, PerThing, Percent, RuntimeDebug, + AccountId32, FixedPointNumber, FixedU128, KeyTypeId as CryptoKeyTypeId, PerThing, Percent, + RuntimeDebug, }; use sp_std::{borrow::ToOwned, fmt::Debug, str, vec, vec::Vec}; @@ -111,8 +113,7 @@ pub mod pallet { + Into + Debug + Default - + TypeInfo - + PriceableAsset; + + TypeInfo; type PriceValue: Default + Parameter + Codec @@ -146,6 +147,7 @@ pub mod pallet { /// The weight information of this pallet. type WeightInfo: WeightInfo; + type LocalAssets: LocalAssets; } #[derive(Encode, Decode, Default, Debug, PartialEq, TypeInfo)] @@ -371,9 +373,8 @@ pub mod pallet { type Balance = T::PriceValue; type AssetId = T::AssetId; type Timestamp = ::BlockNumber; + type LocalAssets = T::LocalAssets; - // TODO(hussein-aitlahcen): - // implement the amount based computation with decimals once it's been completely defined fn get_price( asset: Self::AssetId, _amount: Self::Balance, @@ -389,6 +390,22 @@ pub mod pallet { ) -> Result { Self::get_twap(of, weighting) } + + fn get_ratio( + pair: composable_traits::defi::CurrencyPair, + ) -> Result { + let base: u128 = + Self::get_price(pair.base, (10 ^ T::LocalAssets::decimals(pair.base)?).into())? + .price + .into(); + let quote: u128 = + Self::get_price(pair.quote, (10 ^ T::LocalAssets::decimals(pair.base)?).into())? + .price + .into(); + let base = FixedU128::saturating_from_integer(base); + let quote = FixedU128::saturating_from_integer(quote); + Ok(base.safe_div("e)?) + } } #[pallet::call] diff --git a/frame/oracle/src/mock.rs b/frame/oracle/src/mock.rs index 5aae71babc9..b7c74765481 100644 --- a/frame/oracle/src/mock.rs +++ b/frame/oracle/src/mock.rs @@ -133,6 +133,7 @@ impl pallet_oracle::Config for Test { type MaxAssetsCount = MaxAssetsCount; type MaxHistory = MaxHistory; type WeightInfo = (); + type LocalAssets = (); } // Build genesis storage according to the mock runtime. diff --git a/runtime/common/src/impls.rs b/runtime/common/src/impls.rs index 78c361ab098..82310533a0a 100644 --- a/runtime/common/src/impls.rs +++ b/runtime/common/src/impls.rs @@ -55,7 +55,6 @@ mod tests { use super::*; use crate::{Balance, BlockNumber, DAYS}; use collator_selection::IdentityCollator; - use composable_traits::currency::PriceableAsset; use frame_support::{ ord_parameter_types, parameter_types, traits::{Everything, FindAuthor, ValidatorRegistration}, diff --git a/runtime/composable/src/lib.rs b/runtime/composable/src/lib.rs index b681e165e11..ca3fdaca001 100644 --- a/runtime/composable/src/lib.rs +++ b/runtime/composable/src/lib.rs @@ -24,7 +24,6 @@ use common::{ CouncilInstance, EnsureRootOrHalfCouncil, Hash, Signature, AVERAGE_ON_INITIALIZE_RATIO, DAYS, HOURS, MAXIMUM_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; -use composable_traits::currency::PriceableAsset; use orml_traits::parameter_type_with_key; use primitives::currency::CurrencyId; use sp_api::impl_runtime_apis; diff --git a/runtime/dali/src/lib.rs b/runtime/dali/src/lib.rs index ccb6e3f7f97..c7fb2cf139e 100644 --- a/runtime/dali/src/lib.rs +++ b/runtime/dali/src/lib.rs @@ -25,7 +25,6 @@ use common::{ CouncilInstance, EnsureRootOrHalfCouncil, Hash, Signature, AVERAGE_ON_INITIALIZE_RATIO, DAYS, HOURS, MAXIMUM_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; -use composable_traits::currency::PriceableAsset; use orml_traits::parameter_type_with_key; use primitives::currency::CurrencyId; use sp_api::impl_runtime_apis; @@ -441,6 +440,7 @@ impl oracle::Config for Runtime { type MaxAssetsCount = MaxAssetsCount; type MaxHistory = MaxHistory; type WeightInfo = weights::oracle::WeightInfo; + type LocalAssets = Factory; } // Parachain stuff. diff --git a/runtime/picasso/src/lib.rs b/runtime/picasso/src/lib.rs index e15934e20f8..11f0f416cee 100644 --- a/runtime/picasso/src/lib.rs +++ b/runtime/picasso/src/lib.rs @@ -26,7 +26,6 @@ use common::{ CouncilInstance, EnsureRootOrHalfCouncil, Hash, Signature, AVERAGE_ON_INITIALIZE_RATIO, DAYS, HOURS, MAXIMUM_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; -use composable_traits::currency::PriceableAsset; use orml_traits::parameter_type_with_key; use primitives::currency::CurrencyId; use sp_api::impl_runtime_apis; diff --git a/runtime/primitives/src/currency.rs b/runtime/primitives/src/currency.rs index 5fc2a1555e2..73981396b48 100644 --- a/runtime/primitives/src/currency.rs +++ b/runtime/primitives/src/currency.rs @@ -1,68 +1,7 @@ //! CurrencyId implementation -/// Asset id as if it was deserialized, not necessary exists. -/// We could check asset id during serde, but that: -/// - will make serde setup complicated (need to write and consistently apply static singletons -/// to all places with asset id) -/// - validate will involve at minimum in memory cache call (in worth case db call) during -/// extrinsic invocation -/// - will need to disable this during calls when it is really no need for validation (new -/// currency mapping) -/// - normal path will pay price (validate each time), in instead when fail pays only (like -/// trying to transfer non existing asset id) -/// - we cannot guarantee existence of asset as it may be removed during transaction (so we -/// should make removal exclusive case) -/// -/// Given above we stick with possibly wrong asset id passed into API. -/// -/// # Assert id pallet design -/// ```ignore -/// pub trait MaximalConstGet { -/// const VALUE: T; -/// } -/// /// knows existing local assets and how to map them to simple numbers -/// pub trait LocalAssetsRegistry { -/// /// asset id which is exist from now in current block -/// /// valid does not means usable, it can be subject to deletion or not yet approved to be used -/// type AssetId : AssetIdLike + Into; -/// /// just id after serde -/// type MayBeAssetId : AssetIdLike + From; -/// /// assets which we well know and embedded into `enum`. -/// /// maximal of this is smaller than minimal `OtherAssetId` -/// /// can always convert to valid asset id -/// type WellKnownAssetId : MaximalConstGet + Into + Into + Decimals + TryFrom; -/// -/// /// Larger than maximal of `WellKnownAssetId` but smaller than minimal `DerivativeAssetId`. -/// type OtherAssetId : MinimalConstGet + MaximalConstGet + Into + Into; -/// /// allows to get next asset id -/// /// can consider split out producing assets interface into separate trait -/// type NextOtherAssetId = ErrorNext; -/// -/// /// locally diluted derivative and liquidity assets. -/// /// larger than maximal `OtherAssetId` -/// /// `Self::OtherAssetId` may be diluted(derived/wrapped), but only remote. -/// type DerivativeAssetId: MinimalConstGet + Into; -/// /// may consider split out asset producing trait -/// type NextDerivativeAssetId = ErrorNext; -/// -/// // note: fn to be replaced with Get or traits, just shortcuted here -/// -/// fn try_from>(value : N) -> Result; -/// /// one unique native asset id -/// fn native() -> Self::WellKnownAssetId; -/// -/// /// really u8, but easy to do math operations -/// /// ORML also has separate trait on Balances to inspect decimals, that is not on type it self -/// fn decimals(asset_id: Self::AssetId) -> u32; -/// } -/// /// read remote paths -/// /// registering is separate trait -/// pub trait RemoteAssetRegistry : LocalAssetsRegistry { -/// fn substrate(asset_id: Self::AssetId) -> Self:XcmPath; -/// fn remote(asset_id: Self::AssetId, network_id:) -> Self::Path; -/// } -/// ``` use codec::{CompactAs, Decode, Encode}; -use composable_traits::currency::{DynamicCurrencyId, Exponent, PriceableAsset}; +use composable_traits::currency::{DynamicCurrencyId, Exponent}; +use core::ops::Div; use scale_info::TypeInfo; use sp_runtime::{ArithmeticError, DispatchError, RuntimeDebug}; @@ -84,16 +23,18 @@ impl CurrencyId { pub const LAYR: CurrencyId = CurrencyId(2); pub const CROWD_LOAN: CurrencyId = CurrencyId(3); pub const KSM: CurrencyId = CurrencyId(4); - pub const LOCAL_LP_TOKEN_START: CurrencyId = CurrencyId(u128::MAX / 2); -} -/// All assets are normalized to 12 decimals. -impl PriceableAsset for CurrencyId { #[inline(always)] - fn decimals(&self) -> Exponent { + pub fn decimals(&self) -> Exponent { 12 } + pub fn unit>(&self) -> T { + T::from(10_u64.pow(self.decimals())) + } + pub fn milli + Div>(&self) -> T { + self.unit::() / T::from(1000_u64) + } } // NOTE(hussein-aitlahcen): we could add an index to DynamicCurrency to differentiate sub-ranges From 290e574d6b0da98dde4d8d66bcb8db04f7474196 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Wed, 12 Jan 2022 10:26:40 +0200 Subject: [PATCH 2/9] fixing pr comments Signed-off-by: Dzmitry Lahoda --- frame/lending/src/lib.rs | 5 ----- frame/lending/src/tests.rs | 12 +++++++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frame/lending/src/lib.rs b/frame/lending/src/lib.rs index b98bd0e5ef4..8dae973b6ae 100644 --- a/frame/lending/src/lib.rs +++ b/frame/lending/src/lib.rs @@ -1001,14 +1001,10 @@ pub mod pallet { let borrow_limit = Self::get_borrow_limit(market_id, debt_owner)?; let borrow_amount_value = Self::get_price(borrow_asset, amount_to_borrow)?; - dbg!("{:?}", borrow_limit); - dbg!("{:?}", amount_to_borrow); - dbg!("{:?}", amount_to_borrow); ensure!( borrow_limit >= borrow_amount_value, Error::::NotEnoughCollateralToBorrowAmount ); - dbg!("{:?}", ::Currency::balance(borrow_asset, market_account)); ensure!( ::Currency::can_withdraw( borrow_asset, @@ -1346,7 +1342,6 @@ pub mod pallet { DebtMarkets::::try_get(market_id).map_err(|_| Error::::MarketDoesNotExist)?; let account_debt = DebtIndex::::get(market_id, account); - dbg!("{:?}", account_debt); match account_debt { Some(account_interest_index) => { let principal = T::MarketDebtCurrency::balance_on_hold(debt_asset_id, account); diff --git a/frame/lending/src/tests.rs b/frame/lending/src/tests.rs index 9b95d961a2e..ef9d070819b 100644 --- a/frame/lending/src/tests.rs +++ b/frame/lending/src/tests.rs @@ -245,7 +245,8 @@ fn accrue_interest_plotter() { #[cfg(feature = "visualization")] { use plotters::prelude::*; - let area = BitMapBackend::new("./accrue_interest.png", (1024, 768)).into_drawing_area(); + let area = + BitMapBackend::new("./accrue_interest_plotter.png", (1024, 768)).into_drawing_area(); area.fill(&WHITE).unwrap(); let mut chart = ChartBuilder::on(&area) @@ -288,8 +289,12 @@ fn test_borrow_repay_in_same_block() { process_block(i); } + let price = + |currency_id, amount| Oracle::get_price(currency_id, amount).expect("impossible").price; + assert_eq!(Lending::borrow_balance_current(&market, &ALICE), Ok(Some(0))); - let alice_limit = 100; + let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); + let alice_limit = limit_normalized / price(MockCurrencyId::BTC, 1); assert_eq!(Lending::total_cash(&market), Ok(total_cash)); process_block(1); assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_limit / 4)); @@ -360,9 +365,7 @@ fn borrow_flow() { price(MockCurrencyId::USDT, 1000); assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), collateral_amount); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); let limit = limit_normalized / price(MockCurrencyId::BTC, 1); assert_eq!(limit, alice_capable_btc / DEFAULT_COLLATERAL_FACTOR); @@ -430,7 +433,6 @@ fn borrow_flow() { assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - dbg!("{:?}", alice_limit); assert!(price(MockCurrencyId::BTC, alice_capable_btc) > alice_limit); assert!(alice_limit > price(MockCurrencyId::BTC, alice_borrow)); From ee4092297210507a19f7ab5c69d787fdd5721045 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Wed, 12 Jan 2022 20:00:37 +0200 Subject: [PATCH 3/9] fixed comment Signed-off-by: Dzmitry Lahoda --- frame/composable-traits/src/currency.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frame/composable-traits/src/currency.rs b/frame/composable-traits/src/currency.rs index fd1a838e52f..d37052d7a8f 100644 --- a/frame/composable-traits/src/currency.rs +++ b/frame/composable-traits/src/currency.rs @@ -30,15 +30,19 @@ pub trait CurrencyFactory { fn create() -> Result; } +/// Local presentation of asset information. +/// Most pallets do not need it. pub trait LocalAssets { - /// decimals of of big unit over minimal unit - /// ORML also has separate trait on Balances to inspect decimals, that is not on type it self - fn decimals(currency_id: MayBeAssetId) -> Result; - - fn unit>(currency_id: MayBeAssetId) -> Result { - let exponent = Self::decimals(currency_id)?; - Ok(10_u64.pow(exponent).into()) - } + /// decimals of of big unit over minimal unit + /// ORML also has separate trait on Balances to inspect decimals, that is not on type it self + fn decimals(currency_id: MayBeAssetId) -> Result; + /// Amount which humans operate as `1` usually. + /// Amount is probably priceable by Oracles. + /// Amount resonably higher than minimal tradeable amount or minial trading step on DEX. + fn unit>(currency_id: MayBeAssetId) -> Result { + let exponent = Self::decimals(currency_id)?; + Ok(10_u64.pow(exponent).into()) + } } /// when we store assets in native form to chain in smallest units or for mock in tests From 7465be5ab49b7a202c62d29e0cb50df49765ecff Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Wed, 12 Jan 2022 20:47:08 +0200 Subject: [PATCH 4/9] crazy fmt issue Signed-off-by: Dzmitry Lahoda --- frame/composable-traits/src/currency.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frame/composable-traits/src/currency.rs b/frame/composable-traits/src/currency.rs index d37052d7a8f..10b7d95f376 100644 --- a/frame/composable-traits/src/currency.rs +++ b/frame/composable-traits/src/currency.rs @@ -33,16 +33,17 @@ pub trait CurrencyFactory { /// Local presentation of asset information. /// Most pallets do not need it. pub trait LocalAssets { - /// decimals of of big unit over minimal unit - /// ORML also has separate trait on Balances to inspect decimals, that is not on type it self - fn decimals(currency_id: MayBeAssetId) -> Result; - /// Amount which humans operate as `1` usually. - /// Amount is probably priceable by Oracles. - /// Amount resonably higher than minimal tradeable amount or minial trading step on DEX. - fn unit>(currency_id: MayBeAssetId) -> Result { - let exponent = Self::decimals(currency_id)?; - Ok(10_u64.pow(exponent).into()) - } + /// decimals of of big unit over minimal unit. + /// orml also has separate trait on Balances to inspect decimals, that is not on type it self + fn decimals(currency_id: MayBeAssetId) -> Result; + + /// Amount which humans operate as `1` usually. + /// Amount is probably priceable by Oracles. + /// Amount resonably higher than minimal tradeable amount or minial trading step on DEX. + fn unit>(currency_id: MayBeAssetId) -> Result { + let exponent = Self::decimals(currency_id)?; + Ok(10_u64.pow(exponent).into()) + } } /// when we store assets in native form to chain in smallest units or for mock in tests From 7e3c9064b5a780962cc28009e4348f246165d323 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Thu, 13 Jan 2022 18:09:55 +0200 Subject: [PATCH 5/9] just something to tirgget build after fail Signed-off-by: Dzmitry Lahoda --- frame/lending/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/lending/README.md b/frame/lending/README.md index 003c904f2e0..95a741a0c5d 100644 --- a/frame/lending/README.md +++ b/frame/lending/README.md @@ -1,6 +1,6 @@ -https://composablefinance.atlassian.net/wiki/spaces/COM/pages/2916374/Lending +# [Overview](https://app.clickup.com/20465559/v/dc/kghwq-20761/kghwq-3621) ## What From ce815aa3691424874ab77efe6f59152b9734fbb0 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Thu, 13 Jan 2022 18:44:35 +0200 Subject: [PATCH 6/9] fixed lending Signed-off-by: Dzmitry Lahoda --- .config/cargo_spellcheck.dic | 2 +- frame/composable-traits/src/auction.rs | 35 +- frame/composable-traits/src/currency.rs | 16 +- frame/composable-traits/src/defi.rs | 88 ++- frame/composable-traits/src/lending/math.rs | 47 +- frame/composable-traits/src/lending/mod.rs | 87 ++- frame/composable-traits/src/lending/tests.rs | 20 +- 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 | 35 +- frame/dutch-auction/src/lib.rs | 116 ++-- frame/dutch-auction/src/math.rs | 23 +- frame/dutch-auction/src/mock/mod.rs | 2 +- frame/dutch-auction/src/mock/runtime.rs | 9 +- frame/dutch-auction/src/tests.rs | 52 +- frame/lending/Cargo.toml | 4 +- frame/lending/accrue_interest.png | Bin 36565 -> 0 bytes frame/lending/accrue_interest_plotter.png | Bin 0 -> 47727 bytes frame/lending/proptest-regressions/tests.txt | 7 + frame/lending/src/benchmarking.rs | 12 +- frame/lending/src/lib.rs | 532 ++++++++----------- frame/lending/src/mocks/mod.rs | 28 +- frame/lending/src/models.rs | 24 +- frame/lending/src/tests.rs | 159 +++--- frame/liquidations/src/lib.rs | 191 ++++++- frame/liquidations/src/weights.rs | 9 + frame/oracle/src/lib.rs | 1 + 32 files changed, 918 insertions(+), 695 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/accrue_interest_plotter.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/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 b8615aedb89..25414f53c59 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 }, @@ -174,6 +179,7 @@ pub trait DeFiComposableConfig: frame_system::Config { type MayBeAssetId: AssetIdLike + MaybeSerializeDeserialize + Default; type Balance: BalanceLike + + MathBalance + Default + Parameter + Codec @@ -186,12 +192,54 @@ 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 crate::defi::LiftedFixedBalance; + + 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 8eef34c1384..ef7532f578c 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, @@ -114,9 +102,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) @@ -155,15 +147,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 }; @@ -394,7 +389,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 @@ -406,7 +401,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) } @@ -416,5 +411,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 0a26afa2960..84cd9787c72 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 { + /// Reserve factor of market. + pub reserved_factor: Perquintill, + /// 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, +} + +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.updatable.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..73649651ec5 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), ); } @@ -84,17 +84,17 @@ fn curve_model_correctly_calculates_borrow_rate() { #[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)), + (1..=10u32).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), + (11..=30u32).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), + (31..=50).prop_map(|x| ZeroToOneFixedU128::saturating_from_rational(x, 100)), (0..=100u8).prop_map(Percent::from_percent), ) .prop_filter("Jump rate model", |(base, jump, full, _)| { 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 af2c9e9f2e4..6f9188efba9 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)] @@ -65,6 +67,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 @@ -75,5 +78,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..464c5998b93 100644 --- a/frame/dutch-auction/src/benchmarking.rs +++ b/frame/dutch-auction/src/benchmarking.rs @@ -1,10 +1,11 @@ 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 @@ -12,13 +13,13 @@ pub fn sell_identity( ) -> Sell<::MayBeAssetId, ::Balance> { let one: ::Balance = 1u64.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) + Take::new(one, Ratio::saturating_from_integer(one)) } pub type AssetIdOf = ::MayBeAssetId; @@ -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 mut 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 mut 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,7 +99,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 mut 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(); 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/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..67c34fc40bd 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; 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 56d6c75a725..2b6b6497f15 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 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 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..8fcf0a39031 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] @@ -386,21 +321,34 @@ pub mod pallet { #[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 +357,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 +381,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 +394,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 +411,7 @@ pub mod pallet { MarketIndex, Twox64Concat, T::AccountId, - Ratio, + ZeroToOneFixedU128, OptionQuery, >; @@ -468,7 +432,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 +486,44 @@ 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()) } + #[pallet::weight(::WeightInfo::create_new_market())] + #[transactional] + pub fn update_market( + origin: OriginFor, + _input: UpdateInput, + ) -> DispatchResultWithPostInfo { + let _who = ensure_signed(origin)?; + // 1. validate owner + // 2. update configuration + // 3. what is owner updates and forces liquidations? ok. + Err(DispatchError::Other("not implemented").into()) + } + /// Deposit collateral to market. /// - `origin` : Sender of this extrinsic. /// - `market` : Market index to which collateral will be deposited. @@ -599,7 +554,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 +582,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 +627,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 +669,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 +718,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 +739,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 +748,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 +760,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 +833,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 +896,7 @@ pub mod pallet { } fn get_price( - asset_id: ::AssetId, + asset_id: ::MayBeAssetId, amount: T::Balance, ) -> Result { ::get_price(asset_id, amount) @@ -958,23 +907,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 +930,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 +975,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 +987,7 @@ pub mod pallet { ); } ensure!( - repay_amount > ::Balance::zero(), + repay_amount > ::Balance::zero(), Error::::RepayAmountMustBeGraterThanZero ); ensure!(repay_amount <= owed, Error::::CannotRepayMoreThanBorrowAmount); @@ -1065,31 +1008,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 +1053,14 @@ 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.updatable.reserved_factor), )] .iter() .cloned() @@ -1123,19 +1069,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 +1118,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 +1140,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 +1152,7 @@ pub mod pallet { market_id, from, beneficiary, - repay_amount, + total_repay_amount, owed, borrow_asset_id, &market_account, @@ -1219,9 +1160,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 +1196,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 +1215,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 +1233,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 +1253,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 +1265,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 +1288,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 +1315,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 +1451,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 +1471,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 ef9d070819b..34f97e5d752 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 { + reserved_factor: reserved, + collateral_factor, + under_collaterized_warn_percent: Percent::from_float(0.10), + liquidators: vec![], + interest_rate_model: InterestRateModel::default(), + }, + 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,34 +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(&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(); @@ -404,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); @@ -581,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); @@ -631,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 @@ -770,7 +775,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(); @@ -850,7 +855,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..745e397d068 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,157 @@ 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; + } + + #[pallet::genesis_config] + pub struct GenesisConfig { + _phantom: sp_std::marker::PhantomData, + } - type LiquidationId = u128; + 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 GroupId = T::GroupId; + 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 { + if configuration.is_empty() { + let configuration = Strategies::::get(DefaultStrategyIndex::::get()) + .expect("default always exists"); + match configuration { + LiquidationStrategyConfiguration::DutchAuction(configuration) => { + Self::deposit_event(Event::::PositionWasSentToLiquidation {}); + return T::DutchAuction::ask(from_to, order, configuration) + }, + _ => return Err(DispatchError::Other("TODO")), + } + } else { + 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("TODO")), + }; + + 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 5b40eedb6f4..c12352fd446 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; From 6e8250124b03fbc42c79501b1ba46e31151bc16b Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Sun, 16 Jan 2022 15:59:39 +0200 Subject: [PATCH 7/9] fixed price, added ratio test Signed-off-by: Dzmitry Lahoda --- frame/composable-traits/src/defi.rs | 16 +++++++-- frame/composable-traits/src/oracle.rs | 1 + frame/oracle/src/lib.rs | 6 ++-- frame/oracle/src/tests.rs | 48 ++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/frame/composable-traits/src/defi.rs b/frame/composable-traits/src/defi.rs index b8615aedb89..6b7dd9d7b55 100644 --- a/frame/composable-traits/src/defi.rs +++ b/frame/composable-traits/src/defi.rs @@ -64,14 +64,20 @@ impl Sell { - /// See [Base Currency](https://www.investopedia.com/terms/b/basecurrency.asp) + /// See [Base Currency](https://www.investopedia.com/terms/b/basecurrency.asp). + /// Also can be named `native`(to the market) currency. pub base: AssetId, - /// counter currency + /// Counter currency. + /// Also can be named `price` currency. pub quote: AssetId, } +/// Generically pair can be of external URI/location, not copy. +impl Copy for CurrencyPair { +} + impl CurrencyPair { pub fn new(base: AssetId, quote: AssetId) -> Self { Self { base, quote } @@ -92,6 +98,10 @@ impl CurrencyPair { pub fn as_slice(&self) -> &[AssetId] { unsafe { sp_std::slice::from_raw_parts(self as *const Self as *const AssetId, 2) } } + + pub fn reverse(&mut self) { + sp_std::mem::swap(&mut self.quote, &mut self.base) + } } impl AsRef<[AssetId]> for CurrencyPair { diff --git a/frame/composable-traits/src/oracle.rs b/frame/composable-traits/src/oracle.rs index af2c9e9f2e4..56f7e8b548e 100644 --- a/frame/composable-traits/src/oracle.rs +++ b/frame/composable-traits/src/oracle.rs @@ -60,6 +60,7 @@ pub trait Oracle { Self::get_price(asset, unit).map(|_| true) } + /// Time Weighted Average Price fn get_twap( of: Self::AssetId, weighting: Vec, diff --git a/frame/oracle/src/lib.rs b/frame/oracle/src/lib.rs index 5b40eedb6f4..326e1a858ff 100644 --- a/frame/oracle/src/lib.rs +++ b/frame/oracle/src/lib.rs @@ -163,8 +163,10 @@ pub mod pallet { pub who: AccountId, } + // block timestamped value #[derive(Encode, Decode, Default, Debug, PartialEq, TypeInfo, Clone)] pub struct Price { + /// value pub price: PriceValue, pub block: BlockNumber, } @@ -377,11 +379,11 @@ pub mod pallet { fn get_price( asset: Self::AssetId, - _amount: Self::Balance, + amount: Self::Balance, ) -> Result, DispatchError> { let Price { price, block } = Prices::::try_get(asset).map_err(|_| Error::::PriceNotFound)?; - Ok(LastPrice { price, block }) + Ok(LastPrice { price: price.safe_mul(&amount)?, block }) } fn get_twap( diff --git a/frame/oracle/src/tests.rs b/frame/oracle/src/tests.rs index d8a8ce4801a..d5c3a5c94f8 100644 --- a/frame/oracle/src/tests.rs +++ b/frame/oracle/src/tests.rs @@ -3,6 +3,7 @@ use crate::{ AssetInfo, Error, PrePrice, Price, Withdraw, *, }; use codec::Decode; +use composable_traits::defi::CurrencyPair; use frame_support::{ assert_noop, assert_ok, traits::{Currency, OnInitialize}, @@ -14,7 +15,7 @@ use sp_io::TestExternalities; use sp_keystore::{testing::KeyStore, KeystoreExt, SyncCryptoStore}; use sp_runtime::{ traits::{BadOrigin, Zero}, - Percent, RuntimeAppPublic, + FixedPointNumber, FixedU128, Percent, RuntimeAppPublic, }; use std::sync::Arc; @@ -594,6 +595,51 @@ fn historic_pricing() { }); } +#[test] +fn price_of_amount() { + new_test_ext().execute_with(|| { + let value = 100500; + let id = 42; + let amount = 10000; + + let price = Price { price: value, block: System::block_number() }; + Prices::::insert(id, price); + let total_price = + ::get_price(id, amount).unwrap(); + + assert_eq!(total_price.price, value * amount) + }); +} +#[test] +fn ratio_human_case() { + new_test_ext().execute_with(|| { + let price = Price { price: 10000, block: System::block_number() }; + Prices::::insert(13, price); + let price = Price { price: 100, block: System::block_number() }; + Prices::::insert(42, price); + let mut pair = CurrencyPair::new(13, 42); + + let ratio = ::get_ratio(pair).unwrap(); + assert_eq!(ratio, FixedU128::saturating_from_integer(100)); + pair.reverse(); + let ratio = ::get_ratio(pair).unwrap(); + assert_eq!(ratio, FixedU128::saturating_from_rational(1_u32, 100_u32)); + }) +} + +#[test] +fn ratio_base_is_way_less_smaller() { + new_test_ext().execute_with(|| { + let price = Price { price: 1, block: System::block_number() }; + Prices::::insert(13, price); + let price = Price { price: 10_u128.pow(12), block: System::block_number() }; + Prices::::insert(42, price); + let pair = CurrencyPair::new(13, 42); + let ratio = ::get_ratio(pair).unwrap(); + assert_eq!(ratio, FixedU128::saturating_from_rational(1, 1000000000000_u64)); + }) +} + #[test] fn get_twap() { new_test_ext().execute_with(|| { From 99723d07f547c5b414c89bac8552d7ba329af419 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Sun, 16 Jan 2022 17:56:50 +0200 Subject: [PATCH 8/9] fixed comments of review Signed-off-by: Dzmitry Lahoda --- frame/composable-traits/src/defi.rs | 4 ++- frame/composable-traits/src/lending/mod.rs | 6 ++-- frame/lending/src/lib.rs | 29 ++++++++++----- frame/lending/src/tests.rs | 2 +- frame/liquidations/src/lib.rs | 41 ++++++++++------------ 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/frame/composable-traits/src/defi.rs b/frame/composable-traits/src/defi.rs index 63c49675661..51ec16dc5ac 100644 --- a/frame/composable-traits/src/defi.rs +++ b/frame/composable-traits/src/defi.rs @@ -73,9 +73,11 @@ impl Sell { pub struct CurrencyPair { /// 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. + /// Also can be named `price` currency. + /// Usually more stable, may be `borrowable` asset. pub quote: AssetId, } diff --git a/frame/composable-traits/src/lending/mod.rs b/frame/composable-traits/src/lending/mod.rs index 84cd9787c72..709fd9fdae6 100644 --- a/frame/composable-traits/src/lending/mod.rs +++ b/frame/composable-traits/src/lending/mod.rs @@ -19,8 +19,6 @@ pub type BorrowAmountOf = ::Balance; #[derive(Encode, Decode, Default, TypeInfo, Debug, Clone, PartialEq)] pub struct UpdateInput { - /// Reserve factor of market. - pub reserved_factor: Perquintill, /// Collateral factor of market pub collateral_factor: MoreThanOneFixedU128, /// warn borrower when loan's collateral/debt ratio @@ -39,6 +37,8 @@ pub struct CreateInput { /// 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 { @@ -50,7 +50,7 @@ impl CreateInput Perquintill { - self.updatable.reserved_factor + self.reserved_factor } } diff --git a/frame/lending/src/lib.rs b/frame/lending/src/lib.rs index 8fcf0a39031..2193ce85095 100644 --- a/frame/lending/src/lib.rs +++ b/frame/lending/src/lib.rs @@ -315,6 +315,7 @@ pub mod pallet { ExceedLendingCount, LiquidationFailed, BorrowerDataCalculationFailed, + Unauthorized, } #[pallet::event] @@ -511,17 +512,30 @@ pub mod pallet { Ok(().into()) } + /// owner must be very careful calling this #[pallet::weight(::WeightInfo::create_new_market())] #[transactional] pub fn update_market( origin: OriginFor, - _input: UpdateInput, + market_id: MarketIndex, + input: UpdateInput, ) -> DispatchResultWithPostInfo { - let _who = ensure_signed(origin)?; - // 1. validate owner - // 2. update configuration - // 3. what is owner updates and forces liquidations? ok. - Err(DispatchError::Other("not implemented").into()) + 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. @@ -1059,8 +1073,7 @@ pub mod pallet { strategies: [( Self::account_id(&market_id), // Borrowable = 100% - reserved - Perquintill::one() - .saturating_sub(config_input.updatable.reserved_factor), + Perquintill::one().saturating_sub(config_input.reserved_factor()), )] .iter() .cloned() diff --git a/frame/lending/src/tests.rs b/frame/lending/src/tests.rs index 34f97e5d752..058627bd788 100644 --- a/frame/lending/src/tests.rs +++ b/frame/lending/src/tests.rs @@ -61,12 +61,12 @@ fn create_market( ) -> (MarketIndex, BorrowAssetVault) { let config = CreateInput { updatable: UpdateInput { - reserved_factor: reserved, 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), }; ::create(manager, config).unwrap() diff --git a/frame/liquidations/src/lib.rs b/frame/liquidations/src/lib.rs index 745e397d068..32f43d42cc0 100644 --- a/frame/liquidations/src/lib.rs +++ b/frame/liquidations/src/lib.rs @@ -205,7 +205,6 @@ pub mod pallet { impl Liquidation for Pallet { type LiquidationStrategyId = T::LiquidationStrategyId; - type OrderId = T::OrderId; fn liquidate( @@ -213,30 +212,26 @@ pub mod pallet { order: Sell, configuration: Vec, ) -> Result { + let mut configuration = configuration; if configuration.is_empty() { - let configuration = Strategies::::get(DefaultStrategyIndex::::get()) - .expect("default always exists"); - match configuration { - LiquidationStrategyConfiguration::DutchAuction(configuration) => { + 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 T::DutchAuction::ask(from_to, order, configuration) - }, - _ => return Err(DispatchError::Other("TODO")), - } - } else { - 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("TODO")), - }; - - if result.is_ok() { - Self::deposit_event(Event::::PositionWasSentToLiquidation {}); - return result - } + return result } } } From f486cca9c81642e28fb15f858eb23e2793f5249f Mon Sep 17 00:00:00 2001 From: Dzmitry Lahoda Date: Mon, 17 Jan 2022 19:30:38 +0200 Subject: [PATCH 9/9] fixed comments Signed-off-by: Dzmitry Lahoda --- frame/composable-traits/src/defi.rs | 12 +++++++----- frame/composable-traits/src/lending/mod.rs | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frame/composable-traits/src/defi.rs b/frame/composable-traits/src/defi.rs index 35c881d4cea..433855bc9bc 100644 --- a/frame/composable-traits/src/defi.rs +++ b/frame/composable-traits/src/defi.rs @@ -59,10 +59,12 @@ impl Sell { @@ -74,7 +76,7 @@ pub struct CurrencyPair { pub quote: AssetId, } -/// Generically pair can be of external URI/location, not copy. +/// `AssetId` is Copy, than consider pair to be Copy impl Copy for CurrencyPair {} impl CurrencyPair { diff --git a/frame/composable-traits/src/lending/mod.rs b/frame/composable-traits/src/lending/mod.rs index 0a26afa2960..b0261d83df0 100644 --- a/frame/composable-traits/src/lending/mod.rs +++ b/frame/composable-traits/src/lending/mod.rs @@ -192,7 +192,7 @@ pub trait Lending { borrow_amount: Self::Balance, ) -> Result; - /// Returns the borrow limit for an account in normalized price. + /// Returns the borrow limit for an account in `Oracle` price. /// Calculation uses indexes from start of block time. /// Depends on overall collateral put by user into vault. /// This borrow limit of specific user, depends only on prices and users collateral, not on