diff --git a/Cargo.lock b/Cargo.lock index efacee828b6..cb99df40436 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1573,6 +1573,7 @@ name = "composable-traits" version = "0.0.1" dependencies = [ "bitflags", + "composable-support", "frame-support", "frame-system", "parity-scale-codec", @@ -6964,6 +6965,7 @@ dependencies = [ name = "pallet-lending" version = "0.0.1" dependencies = [ + "composable-support", "composable-tests-helpers", "composable-traits", "frame-benchmarking", @@ -6988,6 +6990,7 @@ dependencies = [ "proptest 0.9.6", "scale-info", "serde", + "smallvec 1.8.0", "sp-arithmetic", "sp-core", "sp-io", diff --git a/README.md b/README.md index a967484dd38..d8466303574 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,9 @@ [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/composablefi/composable)](https://github.com/composablefi/composable/tags) [![Twitter](https://img.shields.io/badge/Twitter-gray?logo=twitter)](https://twitter.com/ComposableFin) [![Discord](https://img.shields.io/badge/Discord-gray?logo=discord)](https://discord.gg/pFZn2GCn65) [![Telegram](https://img.shields.io/badge/Telegram-gray?logo=telegram)](https://t.me/ComposableFinanceAnnouncements) [![Medium](https://img.shields.io/badge/Medium-gray?logo=medium)](https://composablefi.medium.com/) -Picasso is our custom built kusama parachain, based on the substrate framework. +Picasso is our custom built Kusama parachain, based on the substrate framework. - - - -## Install +## Install For linux, FreeBSD, OpenBSD and macOS: @@ -26,7 +23,7 @@ git clone https://github.com/composableFi/composable cd composable/ sh scripts/init.sh cargo build --release -``` +``` or you can simply install it with this one liner: diff --git a/frame/bonded-finance/README.md b/frame/bonded-finance/README.md index 84f237d5feb..e7bf481fb9e 100644 --- a/frame/bonded-finance/README.md +++ b/frame/bonded-finance/README.md @@ -1,17 +1,3 @@ -# Bonded Finance Pallet +# Overview -## Overview - -A simple pallet providing means of submitting bond offers. - -## Interface - -This pallet implements the `BondedFinance` trait from `composable-traits`. - -## Dispatchable Functions - -- `offer` ― Register a new bond offer, allowing use to later bond it. -- `bond` ― Bond to an offer, the user should provide the number of contracts a user is willing - to buy. On offer completion (a.k.a. no more contract on the offer), the `stake` put by the creator is refunded. -- `cancel_offer` ― Cancel a running offer, blocking further bond but not cancelling the - currently vested rewards. The `stake` put by the creator is refunded. +A simple pallet providing means of submitting bond offers. \ No newline at end of file diff --git a/frame/bonded-finance/src/lib.rs b/frame/bonded-finance/src/lib.rs index cdca894ec45..50466fbc237 100644 --- a/frame/bonded-finance/src/lib.rs +++ b/frame/bonded-finance/src/lib.rs @@ -39,17 +39,21 @@ //! //! A simple pallet providing means of submitting bond offers. //! -//! ## Interface +//! Alice offers some bond priced in specific assets per bond as some amount of shares. +//! At same time she provides reward asset which will be vested into account which takes bond +//! offers. She locks some native currency to make the offer registered. +//! +//! Bob bonds parts of shares from offer by transfer some asset amount desired by Alice. +//! Bob will be vested part of reward amount during maturity period from that moment. +//! It the end of some period Bob may or may be not be return with amount he provided to Alice - +//! depends on how offer was setup. //! -//! This pallet implements the `BondedFinance` trait from `composable-traits`. +//! Alice may cancel offer and prevent new bonds on offer, she gets her native tokens back. +//! All existing vesting periods continue to be executed. //! -//! ## Dispatchable Functions +//! ## Interface //! -//! - `offer` - Register a new bond offer, allowing use to later bond it. -//! - `bond` - Bond to an offer, the user should provide the number of nb_of_bonds a user is willing -//! to buy. -//! - `cancel_offer` - Cancel a running offer, blocking further bond but not cancelling the -//! currently vested rewards. +//! This pallet implements the `composable_traits::bonded_finance::BondedFinance`. mod benchmarks; mod mock; @@ -194,7 +198,7 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Create a new offer. + /// Create a new offer. So later can `bond` it. /// /// The dispatch origin for this call must be _Signed_ and the sender must have the /// appropriate funds. @@ -208,6 +212,9 @@ pub mod pallet { } /// Bond to an offer. + /// And user should provide the number of contracts she is willing to buy. + /// On offer completion (a.k.a. no more contract on the offer), the `stake` put by the + /// creator is refunded. /// /// The dispatch origin for this call must be _Signed_ and the sender must have the /// appropriate funds. @@ -225,12 +232,12 @@ pub mod pallet { Ok(()) } - /// Cancel an offer. - /// + /// Cancel a running offer. Blocking further bond but not cancelling the + /// currently vested rewards. The `stake` put by the creator is refunded. /// The dispatch origin for this call must be _Signed_ and the sender must be `AdminOrigin` /// /// Emits a `OfferCancelled`. - #[pallet::weight(10_000)] + #[pallet::weight(10_000)] // TODO: add weights #[transactional] pub fn cancel(origin: OriginFor, offer_id: T::BondOfferId) -> DispatchResult { let (issuer, offer) = Self::get_offer(offer_id)?; @@ -315,7 +322,7 @@ pub mod pallet { nb_of_bonds <= offer.nb_of_bonds, Error::::InvalidNumberOfBonds ); - // NOTE(hussein-aitlahcen): can't overflow, subsumed by `offer.valid()` in + // can't overflow, subsumed by `offer.valid()` in // `do_offer` let value = nb_of_bonds * offer.bond_price; let reward_share = T::Convert::convert( @@ -355,7 +362,6 @@ pub mod pallet { )?; }, BondDuration::Infinite => { - // NOTE(hussein-aitlahcen): in the case of an inifite duration for // the offer, the liquidity is never returned to the bonder, meaning // that the protocol is now owning the funds. }, diff --git a/frame/bonded-finance/src/proptest-regressions/tests.txt b/frame/bonded-finance/src/proptest-regressions/tests.txt new file mode 100644 index 00000000000..58684d90a59 --- /dev/null +++ b/frame/bonded-finance/src/proptest-regressions/tests.txt @@ -0,0 +1 @@ +cc e005bce405358673d3249e53d51e474d4b135bdf8f2b0b82f94f525c0efbdbf8 #Test failed: 190000000 * 1000 / 190500000 = 997375328083989501 != 1000 at frame/bonded-finance/src/tests.rs:404; minimal failing input: offer = BondOffer { beneficiary: 1, asset: BTC, bond_price: 1000000, nb_of_bonds: 381, maturity: BondDuration::Infinite, reward: BondOfferReward { asset: ETH, amount: 381000000, maturity: 1 } } \ No newline at end of file diff --git a/frame/bonded-finance/src/tests.rs b/frame/bonded-finance/src/tests.rs index 1bdc6d25823..7e6b9a81d21 100644 --- a/frame/bonded-finance/src/tests.rs +++ b/frame/bonded-finance/src/tests.rs @@ -401,8 +401,8 @@ proptest! { prop_assert_ok!(charlie_reward); let charlie_reward = charlie_reward.expect("impossible; qed;"); - prop_assert_acceptable_computation_error!(bob_reward, half_reward); - prop_assert_acceptable_computation_error!(charlie_reward, half_reward); + prop_assert_acceptable_computation_error!(bob_reward, half_reward, 3); + prop_assert_acceptable_computation_error!(charlie_reward, half_reward, 3); prop_assert!(Tokens::can_withdraw(offer.reward.asset, &BOB, bob_reward) == WithdrawConsequence::Frozen); prop_assert!(Tokens::can_withdraw(offer.reward.asset, &CHARLIE, charlie_reward) == WithdrawConsequence::Frozen); diff --git a/frame/composable-support/Cargo.toml b/frame/composable-support/Cargo.toml index 6372561f79a..ffe7ba5372c 100644 --- a/frame/composable-support/Cargo.toml +++ b/frame/composable-support/Cargo.toml @@ -11,7 +11,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dev-dependencies] proptest = { version = "1.0" } -serde_json = "1.*" +serde_json = "1.0.45" [dependencies] frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } @@ -21,9 +21,8 @@ sp-runtime = { default-features = false, git = "https://github.com/paritytech/su sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } scale-info = { version = "1.0", default-features = false, features = ["derive"] } sorted-vec = "0.7.0" -serde = { version = "1.*", features = [ "derive" ], optional = true } -is_sorted = "0.1.1" - +serde = { version = "1.0.130", features = [ "derive" ], optional = true } +is_sorted = { version = "0.1.1", default-features = false } [dependencies.codec] default-features = false diff --git a/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs b/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs index 9d3559f9db4..c5d135f4679 100644 --- a/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs +++ b/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs @@ -5,7 +5,7 @@ use core::{ slice::SliceIndex, }; use frame_support::{traits::Get, BoundedVec}; -use sp_std::{convert::TryFrom, fmt, marker::PhantomData, prelude::*}; +use sp_std::{convert::TryFrom, marker::PhantomData, prelude::*}; /// A bounded, sorted vector. /// @@ -137,12 +137,12 @@ impl Default for BoundedSortedVec { } #[cfg(feature = "std")] -impl fmt::Debug for BoundedSortedVec +impl sp_std::fmt::Debug for BoundedSortedVec where - T: fmt::Debug, + T: sp_std::fmt::Debug, S: Get, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { f.debug_tuple("BoundedVec").field(&self.0).field(&Self::bound()).finish() } } diff --git a/frame/composable-support/src/validation.rs b/frame/composable-support/src/validation.rs index fbb98aaf147..d375d85c895 100644 --- a/frame/composable-support/src/validation.rs +++ b/frame/composable-support/src/validation.rs @@ -1,5 +1,6 @@ -use core::{marker::PhantomData, ops::Deref}; +use core::marker::PhantomData; use scale_info::TypeInfo; +use sp_runtime::DispatchError; /// Black box that embbed the validated value. #[derive(Default, Copy, Clone, PartialEq, Eq, Debug, TypeInfo)] @@ -8,54 +9,88 @@ pub struct Validated { _marker: PhantomData, } -impl Validated { - #[inline(always)] - pub fn value(&self) -> T { - self.value +impl Validated +where + Validated: Validate, +{ + pub fn new(value: T, _validator_tag: U) -> Result { + Validate::::validate(Self { value, _marker: PhantomData }) } } -impl Deref for Validated { - type Target = T; +pub trait ValidateDispatch: Sized { + fn validate(self) -> Result; +} + +pub trait Validate: Sized { + // use string here because in serde layer there is not dispatch + fn validate(self) -> Result; +} + +#[derive(Debug, Eq, PartialEq, Default)] +pub struct Valid; + +#[derive(Debug, Eq, PartialEq, Default)] +pub struct Invalid; + +impl Validate for T { #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.value + fn validate(self) -> Result { + Err("not valid") } } -impl AsRef for Validated { +impl Validate for T { #[inline(always)] - fn as_ref(&self) -> &T { - &self.value + fn validate(self) -> Result { + Ok(self) } } -pub trait Validate: Sized { - fn validate(self) -> Result; +impl + Validate, U, V> Validate<(U, V)> for T { + #[inline(always)] + fn validate(self) -> Result { + let value = Validate::::validate(self)?; + let value = Validate::::validate(value)?; + Ok(value) + } } -#[derive(Debug, Eq, PartialEq)] -pub struct QED; - -impl Validate for T { +// as per substrate pattern and existing macroses for similar purposes, they tend to make things +// flat like `#[impl_trait_for_tuples::impl_for_tuples(30)]` +// so if we will need more than 3, can consider it +impl + Validate + Validate, U, V, W> Validate<(U, V, W)> for T { #[inline(always)] fn validate(self) -> Result { - Ok(self) + let value = Validate::::validate(self)?; + let value = Validate::::validate(value)?; + let value = Validate::::validate(value)?; + Ok(value) } } -impl + Validate, U, V> Validate<(U, V)> for T { +impl + Validate + Validate + Validate, U, V, W, Z> Validate<(U, V, W, Z)> + for T +{ #[inline(always)] fn validate(self) -> Result { let value = Validate::::validate(self)?; let value = Validate::::validate(value)?; + let value = Validate::::validate(value)?; + let value = Validate::::validate(value)?; Ok(value) } } -impl, U, V> codec::Decode for Validated { +impl, U> Validated { + pub fn value(self) -> T { + self.value + } +} + +impl, U> codec::Decode for Validated { fn decode(input: &mut I) -> Result { - let value = Validate::validate(T::decode(input)?).map_err(Into::::into)?; + let value = Validate::::validate(T::decode(input)?)?; Ok(Validated { value, _marker: PhantomData }) } fn skip(input: &mut I) -> Result<(), codec::Error> { @@ -63,6 +98,21 @@ impl, U, V> codec::Decode for Validated Deref for Validated { + type Target = T; + #[doc(hidden)] + fn deref(&self) -> &Self::Target { + &self.value + } + } +} + impl, U> codec::WrapperTypeEncode for Validated { @@ -72,17 +122,23 @@ impl, U> codec::WrapperTypeEncode mod test { use super::*; use codec::{Decode, Encode}; + use frame_support::assert_ok; - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, Default)] struct ValidARange; - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, Default)] struct ValidBRange; - type CheckARange = (ValidARange, QED); - type CheckBRange = (ValidBRange, QED); - type CheckABRange = (ValidARange, (ValidBRange, QED)); + type CheckARangeTag = (ValidARange, Valid); + type CheckBRangeTag = (ValidBRange, Valid); + type CheckABRangeTag = (ValidARange, (ValidBRange, Valid)); + // note: next seems is not supported yet + // type NestedValidated = (Validated, Validated); + // #[derive(Debug, Eq, PartialEq, codec::Encode, codec::Decode, Default)] + // struct Y { + // } - #[derive(Debug, Eq, PartialEq, codec::Encode, codec::Decode)] + #[derive(Debug, Eq, PartialEq, codec::Encode, codec::Decode, Default, Clone)] struct X { a: u32, b: u32, @@ -108,13 +164,41 @@ mod test { } } + #[test] + fn nested_validator() { + let valid = X { a: 10, b: 0xCAFEBABE }; + + type ManyValidatorsTagsNested = (ValidARange, (ValidBRange, (Invalid, Valid))); + + assert!(Validate::::validate(valid).is_err()); + } + + #[test] + fn either_nested_or_flat() { + let valid = X { a: 10, b: 0xCAFEBABE }; + type ManyValidatorsTagsNested = (ValidARange, (ValidBRange, (Invalid, Valid))); + type ManyValidatorsTagsFlat = (ValidARange, ValidBRange, Invalid, Valid); + assert_eq!( + Validate::::validate(valid.clone()), + Validate::::validate(valid) + ); + } + + #[test] + fn value() { + let value = Validated::new(42, Valid); + assert_ok!(value); + let value = Validated::new(42, Invalid); + assert!(value.is_err()); + } + #[test] fn test_valid_a() { let valid = X { a: 10, b: 0xCAFEBABE }; let bytes = valid.encode(); assert_eq!( Ok(Validated { value: valid, _marker: PhantomData }), - Validated::::decode(&mut &bytes[..]) + Validated::::decode(&mut &bytes[..]) ); } @@ -122,7 +206,19 @@ mod test { fn test_invalid_a() { let invalid = X { a: 0xDEADC0DE, b: 0xCAFEBABE }; let bytes = invalid.encode(); - assert!(Validated::::decode(&mut &bytes[..]).is_err()); + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); + } + + #[test] + fn encode_decode_validated_encode_decode() { + let original = X { a: 0xDEADC0DE, b: 0xCAFEBABE }; + let bytes = original.encode(); + let wrapped = Validated::::decode(&mut &bytes[..]).unwrap(); + + let bytes = wrapped.encode(); + let reencoded = X::decode(&mut &bytes[..]).unwrap(); + assert_eq!(reencoded, original); } #[test] @@ -131,7 +227,7 @@ mod test { let bytes = valid.encode(); assert_eq!( Ok(Validated { value: valid, _marker: PhantomData }), - Validated::::decode(&mut &bytes[..]) + Validated::::decode(&mut &bytes[..]) ); } @@ -139,7 +235,8 @@ mod test { fn test_invalid_b() { let invalid = X { a: 0xCAFEBABE, b: 0xDEADC0DE }; let bytes = invalid.encode(); - assert!(Validated::::decode(&mut &bytes[..]).is_err()); + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); } #[test] @@ -148,7 +245,7 @@ mod test { let bytes = valid.encode(); assert_eq!( Ok(Validated { value: valid, _marker: PhantomData }), - Validated::::decode(&mut &bytes[..]) + Validated::::decode(&mut &bytes[..]) ); } @@ -156,20 +253,42 @@ mod test { fn test_invalid_ab() { let invalid = X { a: 0xDEADC0DE, b: 0xCAFEBABE }; let bytes = invalid.encode(); - assert!(Validated::::decode(&mut &bytes[..]).is_err()); + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); } #[test] fn test_invalid_a_ab() { let invalid = X { a: 0xDEADC0DE, b: 10 }; let bytes = invalid.encode(); - assert!(Validated::::decode(&mut &bytes[..]).is_err()); + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); } #[test] fn test_invalid_b_ab() { let invalid = X { a: 10, b: 0xDEADC0DE }; let bytes = invalid.encode(); - assert!(Validated::::decode(&mut &bytes[..]).is_err()); + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); + } + + #[test] + fn valid_triple() { + let value = X { a: 10, b: 0xDEADC0DE }; + let bytes = value.encode(); + assert_eq!( + Ok(Validated { value, _marker: PhantomData }), + Validated::::decode(&mut &bytes[..]) + ); + } + + #[test] + fn valid_invalid_valid() { + let value = X { a: 10, b: 0xDEADC0DE }; + let bytes = value.encode(); + + let invalid = Validated::::decode(&mut &bytes[..]); + assert!(invalid.is_err()); } } diff --git a/frame/composable-traits/Cargo.toml b/frame/composable-traits/Cargo.toml index 96f552d286d..747f7036e0d 100644 --- a/frame/composable-traits/Cargo.toml +++ b/frame/composable-traits/Cargo.toml @@ -17,6 +17,7 @@ sp-arithmetic = { default-features = false, git = "https://github.com/paritytech sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +composable-support = {default-features = false, path = "../composable-support"} scale-info = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = '1', optional = true } plotters = {version = "0.3.1", optional = true} diff --git a/frame/composable-traits/src/bonded_finance.rs b/frame/composable-traits/src/bonded_finance.rs index 9e7ccbc8510..96995dd5e0f 100644 --- a/frame/composable-traits/src/bonded_finance.rs +++ b/frame/composable-traits/src/bonded_finance.rs @@ -40,8 +40,9 @@ pub struct BondOffer { /// The account that will receive the locked assets. pub beneficiary: AccountId, /// Asset to be locked. Unlockable after `duration`. + /// Asset which `beneficiary` wants to get for his offer. pub asset: AssetId, - /// Price of a bond. + /// Price of a bond unit in `asset`. pub bond_price: Balance, /// Number of bonds. We use the Balance type for the sake of simplicity. pub nb_of_bonds: Balance, @@ -51,7 +52,7 @@ pub struct BondOffer { pub reward: BondOfferReward, } -/// The Bond reward. +/// The Bond reward. Asset and rules reward will be given. #[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub struct BondOfferReward { /// The actual reward asset. diff --git a/frame/composable-traits/src/defi.rs b/frame/composable-traits/src/defi.rs index e713cf534cb..05fe9c1bf56 100644 --- a/frame/composable-traits/src/defi.rs +++ b/frame/composable-traits/src/defi.rs @@ -83,6 +83,12 @@ pub struct CurrencyPair { pub quote: AssetId, } +impl From<(AssetId, AssetId)> for CurrencyPair { + fn from(other: (AssetId, AssetId)) -> Self { + Self { base: other.0, quote: other.1 } + } +} + /// `AssetId` is Copy, than consider pair to be Copy impl Copy for CurrencyPair {} @@ -234,6 +240,8 @@ pub type Ratio = FixedU128; #[cfg(test)] mod tests { + use crate::defi::LiftedFixedBalance; + use super::{Ratio, Take}; use sp_runtime::FixedPointNumber; diff --git a/frame/composable-traits/src/lending/math.rs b/frame/composable-traits/src/lending/math.rs index f5a46168fe1..cc4f1c02b02 100644 --- a/frame/composable-traits/src/lending/math.rs +++ b/frame/composable-traits/src/lending/math.rs @@ -1,6 +1,7 @@ use core::ops::Neg; use codec::{Decode, Encode}; +use composable_support::validation::Validate; use scale_info::TypeInfo; use sp_std::{cmp::Ordering, convert::TryInto}; @@ -111,6 +112,33 @@ impl InterestRateModel { } } +pub struct InteresteRateModelIsValid; +impl Validate for InterestRateModel { + fn validate(self) -> Result { + const ERROR: &str = "interest rate model is not valid"; + match self { + InterestRateModel::Jump(x) => + JumpModel::new(x.base_rate, x.jump_rate, x.full_rate, x.target_utilization) + .ok_or(ERROR) + .map(InterestRateModel::Jump), + InterestRateModel::Curve(x) => + CurveModel::new(x.base_rate).ok_or(ERROR).map(InterestRateModel::Curve), + InterestRateModel::DynamicPIDController(x) => DynamicPIDControllerModel::new( + x.proportional_parameter, + x.integral_parameter, + x.derivative_parameter, + x.previous_interest_rate, + x.target_utilization, + ) + .ok_or(ERROR) + .map(InterestRateModel::DynamicPIDController), + InterestRateModel::DoubleExponent(x) => DoubleExponentModel::new(x.coefficients) + .ok_or(ERROR) + .map(InterestRateModel::DoubleExponent), + } + } +} + // TODO: Use enum_dispatch crate impl InterestRate for InterestRateModel { /// Calculates the current borrow interest rate @@ -157,14 +185,13 @@ impl JumpModel { full_rate: ZeroToOneFixedU128, target_utilization: Percent, ) -> Option { - let model = Self { base_rate, jump_rate, full_rate, target_utilization }; - - if model.base_rate <= Self::MAX_BASE_RATE && - model.jump_rate <= Self::MAX_JUMP_RATE && - model.full_rate <= Self::MAX_FULL_RATE && - model.base_rate <= model.jump_rate && - model.jump_rate <= model.full_rate + if base_rate <= Self::MAX_BASE_RATE && + jump_rate <= Self::MAX_JUMP_RATE && + full_rate <= Self::MAX_FULL_RATE && + base_rate <= jump_rate && + jump_rate <= full_rate { + let model = Self { base_rate, jump_rate, full_rate, target_utilization }; Some(model) } else { None @@ -312,17 +339,21 @@ impl DynamicPIDControllerModel { integral_parameter: FixedI128, derivative_parameter: FixedI128, initial_interest_rate: FixedU128, - target_utilization: FixedU128, + target_utilization: ZeroToOneFixedU128, ) -> Option { - Some(DynamicPIDControllerModel { - proportional_parameter, - integral_parameter, - derivative_parameter, - previous_error_value: <_>::zero(), - previous_integral_term: <_>::zero(), - previous_interest_rate: initial_interest_rate, - target_utilization, - }) + if target_utilization > ZeroToOneFixedU128::one() { + None + } else { + Some(DynamicPIDControllerModel { + proportional_parameter, + integral_parameter, + derivative_parameter, + previous_error_value: <_>::zero(), + previous_integral_term: <_>::zero(), + previous_interest_rate: initial_interest_rate, + target_utilization, + }) + } } } diff --git a/frame/composable-traits/src/lending/mod.rs b/frame/composable-traits/src/lending/mod.rs index 98e74e00518..f97ecc3487d 100644 --- a/frame/composable-traits/src/lending/mod.rs +++ b/frame/composable-traits/src/lending/mod.rs @@ -7,9 +7,10 @@ use crate::{ defi::{CurrencyPair, DeFiEngine, MoreThanOneFixedU128}, time::Timestamp, }; +use composable_support::validation::Validate; use frame_support::{pallet_prelude::*, sp_runtime::Perquintill, sp_std::vec::Vec}; use scale_info::TypeInfo; -use sp_runtime::Percent; +use sp_runtime::{traits::One, Percent}; use self::math::*; @@ -41,6 +42,38 @@ pub struct CreateInput { pub reserved_factor: Perquintill, } +#[derive(Clone, Copy, Debug, PartialEq, TypeInfo)] +pub struct MarketModelValid; +#[derive(Clone, Copy, Debug, PartialEq, TypeInfo)] +pub struct CurrencyPairIsNotSame; + +impl Validate + for CreateInput +{ + fn validate(self) -> Result { + if self.updatable.collateral_factor < MoreThanOneFixedU128::one() { + return Err("collateral factor must be >=1") + } + + let interest_rate_model = + Validate::::validate(self.updatable.interest_rate_model)?; + + Ok(Self { updatable: UpdateInput { interest_rate_model, ..self.updatable }, ..self }) + } +} + +impl Validate + for CreateInput +{ + fn validate(self) -> Result { + if self.currency_pair.base == self.currency_pair.quote { + Err("currency pair must be different assets") + } else { + Ok(self) + } + } +} + impl CreateInput { pub fn borrow_asset(&self) -> AssetId { self.currency_pair.quote @@ -195,6 +228,7 @@ pub trait Lending: DeFiEngine { /// ``` fn accrue_interest(market_id: &Self::MarketId, now: Timestamp) -> Result<(), DispatchError>; + /// current borrowable balance of market fn total_cash(market_id: &Self::MarketId) -> Result; /// utilization_ratio = total_borrows / (total_cash + total_borrows). diff --git a/frame/composable-traits/src/oracle.rs b/frame/composable-traits/src/oracle.rs index fb38d00cbd4..6af79b70698 100644 --- a/frame/composable-traits/src/oracle.rs +++ b/frame/composable-traits/src/oracle.rs @@ -22,6 +22,8 @@ pub trait Oracle { /// Which is 0.01 of USDT. `Result::Err` is returned if `asset_id` not supported or price /// information not available. /// + /// Returns last price as it known. + /// /// # Normal assets /// /// Assuming we have a price `price` for an unit (not smallest) of `asset_id` in USDT cents. @@ -49,6 +51,9 @@ pub trait Oracle { /// price (Base BTC) = 5000000 /// price (Vaulted base stock_dilution_rate) = price base * stock_dilution_rate /// ``` + /// + /// Semantically this method is `get_ratio` for `asset_id` and price pegging asset multiplied by + /// `amount` fn get_price( asset_id: Self::AssetId, amount: Self::Balance, @@ -80,4 +85,12 @@ pub trait Oracle { /// let needed_base_for_quote = base_amount * ratio; // 300.0 /// ``` fn get_ratio(pair: CurrencyPair) -> Result; + + /// Given `asset_id` and `amount` of price asset. + /// Returns what amount of `asset_id` will be required to be same price as `amount` of + /// normalized currency + fn get_price_inverse( + asset_id: Self::AssetId, + amount: Self::Balance, + ) -> Result; } diff --git a/frame/composable-traits/src/vault.rs b/frame/composable-traits/src/vault.rs index 325809035de..91a9f6d6b05 100644 --- a/frame/composable-traits/src/vault.rs +++ b/frame/composable-traits/src/vault.rs @@ -75,6 +75,7 @@ pub trait Vault { /// underlying asset id fn asset_id(vault_id: &Self::VaultId) -> Result; + /// asset issues for underlying `asset_id` fn lp_asset_id(vault_id: &Self::VaultId) -> Result; fn account_id(vault: &Self::VaultId) -> Self::AccountId; @@ -144,12 +145,6 @@ pub trait CapabilityVault: Vault { fn deposits_allowed(vault_id: &Self::VaultId) -> Result; } -pub trait LpTokenVault { - type AssetId; - - fn lp_asset_id() -> Self::AssetId; -} - /// A vault which can be used by different strategies, such as pallets and smart contracts, to /// efficiently use capital. An example may be a vault which allocates 40% in a lending protocol, /// and 60% of the stored capital in a DEX. diff --git a/frame/dutch-auction/src/lib.rs b/frame/dutch-auction/src/lib.rs index c0656ed6f18..a8db60b4057 100644 --- a/frame/dutch-auction/src/lib.rs +++ b/frame/dutch-auction/src/lib.rs @@ -191,6 +191,7 @@ pub mod pallet { TakeParametersIsInvalid, TakeLimitDoesNotSatisfiesOrder, OrderNotFound, + NotEnoughNativeCurrentyToPayForAuction, } #[pallet::pallet] @@ -304,7 +305,9 @@ pub mod pallet { let deposit = T::WeightToFee::calc(&T::WeightInfo::liquidate()); >::transfer( from_to, treasury, deposit, true, - )?; + ) + .map_err(|_| Error::::NotEnoughNativeCurrentyToPayForAuction)?; + let now = T::UnixTime::now().as_secs(); let order = SellOf:: { from_to: from_to.clone(), @@ -312,6 +315,7 @@ pub mod pallet { order, context: Context:: { added_at: now, deposit }, }; + T::MultiCurrency::reserve(order.order.pair.base, from_to, order.order.take.amount)?; SellOrders::::insert(order_id, order); diff --git a/frame/lending/Cargo.toml b/frame/lending/Cargo.toml index afeada82071..eb469396769 100644 --- a/frame/lending/Cargo.toml +++ b/frame/lending/Cargo.toml @@ -30,6 +30,7 @@ sp-core = { default-features = false, git = "https://github.com/paritytech/subs sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } composable-traits = { default-features = false, path = "../composable-traits" } +composable-support = { default-features = false, path = "../composable-support" } pallet-oracle = { default-features = false, optional = true, version = "1.0.0", path = "../oracle" } pallet-vault = { default-features = false, path = "../vault", optional = true } @@ -43,7 +44,7 @@ serde = { version = '1.0.119' } hex-literal = "0.3.3" once_cell = "1.8.0" proptest = "0.9.6" - +smallvec = "1.7.0" 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 } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } diff --git a/frame/lending/lending.plantuml b/frame/lending/lending.plantuml index fd27b0c2492..b9653f5d3e2 100644 --- a/frame/lending/lending.plantuml +++ b/frame/lending/lending.plantuml @@ -1,5 +1,5 @@ @startuml - +TODO: update diagram actor Alice as a actor Bob as b actor Charlie as ac diff --git a/frame/lending/src/lib.rs b/frame/lending/src/lib.rs index 2193ce85095..f15c7b5d6b6 100644 --- a/frame/lending/src/lib.rs +++ b/frame/lending/src/lib.rs @@ -53,12 +53,14 @@ pub use crate::weights::WeightInfo; pub mod pallet { use crate::{models::BorrowerData, weights::WeightInfo}; use codec::Codec; + use composable_support::validation::Validated; use composable_traits::{ currency::CurrencyFactory, defi::*, lending::{ math::{self, *}, - BorrowAmountOf, CollateralLpAmountOf, CreateInput, Lending, MarketConfig, UpdateInput, + BorrowAmountOf, CollateralLpAmountOf, CreateInput, CurrencyPairIsNotSame, Lending, + MarketConfig, MarketModelValid, UpdateInput, }, liquidation::Liquidation, math::SafeArithmetic, @@ -70,11 +72,14 @@ pub mod pallet { pallet_prelude::*, storage::{with_transaction, TransactionOutcome}, traits::{ + fungible::{Inspect as NativeInspect, Transfer as NativeTransfer}, fungibles::{Inspect, InspectHold, Mutate, MutateHold, Transfer}, tokens::DepositConsequence, UnixTime, }, - transactional, PalletId, + transactional, + weights::WeightToFeePolynomial, + PalletId, }; use frame_system::{ offchain::{AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer}, @@ -118,7 +123,7 @@ pub mod pallet { handle_must_liquidate: u32, } - pub const PALLET_ID: PalletId = PalletId(*b"Lending!"); + //pub const PALLET_ID: PalletId = PalletId(*b"Lending!"); pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"lend"); pub const CRYPTO_KEY_TYPE: CryptoKeyTypeId = CryptoKeyTypeId(*b"lend"); @@ -173,19 +178,7 @@ pub mod pallet { type CurrencyFactory: CurrencyFactory<::MayBeAssetId>; - /// vault owned - can transfer, cannot mint - 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< + type MultiCurrency: Transfer< Self::AccountId, Balance = Self::Balance, AssetId = ::MayBeAssetId, @@ -216,6 +209,40 @@ pub mod pallet { /// Id of proxy to liquidate type LiquidationStrategyId: Parameter + Default + PartialEq + Clone + Debug + TypeInfo; + + /// Minimal price of borrow asset in Oracle price required to create + /// Creators puts that amount and it is staked under Vault account. + /// So he does not owns it anymore. + /// So borrow is both stake and tool to create market. + /// + /// # Why not pure borrow amount minimum? + /// + /// Borrow may have very small price. Will imbalance some markets on creation. + /// + /// # Why not native parachain token? + /// + /// Possible option. But I doubt closing market as easy as transferring back rent. So it is + /// not exactly platform rent only. + /// + /// # Why borrow amount priced by Oracle? + /// + /// We depend on Oracle to price in Lending. So we know price anyway. + /// We normalized price over all markets and protect from spam all possible pairs equally. + /// Locking borrow amount ensures manager can create market wit borrow assets, and we force + /// him to really create it. + /// + /// This solution forces to have amount before creating market. + /// Vault can take that amount if reconfigured so, but that may be changed during runtime + /// upgrades. + #[pallet::constant] + type MarketCreationStake: Get; + + #[pallet::constant] + type PalletId: Get; + type NativeCurrency: NativeTransfer + + NativeInspect; + /// Convert a weight value into a deductible fee based on the currency type. + type WeightToFee: WeightToFeePolynomial; } #[pallet::pallet] @@ -241,14 +268,6 @@ pub mod pallet { ::WeightInfo::handle_depositable(); weight += u64::from(call_counters.handle_must_liquidate) * ::WeightInfo::handle_must_liquidate(); - - // TODO: move following loop to OCW - for (market_id, account, _) in DebtIndex::::iter() { - if Self::liquidate_internal(&market_id, &account).is_ok() { - Self::deposit_event(Event::LiquidationInitiated { market_id, account }); - } - } - weight } @@ -260,9 +279,9 @@ pub mod pallet { return } for (market_id, account, _) in DebtIndex::::iter() { - let results = signer.send_signed_transaction(|_account| { - // call `liquidate` extrinsic - Call::liquidate { market_id, borrower: account.clone() } + let results = signer.send_signed_transaction(|_account| Call::liquidate { + market_id, + borrowers: vec![account.clone()], }); for (_acc, res) in &results { @@ -316,6 +335,8 @@ pub mod pallet { LiquidationFailed, BorrowerDataCalculationFailed, Unauthorized, + NotEnoughRent, + PriceOfInitialBorrowVaultShoyldBeGreaterThanZero, } #[pallet::event] @@ -360,7 +381,7 @@ pub mod pallet { /// Event emitted when a liquidation is initiated for a loan. LiquidationInitiated { market_id: MarketIndex, - account: T::AccountId, + borrowers: Vec, }, /// Event emitted to warn that loan may go under collaterized soon. SoonMayUnderCollaterized { @@ -429,6 +450,18 @@ pub mod pallet { OptionQuery, >; + #[pallet::storage] + #[pallet::getter(fn borrow_rent)] + pub type BorrowRent = StorageDoubleMap< + _, + Twox64Concat, + MarketIndex, + Twox64Concat, + T::AccountId, + T::Balance, + OptionQuery, + >; + /// market borrow index #[pallet::storage] #[pallet::getter(fn borrow_index)] @@ -499,9 +532,10 @@ pub mod pallet { #[transactional] pub fn create_market( origin: OriginFor, - input: CreateInput, + input: Validated, (MarketModelValid, CurrencyPairIsNotSame)>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + let input = input.value(); let (market_id, vault_id) = Self::create(who.clone(), input.clone())?; Self::deposit_event(Event::::MarketCreated { market_id, @@ -623,17 +657,17 @@ pub mod pallet { /// liquidation. /// - `origin` : Sender of this extrinsic. /// - `market_id` : Market index from which `borrower` has taken borrow. - #[pallet::weight(1000)] + #[pallet::weight(::WeightInfo::liquidate(borrowers.len() as Weight))] #[transactional] pub fn liquidate( origin: OriginFor, market_id: MarketIndex, - borrower: T::AccountId, + borrowers: Vec, ) -> DispatchResultWithPostInfo { - let _sender = ensure_signed(origin)?; + let sender = &(ensure_signed(origin)?); // TODO: should this be restricted to certain users? - Self::liquidate_internal(&market_id, &borrower)?; - Self::deposit_event(Event::LiquidationInitiated { market_id, account: borrower }); + Self::liquidate_internal(Some(sender), &market_id, borrowers.clone())?; + Self::deposit_event(Event::LiquidationInitiated { market_id, borrowers }); Ok(().into()) } } @@ -644,7 +678,7 @@ pub mod pallet { ) -> Result { let debt_asset_id = DebtMarkets::::get(market_id); let total_interest = - T::MarketDebtCurrency::balance(debt_asset_id, &Self::account_id(market_id)); + ::MultiCurrency::balance(debt_asset_id, &Self::account_id(market_id)); Ok(total_interest) } @@ -773,19 +807,37 @@ pub mod pallet { /// if liquidation is required and `liquidate` is successful then return `Ok(true)` /// if there is any error then propagate that error. pub fn liquidate_internal( + liquidator: Option<&::AccountId>, market_id: &::MarketId, - account: &::AccountId, + borrowers: Vec<::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 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)?; + for account in borrowers.iter() { + 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 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)?; + + if let Some(deposit) = BorrowRent::::get(market_id, account) { + let market_account = Self::account_id(market_id); + let liquidator = liquidator.unwrap_or(account); + ::NativeCurrency::transfer( + &market_account, + liquidator, + deposit, + false, + )?; + } + } } Ok(()) } @@ -886,7 +938,7 @@ pub mod pallet { ) -> Result<(), DispatchError> { let asset_id = ::asset_id(&config.borrow)?; let balance = - ::Currency::reducible_balance(asset_id, market_account, false) + ::MultiCurrency::reducible_balance(asset_id, market_account, false) .min(balance); ::deposit(&config.borrow, market_account, balance) } @@ -897,7 +949,7 @@ pub mod pallet { ) -> Result<(), DispatchError> { let asset_id = ::asset_id(&config.borrow)?; let balance = - ::Currency::reducible_balance(asset_id, market_account, false); + ::MultiCurrency::reducible_balance(asset_id, market_account, false); ::deposit(&config.borrow, market_account, balance) } @@ -923,15 +975,21 @@ pub mod pallet { debt_owner: &T::AccountId, amount_to_borrow: T::Balance, ) -> Result { + let debt_asset_id = DebtMarkets::::get(market_id); + 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(ZeroToOneFixedU128::zero); - let debt_asset_id = DebtMarkets::::get(market_id); - let existing_borrow_amount = T::MarketDebtCurrency::balance(debt_asset_id, debt_owner); + let existing_borrow_amount = + ::MultiCurrency::balance(debt_asset_id, debt_owner); + + // TODO: split out math from update, make update last step + ::MultiCurrency::mint_into(debt_asset_id, debt_owner, amount_to_borrow)?; + // TODO: decide if need to split out dept tracking vs debt token + ::MultiCurrency::hold(debt_asset_id, debt_owner, amount_to_borrow)?; - 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.safe_add(&amount_to_borrow)?; let existing_borrow_share = Percent::from_rational(existing_borrow_amount, total_borrow_amount); @@ -963,7 +1021,7 @@ pub mod pallet { Error::::NotEnoughCollateralToBorrowAmount ); ensure!( - ::Currency::can_withdraw( + ::MultiCurrency::can_withdraw( borrow_asset, market_account, amount_to_borrow @@ -972,6 +1030,17 @@ pub mod pallet { .is_ok(), Error::::NotEnoughBorrowAsset, ); + if !BorrowRent::::contains_key(market_id, debt_owner) { + let deposit = T::WeightToFee::calc(&T::WeightInfo::liquidate(1)); + + ensure!( + ::NativeCurrency::can_withdraw(debt_owner, deposit,) + .into_result() + .is_ok(), + Error::::NotEnoughRent, + ); + } + ensure!( !matches!( T::Vault::available_funds(&market.borrow, market_account)?, @@ -1006,15 +1075,19 @@ pub mod pallet { ); ensure!(repay_amount <= owed, Error::::CannotRepayMoreThanBorrowAmount); ensure!( - ::Currency::can_withdraw(borrow_asset_id, from, repay_amount) + ::MultiCurrency::can_withdraw(borrow_asset_id, from, repay_amount) .into_result() .is_ok(), Error::::CannotWithdrawFromProvidedBorrowAccount ); ensure!( - ::Currency::can_deposit(borrow_asset_id, market_account, repay_amount) - .into_result() - .is_ok(), + ::MultiCurrency::can_deposit( + borrow_asset_id, + market_account, + repay_amount + ) + .into_result() + .is_ok(), Error::::TransferFailed ); @@ -1081,6 +1154,30 @@ pub mod pallet { }, )?; + let initial_price_amount = T::MarketCreationStake::get(); + let initial_pool_size = T::Oracle::get_price_inverse( + config_input.borrow_asset(), + initial_price_amount, + )?; + ensure!( + initial_pool_size > T::Balance::zero(), + Error::::PriceOfInitialBorrowVaultShoyldBeGreaterThanZero + ); + T::MultiCurrency::transfer( + config_input.borrow_asset(), + &manager, + &Self::account_id(&market_id), + initial_pool_size, + false, + )?; + + // TODO: discuss on why we do not reposit all amount to vault + // ::deposit( + // &borrow_asset_vault, + // &Self::account_id(&market_id), + // initial_pool_size, + // )?; + let config = MarketConfig { manager, borrow: borrow_asset_vault.clone(), @@ -1103,7 +1200,7 @@ pub mod pallet { } fn account_id(market_id: &Self::MarketId) -> Self::AccountId { - PALLET_ID.into_sub_account(market_id) + T::PalletId::get().into_sub_account(market_id) } fn get_markets_for_borrow(borrow: Self::VaultId) -> Vec { @@ -1136,7 +1233,7 @@ pub mod pallet { let new_account_interest_index = Self::updated_account_interest_index(market_id, debt_owner, amount_to_borrow)?; - ::Currency::transfer( + ::MultiCurrency::transfer( borrow_asset, &market_account, debt_owner, @@ -1146,9 +1243,21 @@ pub mod pallet { DebtIndex::::insert(market_id, debt_owner, new_account_interest_index); BorrowTimestamp::::insert(market_id, debt_owner, Self::last_block_timestamp()); + if !BorrowRent::::contains_key(market_id, debt_owner) { + let deposit = T::WeightToFee::calc(&T::WeightInfo::liquidate(2)); + ::NativeCurrency::transfer( + debt_owner, + &market_account, + deposit, + true, + )?; + BorrowRent::::insert(market_id, debt_owner, deposit); + } + Ok(()) } + /// must be called in transaction fn repay_borrow( market_id: &Self::MarketId, from: &Self::AccountId, @@ -1173,50 +1282,60 @@ pub mod pallet { let debt_asset_id = DebtMarkets::::get(market_id); - let burn_amount = ::Currency::balance(debt_asset_id, beneficiary); + let burn_amount = ::MultiCurrency::balance(debt_asset_id, beneficiary); let mut remaining_borrow_amount = - T::MarketDebtCurrency::balance(debt_asset_id, &market_account); - if total_repay_amount <= burn_amount { - // only repay interest - T::MarketDebtCurrency::release( - debt_asset_id, - beneficiary, - total_repay_amount, - true, - ) - .expect("can always release held debt balance"); - T::MarketDebtCurrency::burn_from( - debt_asset_id, - beneficiary, - total_repay_amount, - ) - .expect("can always burn debt balance"); + ::MultiCurrency::balance(debt_asset_id, &market_account); + + // BUG: so each time we repay, we must burn from market and from account, evidently + // this is not case now NOTE: we do not ++ borrow on each user, but on market total, + // so that there gas burn too much, so real borrow is borrow * (market index / + // borrower index) TODO: cover relation with test and fix it + let debt_to_release = if total_repay_amount <= burn_amount { + total_repay_amount } else { let repay_borrow_amount = total_repay_amount - burn_amount; - remaining_borrow_amount -= repay_borrow_amount; - T::MarketDebtCurrency::burn_from(debt_asset_id, &market_account, repay_borrow_amount).expect( - "debt balance of market must be of parts of debts of borrowers and can reduce it", - ); - T::MarketDebtCurrency::release(debt_asset_id, beneficiary, burn_amount, true) - .expect("can always release held debt balance"); - T::MarketDebtCurrency::burn_from(debt_asset_id, beneficiary, burn_amount) - .expect("can always burn debt balance"); - } - // TODO: fuzzing is must to uncover cases when sum != total - ::Currency::transfer( + ::MultiCurrency::burn_from( + debt_asset_id, + &market_account, + repay_borrow_amount, + )?; + burn_amount + }; + + // release_and_burn + ::MultiCurrency::release( + debt_asset_id, + beneficiary, + debt_to_release, + true, + )?; + ::MultiCurrency::burn_from( + debt_asset_id, + beneficiary, + debt_to_release, + )?; + + ::MultiCurrency::transfer( borrow_asset_id, from, &market_account, - total_repay_amount, + debt_to_release, false, - ) - .expect("must be able to transfer because of above checks"); + )?; if remaining_borrow_amount == T::Balance::zero() { BorrowTimestamp::::remove(market_id, beneficiary); DebtIndex::::remove(market_id, beneficiary); + if let Some(rent) = BorrowRent::::get(market_id, beneficiary) { + ::NativeCurrency::transfer( + &market_account, + beneficiary, + rent, + false, + )?; + } } } @@ -1226,8 +1345,8 @@ pub mod pallet { fn total_borrows(market_id: &Self::MarketId) -> Result { let debt_asset_id = DebtMarkets::::get(market_id); let accrued_debt = - T::MarketDebtCurrency::balance(debt_asset_id, &Self::account_id(market_id)); - let total_issued = T::MarketDebtCurrency::total_issuance(debt_asset_id); + ::MultiCurrency::balance(debt_asset_id, &Self::account_id(market_id)); + let total_issued = ::MultiCurrency::total_issuance(debt_asset_id); let total_borrows = total_issued - accrued_debt; Ok(total_borrows) } @@ -1235,14 +1354,14 @@ pub mod pallet { 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)); + ::MultiCurrency::balance(debt_asset_id, &Self::account_id(market_id)); Ok(total_interest) } fn total_cash(market_id: &Self::MarketId) -> Result { let market = Self::get_market(market_id)?; let borrow_id = T::Vault::asset_id(&market.borrow)?; - Ok(::Currency::balance(borrow_id, &Self::account_id(market_id))) + Ok(::MultiCurrency::balance(borrow_id, &Self::account_id(market_id))) } fn calc_utilization_ratio( @@ -1282,7 +1401,11 @@ pub mod pallet { )?; BorrowIndex::::insert(market_id, borrow_index_new); - T::MarketDebtCurrency::mint_into(debt_asset_id, &Self::account_id(market_id), accrued)?; + ::MultiCurrency::mint_into( + debt_asset_id, + &Self::account_id(market_id), + accrued, + )?; Ok(()) } @@ -1297,7 +1420,8 @@ pub mod pallet { let account_debt = DebtIndex::::get(market_id, account); match account_debt { Some(account_interest_index) => { - let principal = T::MarketDebtCurrency::balance_on_hold(debt_asset_id, account); + let principal = + ::MultiCurrency::balance_on_hold(debt_asset_id, account); let market_interest_index = Self::get_borrow_index(market_id)?; let balance = borrow_from_principal::( @@ -1363,15 +1487,18 @@ pub mod pallet { let market = Self::get_market(market_id)?; let market_account = Self::account_id(market_id); ensure!( - ::Currency::can_withdraw(market.collateral, account, amount) + ::MultiCurrency::can_withdraw(market.collateral, account, amount) .into_result() .is_ok(), Error::::TransferFailed ); ensure!( - ::Currency::can_deposit(market.collateral, &market_account, amount) == - DepositConsequence::Success, + ::MultiCurrency::can_deposit( + market.collateral, + &market_account, + amount + ) == DepositConsequence::Success, Error::::TransferFailed ); @@ -1383,7 +1510,7 @@ pub mod pallet { collateral_balance.replace(new_collateral_balance); Result::<(), Error>::Ok(()) })?; - ::Currency::transfer( + ::MultiCurrency::transfer( market.collateral, account, &market_account, @@ -1429,14 +1556,18 @@ pub mod pallet { let market_account = Self::account_id(market_id); ensure!( - ::Currency::can_deposit(market.collateral, account, amount) == + ::MultiCurrency::can_deposit(market.collateral, account, amount) == DepositConsequence::Success, Error::::TransferFailed ); ensure!( - ::Currency::can_withdraw(market.collateral, &market_account, amount) - .into_result() - .is_ok(), + ::MultiCurrency::can_withdraw( + market.collateral, + &market_account, + amount + ) + .into_result() + .is_ok(), Error::::TransferFailed ); @@ -1448,7 +1579,7 @@ pub mod pallet { collateral_balance.replace(new_collateral_balance); Result::<(), Error>::Ok(()) })?; - ::Currency::transfer( + ::MultiCurrency::transfer( market.collateral, &market_account, account, diff --git a/frame/lending/src/mocks/currency.rs b/frame/lending/src/mocks/currency.rs new file mode 100644 index 00000000000..0db1e32f3cd --- /dev/null +++ b/frame/lending/src/mocks/currency.rs @@ -0,0 +1,80 @@ +use crate::{self as pallet_lending, *}; +use composable_traits::{ + currency::DynamicCurrencyId, + defi::DeFiComposableConfig, + governance::{GovernanceRegistry, SignedRawOrigin}, +}; +use frame_support::{ + ord_parameter_types, parameter_types, + traits::{Everything, OnFinalize, OnInitialize}, + PalletId, +}; +use frame_system::EnsureSignedBy; +use hex_literal::hex; +use once_cell::sync::Lazy; +use orml_traits::{parameter_type_with_key, GetByKey}; +use scale_info::TypeInfo; +use sp_arithmetic::traits::Zero; +use sp_core::{sr25519::Signature, H256}; +use sp_runtime::{ + testing::{Header, TestXt}, + traits::{ + BlakeTwo256, ConvertInto, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify, + }, + ArithmeticError, DispatchError, +}; + +#[derive( + PartialOrd, + Ord, + PartialEq, + Eq, + Debug, + Copy, + Clone, + codec::Encode, + codec::Decode, + serde::Serialize, + serde::Deserialize, + TypeInfo, +)] +#[allow(clippy::upper_case_acronyms)] // currencies should be CONSTANT_CASE +pub enum CurrencyId { + PICA, + BTC, + ETH, + LTC, + USDT, + LpToken(u128), +} + +impl From for CurrencyId { + fn from(id: u128) -> Self { + match id { + 0 => CurrencyId::PICA, + 1 => CurrencyId::BTC, + 2 => CurrencyId::ETH, + 3 => CurrencyId::LTC, + 4 => CurrencyId::USDT, + 5 => CurrencyId::LpToken(0), + _ => unreachable!(), + } + } +} + +impl Default for CurrencyId { + fn default() -> Self { + CurrencyId::PICA + } +} + +impl DynamicCurrencyId for CurrencyId { + fn next(self) -> Result { + match self { + CurrencyId::LpToken(x) => Ok(CurrencyId::LpToken( + x.checked_add(1).ok_or(DispatchError::Arithmetic(ArithmeticError::Overflow))?, + )), + _ => unreachable!(), + } + } +} diff --git a/frame/lending/src/mocks/mod.rs b/frame/lending/src/mocks/mod.rs index 2cc01102b07..6228295fa8a 100644 --- a/frame/lending/src/mocks/mod.rs +++ b/frame/lending/src/mocks/mod.rs @@ -1,3 +1,4 @@ +use self::currency::CurrencyId; use crate::{self as pallet_lending, *}; use composable_traits::{ currency::DynamicCurrencyId, @@ -6,7 +7,8 @@ use composable_traits::{ }; use frame_support::{ ord_parameter_types, parameter_types, - traits::{Everything, OnFinalize, OnInitialize}, + traits::{Everything, GenesisBuild, OnFinalize, OnInitialize}, + weights::{WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial}, PalletId, }; use frame_system::EnsureSignedBy; @@ -14,6 +16,7 @@ use hex_literal::hex; use once_cell::sync::Lazy; use orml_traits::{parameter_type_with_key, GetByKey}; use scale_info::TypeInfo; +use smallvec::smallvec; use sp_arithmetic::traits::Zero; use sp_core::{sr25519::Signature, H256}; use sp_runtime::{ @@ -21,9 +24,10 @@ use sp_runtime::{ traits::{ BlakeTwo256, ConvertInto, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify, }, - ArithmeticError, DispatchError, + ArithmeticError, DispatchError, Perbill, }; +pub mod currency; pub mod oracle; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -58,61 +62,6 @@ pub static UNRESERVED: Lazy = Lazy::new(|| { AccountId::from_raw(hex!("0000000000000000000000000000000000000000000000000000000000000003")) }); -#[derive( - PartialOrd, - Ord, - PartialEq, - Eq, - Debug, - Copy, - Clone, - codec::Encode, - codec::Decode, - serde::Serialize, - serde::Deserialize, - TypeInfo, -)] -#[allow(clippy::upper_case_acronyms)] // currencies should be CONSTANT_CASE -pub enum MockCurrencyId { - PICA, - BTC, - ETH, - LTC, - USDT, - LpToken(u128), -} - -impl From for MockCurrencyId { - fn from(id: u128) -> Self { - match id { - 0 => MockCurrencyId::PICA, - 1 => MockCurrencyId::BTC, - 2 => MockCurrencyId::ETH, - 3 => MockCurrencyId::LTC, - 4 => MockCurrencyId::USDT, - 5 => MockCurrencyId::LpToken(0), - _ => unreachable!(), - } - } -} - -impl Default for MockCurrencyId { - fn default() -> Self { - MockCurrencyId::PICA - } -} - -impl DynamicCurrencyId for MockCurrencyId { - fn next(self) -> Result { - match self { - MockCurrencyId::LpToken(x) => Ok(MockCurrencyId::LpToken( - x.checked_add(1).ok_or(DispatchError::Arithmetic(ArithmeticError::Overflow))?, - )), - _ => unreachable!(), - } - } -} - // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where @@ -195,18 +144,18 @@ impl pallet_timestamp::Config for Test { } parameter_types! { - pub const DynamicCurrencyIdInitial: MockCurrencyId = MockCurrencyId::LpToken(0); + pub const DynamicCurrencyIdInitial: CurrencyId = CurrencyId::LpToken(0); } impl pallet_currency_factory::Config for Test { type Event = Event; - type DynamicCurrencyId = MockCurrencyId; + type DynamicCurrencyId = CurrencyId; type DynamicCurrencyIdInitial = DynamicCurrencyIdInitial; } parameter_types! { pub const MaxStrategies: usize = 255; - pub const NativeAssetId: MockCurrencyId = MockCurrencyId::PICA; + pub const NativeAssetId: CurrencyId = CurrencyId::PICA; pub const CreationDeposit: Balance = 10; pub const RentPerBlock: Balance = 1; pub const MinimumDeposit: Balance = 0; @@ -218,7 +167,7 @@ parameter_types! { impl pallet_vault::Config for Test { type Event = Event; type Currency = Tokens; - type AssetId = MockCurrencyId; + type AssetId = CurrencyId; type Balance = Balance; type MaxStrategies = MaxStrategies; type CurrencyFactory = LpTokenFactory; @@ -236,7 +185,7 @@ impl pallet_vault::Config for Test { } parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: MockCurrencyId| -> Balance { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { Zero::zero() }; } @@ -249,7 +198,7 @@ impl orml_tokens::Config for Test { type Event = Event; type Balance = Balance; type Amount = Amount; - type CurrencyId = MockCurrencyId; + type CurrencyId = CurrencyId; type WeightInfo = (); type ExistentialDeposits = ExistentialDeposits; type OnDust = (); @@ -261,18 +210,18 @@ ord_parameter_types! { pub const RootAccount: AccountId = *ALICE; } -impl GovernanceRegistry for () { - fn set(_k: MockCurrencyId, _value: composable_traits::governance::SignedRawOrigin) {} +impl GovernanceRegistry for () { + fn set(_k: CurrencyId, _value: composable_traits::governance::SignedRawOrigin) {} } impl GetByKey< - MockCurrencyId, + CurrencyId, Result, sp_runtime::DispatchError>, > for () { fn get( - _k: &MockCurrencyId, + _k: &CurrencyId, ) -> Result, sp_runtime::DispatchError> { Ok(SignedRawOrigin::Root) } @@ -281,7 +230,7 @@ impl impl pallet_assets::Config for Test { type NativeAssetId = NativeAssetId; type GenerateCurrencyId = LpTokenFactory; - type AssetId = MockCurrencyId; + type AssetId = CurrencyId; type Balance = Balance; type NativeCurrency = Balances; type MultiCurrency = Tokens; @@ -296,7 +245,7 @@ impl crate::mocks::oracle::Config for Test { } impl DeFiComposableConfig for Test { - type MayBeAssetId = MockCurrencyId; + type MayBeAssetId = CurrencyId; type Balance = Balance; } @@ -325,11 +274,17 @@ impl pallet_dutch_auction::weights::WeightInfo for DutchAuctionsMocks { } } -impl frame_support::weights::WeightToFeePolynomial for DutchAuctionsMocks { +impl WeightToFeePolynomial for DutchAuctionsMocks { type Balance = Balance; - fn polynomial() -> frame_support::weights::WeightToFeeCoefficients { - todo!("will replace with mocks from relevant pallet") + fn polynomial() -> WeightToFeeCoefficients { + let one = WeightToFeeCoefficient { + degree: 1, + coeff_frac: Perbill::zero(), + coeff_integer: WEIGHT_TO_FEE.with(|v| *v.borrow()), + negative: false, + }; + smallvec![one] } } @@ -387,6 +342,25 @@ where parameter_types! { pub const MaxLendingCount: u32 = 10; + pub LendingPalletId: PalletId = PalletId(*b"liqiudat"); + pub MarketCreationStake : Balance = 10^15; +} + +parameter_types! { + pub static WeightToFee: Balance = 1; +} +impl WeightToFeePolynomial for WeightToFee { + type Balance = Balance; + + fn polynomial() -> WeightToFeeCoefficients { + let one = WeightToFeeCoefficient { + degree: 1, + coeff_frac: Perbill::zero(), + coeff_integer: WEIGHT_TO_FEE.with(|v| *v.borrow()), + negative: false, + }; + smallvec![one] + } } impl pallet_lending::Config for Test { @@ -394,21 +368,25 @@ impl pallet_lending::Config for Test { type VaultId = VaultId; type Vault = Vault; type Event = Event; - type Currency = Tokens; + type NativeCurrency = Balances; + type MultiCurrency = Tokens; type CurrencyFactory = LpTokenFactory; - type MarketDebtCurrency = Tokens; type Liquidation = Liquidations; type UnixTime = Timestamp; type MaxLendingCount = MaxLendingCount; type AuthorityId = crypto::TestAuthId; type WeightInfo = (); type LiquidationStrategyId = LiquidationStrategyId; + type PalletId = LendingPalletId; + type MarketCreationStake = MarketCreationStake; + + type WeightToFee = WeightToFee; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let balances = vec![]; + let balances = vec![(*ALICE, 1_000_000_000), (*BOB, 1_000_000_000), (*CHARLIE, 1_000_000_000)]; pallet_balances::GenesisConfig:: { balances } .assimilate_storage(&mut storage) @@ -416,6 +394,8 @@ pub fn new_test_ext() -> sp_io::TestExternalities { pallet_lending::GenesisConfig {} .assimilate_storage::(&mut storage) .unwrap(); + GenesisBuild::::assimilate_storage(&pallet_liquidations::GenesisConfig {}, &mut storage) + .unwrap(); let mut ext = sp_io::TestExternalities::new(storage); ext.execute_with(|| { diff --git a/frame/lending/src/mocks/oracle.rs b/frame/lending/src/mocks/oracle.rs index 05c16d04f26..7279858c192 100644 --- a/frame/lending/src/mocks/oracle.rs +++ b/frame/lending/src/mocks/oracle.rs @@ -5,21 +5,23 @@ pub mod pallet { use codec::Codec; use composable_traits::{ currency::LocalAssets, + math::SafeArithmetic, oracle::{Oracle, Price}, vault::Vault, }; use frame_support::pallet_prelude::*; use sp_runtime::{ helpers_128bit::multiply_by_rational, ArithmeticError, DispatchError, FixedPointNumber, + FixedU128, }; use sp_std::fmt::Debug; - use crate::mocks::{Balance, MockCurrencyId}; + use crate::mocks::{currency::CurrencyId, Balance}; #[pallet::config] pub trait Config: frame_system::Config { type VaultId: Clone + Codec + Debug + PartialEq + Default + Parameter; - type Vault: Vault; + type Vault: Vault; } #[pallet::pallet] @@ -35,7 +37,7 @@ pub mod pallet { impl Pallet { pub fn get_price( - asset: MockCurrencyId, + asset: CurrencyId, amount: Balance, ) -> Result, DispatchError> { ::get_price(asset, amount) @@ -46,7 +48,7 @@ pub mod pallet { } impl Oracle for Pallet { - type AssetId = MockCurrencyId; + type AssetId = CurrencyId; type Balance = Balance; type Timestamp = (); type LocalAssets = (); @@ -70,11 +72,11 @@ pub mod pallet { Ideally we would have all the static currency quoted against USD cents on chain. So that we would be able to derive LP tokens price. */ - MockCurrencyId::USDT => Ok(Price { price: amount, block: () }), - MockCurrencyId::PICA => derive_price(10_00, amount), - MockCurrencyId::BTC => derive_price(Self::btc_value(), amount), - MockCurrencyId::ETH => derive_price(3400_00, amount), - MockCurrencyId::LTC => derive_price(180_00, amount), + CurrencyId::USDT => Ok(Price { price: amount, block: () }), + CurrencyId::PICA => derive_price(10_00, amount), + CurrencyId::BTC => derive_price(Self::btc_value(), amount), + CurrencyId::ETH => derive_price(3400_00, amount), + CurrencyId::LTC => derive_price(180_00, amount), /* NOTE(hussein-aitlahcen) If we want users to be able to consider LP tokens as currency, @@ -87,7 +89,7 @@ pub mod pallet { One exception can occur if the LP token hasn't been generated by a vault. */ - x @ MockCurrencyId::LpToken(_) => { + x @ CurrencyId::LpToken(_) => { let vault = T::Vault::token_vault(x)?; let base = T::Vault::asset_id(&vault)?; let Price { price, block } = Self::get_price(base, amount)?; @@ -108,9 +110,22 @@ pub mod pallet { } fn get_ratio( - _pair: composable_traits::defi::CurrencyPair, + pair: composable_traits::defi::CurrencyPair, ) -> Result { - Err(DispatchError::Other("No implemented")) + let base: u128 = Self::get_price(pair.base, (10_u32 ^ 12).into())?.price.into(); + let quote: u128 = Self::get_price(pair.quote, (10_u32 ^ 12).into())?.price.into(); + let base = FixedU128::saturating_from_integer(base); + let quote = FixedU128::saturating_from_integer(quote); + Ok(base.safe_div("e)?) + } + + fn get_price_inverse( + asset_id: Self::AssetId, + amount: Self::Balance, + ) -> Result { + let price = Self::get_price(asset_id, 10 ^ 12)?; + let inversed = amount / price.price / 10 ^ 12; + Ok(inversed) } } } diff --git a/frame/lending/src/tests.rs b/frame/lending/src/tests.rs index 402a066bbce..dc6c38177ac 100644 --- a/frame/lending/src/tests.rs +++ b/frame/lending/src/tests.rs @@ -3,9 +3,9 @@ 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, + currency::CurrencyId, new_test_ext, process_block, AccountId, Balance, BlockNumber, + Lending, Oracle, Origin, Test, Tokens, Vault, VaultId, ALICE, BOB, CHARLIE, + MILLISECS_PER_BLOCK, MINIMUM_BALANCE, UNRESERVED, }, models::BorrowerData, Error, MarketIndex, @@ -29,16 +29,17 @@ use sp_runtime::{ArithmeticError, FixedPointNumber, Percent, Perquintill}; type BorrowAssetVault = VaultId; -type CollateralAsset = MockCurrencyId; +type CollateralAsset = CurrencyId; 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; +const INITIAL_BORROW_ASSET_AMOUNT: u128 = 10 ^ 30; /// Create a very simple vault for the given currency, 100% is reserved. fn create_simple_vault( - asset_id: MockCurrencyId, -) -> (VaultId, VaultInfo) { + asset_id: CurrencyId, +) -> (VaultId, VaultInfo) { let v = Vault::do_create_vault( Deposit::Existential, VaultConfig { @@ -53,8 +54,8 @@ fn create_simple_vault( } fn create_market( - borrow_asset: MockCurrencyId, - collateral_asset: MockCurrencyId, + borrow_asset: CurrencyId, + collateral_asset: CurrencyId, manager: AccountId, reserved: Perquintill, collateral_factor: MoreThanOneFixedU128, @@ -69,16 +70,17 @@ fn create_market( reserved_factor: reserved, currency_pair: CurrencyPair::new(collateral_asset, borrow_asset), }; + Tokens::mint_into(borrow_asset, &manager, 1_000_000_000).unwrap(); ::create(manager, config).unwrap() } /// Create a market with a USDT vault LP token as collateral fn create_simple_vaulted_market() -> ((MarketIndex, BorrowAssetVault), CollateralAsset) { let (_collateral_vault, VaultInfo { lp_token_id: collateral_asset, .. }) = - create_simple_vault(MockCurrencyId::USDT); + create_simple_vault(CurrencyId::USDT); ( create_market( - MockCurrencyId::BTC, + CurrencyId::BTC, collateral_asset, *ALICE, DEFAULT_MARKET_VAULT_RESERVE, @@ -91,8 +93,8 @@ fn create_simple_vaulted_market() -> ((MarketIndex, BorrowAssetVault), Collatera /// Create a market with straight USDT as collateral fn create_simple_market() -> (MarketIndex, BorrowAssetVault) { create_market( - MockCurrencyId::BTC, - MockCurrencyId::USDT, + CurrencyId::BTC, + CurrencyId::USDT, *ALICE, DEFAULT_MARKET_VAULT_RESERVE, MoreThanOneFixedU128::saturating_from_rational(DEFAULT_COLLATERAL_FACTOR * 100, 100), @@ -283,20 +285,59 @@ fn accrue_interest_plotter() { } } +#[test] +fn can_create_valid_market() { + new_test_ext().execute_with(|| { + let borrow_asset = CurrencyId::BTC; + let collateral_asset = CurrencyId::USDT; + let manager = *ALICE; + let collateral_factor = + MoreThanOneFixedU128::saturating_from_rational(DEFAULT_COLLATERAL_FACTOR * 100, 100); + let config = CreateInput { + updatable: UpdateInput { + collateral_factor, + under_collaterized_warn_percent: Percent::from_float(0.10), + liquidators: vec![], + interest_rate_model: InterestRateModel::default(), + }, + reserved_factor: DEFAULT_MARKET_VAULT_RESERVE, + currency_pair: CurrencyPair::new(collateral_asset, borrow_asset), + }; + let created = + ::create(manager, config.clone()); + assert!(!created.is_ok()); + Tokens::mint_into(borrow_asset, &manager, INITIAL_BORROW_ASSET_AMOUNT).unwrap(); + let created = ::create(manager, config); + assert_ok!(created); + let new_balance = Tokens::balance(borrow_asset, &manager); + assert!(new_balance < INITIAL_BORROW_ASSET_AMOUNT); + let (market_id, borrow_vault_id) = created.unwrap(); + + let vault_borrow_id = + ::asset_id(&borrow_vault_id).unwrap(); + assert_eq!(vault_borrow_id, borrow_asset); + + let initial_total_cash = Lending::total_cash(&market_id).unwrap(); + assert!(initial_total_cash > 0); + }); +} + #[test] fn test_borrow_repay_in_same_block() { new_test_ext().execute_with(|| { let collateral_amount = 900000000; - let (market, vault) = create_simple_market(); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral_amount)); + let (market_id, vault) = create_simple_market(); + let initial_total_cash = Lending::total_cash(&market_id).unwrap(); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, collateral_amount)); - assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); + assert_ok!(Lending::deposit_collateral_internal(&market_id, &ALICE, collateral_amount)); + assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); let borrow_asset_deposit = 900000; - assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); + assert_ok!(Tokens::mint_into(CurrencyId::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); + let mut total_cash = + DEFAULT_MARKET_VAULT_STRATEGY_SHARE.mul(borrow_asset_deposit) + initial_total_cash; // Allow the market to initialize it's account by withdrawing // from the vault @@ -307,25 +348,25 @@ fn test_borrow_repay_in_same_block() { 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 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)); + assert_eq!(Lending::borrow_balance_current(&market_id, &ALICE), Ok(Some(0))); + let limit_normalized = Lending::get_borrow_limit(&market_id, &ALICE).unwrap(); + let alice_limit = limit_normalized / price(CurrencyId::BTC, 1); + assert_eq!(Lending::total_cash(&market_id), Ok(total_cash)); process_block(1); - assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_limit / 4)); + assert_ok!(Lending::borrow_internal(&market_id, &ALICE, alice_limit / 4)); total_cash -= alice_limit / 4; let total_borrows = alice_limit / 4; - assert_eq!(Lending::total_cash(&market), Ok(total_cash)); - assert_eq!(Lending::total_borrows(&market), Ok(total_borrows)); - let alice_repay_amount = Lending::borrow_balance_current(&market, &ALICE).unwrap(); + assert_eq!(Lending::total_cash(&market_id), Ok(total_cash)); + assert_eq!(Lending::total_borrows(&market_id), Ok(total_borrows)); + let alice_repay_amount = Lending::borrow_balance_current(&market_id, &ALICE).unwrap(); // MINT required BTC so that ALICE and BOB can repay the borrow. assert_ok!(Tokens::mint_into( - MockCurrencyId::BTC, + CurrencyId::BTC, &ALICE, alice_repay_amount.unwrap() - (alice_limit / 4) )); assert_noop!( - Lending::repay_borrow_internal(&market, &ALICE, &ALICE, alice_repay_amount), + Lending::repay_borrow_internal(&market_id, &ALICE, &ALICE, alice_repay_amount), Error::::BorrowAndRepayInSameBlockIsNotSupported ); }); @@ -358,6 +399,8 @@ fn test_borrow_math() { fn borrow_flow() { new_test_ext().execute_with(|| { let (market, vault) = create_simple_market(); + let initial_total_cash = Lending::total_cash(&market).unwrap(); + assert!(initial_total_cash > 0); let unit = 1_000_000_000; Oracle::set_btc_price(50000); @@ -365,21 +408,21 @@ fn borrow_flow() { |currency_id, amount| Oracle::get_price(currency_id, amount).expect("impossible").price; 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)); + let collateral_amount = + alice_capable_btc * price(CurrencyId::BTC, 1000) / price(CurrencyId::USDT, 1000); + assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, collateral_amount)); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - let limit = limit_normalized / price(MockCurrencyId::BTC, 1); + let limit = limit_normalized / price(CurrencyId::BTC, 1); assert_eq!(limit, alice_capable_btc / DEFAULT_COLLATERAL_FACTOR); 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_eq!(Tokens::balance(CurrencyId::BTC, &CHARLIE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); + assert_eq!(Tokens::balance(CurrencyId::BTC, &CHARLIE), borrow_asset_deposit); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, borrow_asset_deposit)); - assert_eq!(Tokens::balance(MockCurrencyId::BTC, &CHARLIE), 0); + assert_eq!(Tokens::balance(CurrencyId::BTC, &CHARLIE), 0); // Allow the market to initialize it's account by withdrawing // from the vault @@ -387,7 +430,8 @@ fn borrow_flow() { process_block(i); } - let expected_cash = DEFAULT_MARKET_VAULT_STRATEGY_SHARE.mul(borrow_asset_deposit); + let expected_cash = + DEFAULT_MARKET_VAULT_STRATEGY_SHARE.mul(borrow_asset_deposit) + initial_total_cash; assert_eq!(Lending::total_cash(&market), Ok(expected_cash)); let alice_borrow = alice_capable_btc / DEFAULT_COLLATERAL_FACTOR / 10; @@ -397,7 +441,7 @@ fn borrow_flow() { assert_eq!(Lending::total_interest_accurate(&market), Ok(0)); let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - let original_limit = limit_normalized / price(MockCurrencyId::BTC, 1); + let original_limit = limit_normalized / price(CurrencyId::BTC, 1); assert_eq!(original_limit, alice_capable_btc / DEFAULT_COLLATERAL_FACTOR - alice_borrow); @@ -411,7 +455,7 @@ fn borrow_flow() { assert!(interest_before < interest_after); let limit_normalized = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - let new_limit = limit_normalized / price(MockCurrencyId::BTC, 1); + let new_limit = limit_normalized / price(CurrencyId::BTC, 1); assert!(new_limit < original_limit); @@ -434,12 +478,12 @@ fn borrow_flow() { process_block(10001); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, collateral_amount)); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, collateral_amount)); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral_amount)); let alice_limit = Lending::get_borrow_limit(&market, &ALICE).unwrap(); - assert!(price(MockCurrencyId::BTC, alice_capable_btc) > alice_limit); - assert!(alice_limit > price(MockCurrencyId::BTC, alice_borrow)); + assert!(price(CurrencyId::BTC, alice_capable_btc) > alice_limit); + assert!(alice_limit > price(CurrencyId::BTC, alice_borrow)); assert_noop!( Lending::borrow_internal(&market, &ALICE, alice_limit), @@ -451,21 +495,19 @@ fn borrow_flow() { } #[test] -fn test_vault_market_cannot_withdraw() { +fn vault_takes_part_of_borrow_so_cannot_withdraw() { new_test_ext().execute_with(|| { - let (market, vault_id) = create_simple_market(); - - let deposit_usdt = 1_000_000; + let (market_id, vault_id) = create_simple_market(); + let initial_total_cash = Lending::total_cash(&market_id).unwrap(); + let deposit_usdt = 1_000_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)); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, deposit_usdt)); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &ALICE, deposit_btc)); assert_ok!(Vault::deposit(Origin::signed(*ALICE), vault_id, deposit_btc)); - assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, deposit_usdt)); - - // We don't even wait 1 block, which mean the market couldn't withdraw funds. + assert_ok!(Lending::deposit_collateral_internal(&market_id, &ALICE, deposit_usdt)); assert_noop!( - Lending::borrow_internal(&market, &ALICE, deposit_btc), + Lending::borrow_internal(&market_id, &ALICE, deposit_btc + initial_total_cash), Error::::NotEnoughBorrowAsset ); }); @@ -478,8 +520,8 @@ fn test_vault_market_can_withdraw() { 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!(Tokens::mint_into(CurrencyId::USDT, &ALICE, collateral)); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &ALICE, borrow)); assert_ok!(Vault::deposit(Origin::signed(*ALICE), vault_id, borrow)); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, collateral)); @@ -506,23 +548,23 @@ fn borrow_repay() { 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, alice_balance)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), alice_balance); + assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, alice_balance)); + assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), alice_balance); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, alice_balance)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); + assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); // Balance for BOB - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &BOB), 0); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &BOB, bob_balance)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &BOB), bob_balance); + assert_eq!(Tokens::balance(CurrencyId::USDT, &BOB), 0); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &BOB, bob_balance)); + assert_eq!(Tokens::balance(CurrencyId::USDT, &BOB), bob_balance); assert_ok!(Lending::deposit_collateral_internal(&market, &BOB, bob_balance)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &BOB), 0); + assert_eq!(Tokens::balance(CurrencyId::USDT, &BOB), 0); 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); + assert_eq!(Tokens::balance(CurrencyId::BTC, &CHARLIE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &CHARLIE, borrow_asset_deposit)); + assert_eq!(Tokens::balance(CurrencyId::BTC, &CHARLIE), borrow_asset_deposit); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, borrow_asset_deposit)); // Allow the market to initialize it's account by withdrawing @@ -535,7 +577,7 @@ fn borrow_repay() { assert_eq!(Lending::borrow_balance_current(&market, &ALICE), Ok(Some(0))); 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; + alice_limit_normalized / Oracle::get_price(CurrencyId::BTC, 1).unwrap().price; assert_ok!(Lending::borrow_internal(&market, &ALICE, alice_limit)); for i in 2..10000 { @@ -545,7 +587,7 @@ fn borrow_repay() { // BOB borrows assert_eq!(Lending::borrow_balance_current(&market, &BOB), Ok(Some(0))); let limit_normalized = Lending::get_borrow_limit(&market, &BOB).unwrap(); - let limit = limit_normalized / Oracle::get_price(MockCurrencyId::BTC, 1).unwrap().price; + let limit = limit_normalized / Oracle::get_price(CurrencyId::BTC, 1).unwrap().price; assert_ok!(Lending::borrow_internal(&market, &BOB, limit,)); let bob_limit = Lending::get_borrow_limit(&market, &BOB).unwrap(); @@ -559,31 +601,26 @@ fn borrow_repay() { // MINT required BTC so that ALICE and BOB can repay the borrow. assert_ok!(Tokens::mint_into( - MockCurrencyId::BTC, + CurrencyId::BTC, &ALICE, alice_repay_amount.unwrap() - alice_limit )); - assert_ok!(Tokens::mint_into( - MockCurrencyId::BTC, - &BOB, - bob_repay_amount.unwrap() - bob_limit - )); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &BOB, bob_repay_amount.unwrap() - bob_limit)); // ALICE , BOB both repay's loan. their USDT balance should have decreased because of // interest paid on borrows assert_ok!(Lending::repay_borrow_internal(&market, &BOB, &BOB, bob_repay_amount)); assert_ok!(Lending::repay_borrow_internal(&market, &ALICE, &ALICE, alice_repay_amount)); - assert!(alice_balance > Tokens::balance(MockCurrencyId::USDT, &ALICE)); - assert!(bob_balance > Tokens::balance(MockCurrencyId::USDT, &BOB)); + assert!(alice_balance > Tokens::balance(CurrencyId::USDT, &ALICE)); + assert!(bob_balance > Tokens::balance(CurrencyId::USDT, &BOB)); }); } -#[ignore = "until we reimplement liquidation engine"] #[test] -fn test_liquidation() { +fn liquidation() { new_test_ext().execute_with(|| { let (market, vault) = create_market( - MockCurrencyId::USDT, - MockCurrencyId::BTC, + CurrencyId::USDT, + CurrencyId::BTC, *ALICE, Perquintill::from_percent(10), MoreThanOneFixedU128::saturating_from_rational(2, 1), @@ -592,20 +629,14 @@ fn test_liquidation() { Oracle::set_btc_price(100); 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); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &ALICE, two_btc_amount)); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, two_btc_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), 0); + assert_eq!(Tokens::balance(CurrencyId::BTC, &ALICE), 0); let usdt_amt = u32::MAX as Balance; - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &CHARLIE), 0); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &CHARLIE, usdt_amt)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &CHARLIE), usdt_amt); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &CHARLIE, usdt_amt)); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, usdt_amt)); - assert_eq!(Lending::borrow_balance_current(&market, &ALICE), Ok(Some(0))); - // Allow the market to initialize it's account by withdrawing // from the vault for i in 1..2 { @@ -613,8 +644,8 @@ fn test_liquidation() { } let borrow_limit = Lending::get_borrow_limit(&market, &ALICE).expect("impossible"); + assert!(borrow_limit > 0); - // Borrow the maximum assert_ok!(Lending::borrow_internal(&market, &ALICE, borrow_limit)); for i in 2..10000 { @@ -624,7 +655,7 @@ fn test_liquidation() { // Collateral going down imply liquidation Oracle::set_btc_price(99); - assert_ok!(Lending::liquidate_internal(&market, &ALICE)); + assert_ok!(Lending::liquidate_internal(None, &market, vec![*ALICE])); }); } @@ -632,8 +663,8 @@ fn test_liquidation() { fn test_warn_soon_under_collaterized() { new_test_ext().execute_with(|| { let (market, vault) = create_market( - MockCurrencyId::USDT, - MockCurrencyId::BTC, + CurrencyId::USDT, + CurrencyId::BTC, *ALICE, Perquintill::from_percent(10), MoreThanOneFixedU128::saturating_from_rational(2, 1), @@ -644,18 +675,18 @@ fn test_warn_soon_under_collaterized() { // Deposit 2 BTC 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); + assert_eq!(Tokens::balance(CurrencyId::BTC, &ALICE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::BTC, &ALICE, two_btc_amount)); + assert_eq!(Tokens::balance(CurrencyId::BTC, &ALICE), two_btc_amount); assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, two_btc_amount)); - assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), 0); + assert_eq!(Tokens::balance(CurrencyId::BTC, &ALICE), 0); // Balance of USDT for CHARLIE // CHARLIE is only lender of USDT let usdt_amt = u32::MAX as Balance; - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &CHARLIE), 0); - assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &CHARLIE, usdt_amt)); - assert_eq!(Tokens::balance(MockCurrencyId::USDT, &CHARLIE), usdt_amt); + assert_eq!(Tokens::balance(CurrencyId::USDT, &CHARLIE), 0); + assert_ok!(Tokens::mint_into(CurrencyId::USDT, &CHARLIE, usdt_amt)); + assert_eq!(Tokens::balance(CurrencyId::USDT, &CHARLIE), usdt_amt); assert_ok!(Vault::deposit(Origin::signed(*CHARLIE), vault, usdt_amt)); // Allow the market to initialize it's account by withdrawing @@ -793,14 +824,14 @@ proptest! { fn market_collateral_deposit_withdraw_identity(amount in valid_amount_without_overflow()) { new_test_ext().execute_with(|| { let (market, _) = create_simple_market(); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); - prop_assert_ok!(Tokens::mint_into(MockCurrencyId::USDT, &ALICE, amount)); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), amount); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); + prop_assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, amount)); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), amount); prop_assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, amount)); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); prop_assert_ok!(Lending::withdraw_collateral_internal(&market, &ALICE, amount)); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), amount); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), amount); Ok(()) })?; @@ -810,12 +841,12 @@ proptest! { fn market_collateral_deposit_withdraw_higher_amount_fails(amount in valid_amount_without_overflow()) { 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 )); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), amount ); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); + prop_assert_ok!(Tokens::mint_into(CurrencyId::USDT, &ALICE, amount )); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), amount ); prop_assert_ok!(Lending::deposit_collateral_internal(&market, &ALICE, amount )); - prop_assert_eq!(Tokens::balance(MockCurrencyId::USDT, &ALICE), 0); + prop_assert_eq!(Tokens::balance(CurrencyId::USDT, &ALICE), 0); prop_assert_eq!( Lending::withdraw_collateral_internal(&market, &ALICE, amount + 1), Err(Error::::NotEnoughCollateral.into()) @@ -855,7 +886,7 @@ proptest! { fn market_creation_with_multi_level_priceable_lp(depth in 0..20) { new_test_ext().execute_with(|| { // Assume we have a pricable base asset - let base_asset = MockCurrencyId::ETH; + let base_asset = CurrencyId::ETH; let base_vault = create_simple_vault(base_asset); let (_, VaultInfo { lp_token_id, ..}) = @@ -866,7 +897,7 @@ proptest! { // A market with two priceable assets can be created create_market( - MockCurrencyId::BTC, + CurrencyId::BTC, lp_token_id, *ALICE, Perquintill::from_percent(10), @@ -896,15 +927,13 @@ proptest! { prop_assert_ne!(Lending::account_id(&market_id1), Lending::account_id(&market_id2)); // Alice lend an amount in market1 vault - prop_assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), 0); - prop_assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &ALICE, amount1)); - prop_assert_eq!(Tokens::balance(MockCurrencyId::BTC, &ALICE), amount1); + prop_assert_ok!(Tokens::mint_into(CurrencyId::BTC, &ALICE, amount1)); prop_assert_ok!(Vault::deposit(Origin::signed(*ALICE), vault_id1, amount1)); // Bob lend an amount in market2 vault - prop_assert_eq!(Tokens::balance(MockCurrencyId::BTC, &BOB), 0); - prop_assert_ok!(Tokens::mint_into(MockCurrencyId::BTC, &BOB, amount2)); - prop_assert_eq!(Tokens::balance(MockCurrencyId::BTC, &BOB), amount2); + prop_assert_eq!(Tokens::balance(CurrencyId::BTC, &BOB), 0); + prop_assert_ok!(Tokens::mint_into(CurrencyId::BTC, &BOB, amount2)); + prop_assert_eq!(Tokens::balance(CurrencyId::BTC, &BOB), amount2); prop_assert_ok!(Vault::deposit(Origin::signed(*BOB), vault_id2, amount2)); // Allow the market to initialize it's account by withdrawing @@ -918,11 +947,11 @@ proptest! { // // The funds should not be shared. prop_assert_acceptable_computation_error!( - Tokens::balance(MockCurrencyId::BTC, &Lending::account_id(&market_id1)), + Tokens::balance(CurrencyId::BTC, &Lending::account_id(&market_id1)), expected_market1_balance ); prop_assert_acceptable_computation_error!( - Tokens::balance(MockCurrencyId::BTC, &Lending::account_id(&market_id2)), + Tokens::balance(CurrencyId::BTC, &Lending::account_id(&market_id2)), expected_market2_balance ); diff --git a/frame/lending/src/weights.rs b/frame/lending/src/weights.rs index e0122d27057..e8893af5432 100644 --- a/frame/lending/src/weights.rs +++ b/frame/lending/src/weights.rs @@ -15,6 +15,7 @@ pub trait WeightInfo { fn withdraw_collateral() -> Weight; fn borrow() -> Weight; fn repay_borrow() -> Weight; + fn liquidate(positions_count: Weight) -> Weight; fn now() -> Weight; fn accrue_interest() -> Weight; fn account_id() -> Weight; @@ -24,65 +25,6 @@ pub trait WeightInfo { fn handle_must_liquidate() -> Weight; } -/// Weight functions for lending. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - fn create_new_market() -> Weight { - (96_881_000 as Weight) - .saturating_add(T::DbWeight::get().reads(5 as Weight)) - .saturating_add(T::DbWeight::get().writes(11 as Weight)) - } - fn deposit_collateral() -> Weight { - 123_789_000_u64 - .saturating_add(T::DbWeight::get().reads(6_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) - } - fn withdraw_collateral() -> Weight { - 138_802_000_u64 - .saturating_add(T::DbWeight::get().reads(10_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)) - } - fn borrow() -> Weight { - (332_730_000 as Weight) - .saturating_add(T::DbWeight::get().reads(19 as Weight)) - .saturating_add(T::DbWeight::get().writes(9 as Weight)) - } - fn repay_borrow() -> Weight { - (209_694_000 as Weight) - .saturating_add(T::DbWeight::get().reads(13 as Weight)) - .saturating_add(T::DbWeight::get().writes(6 as Weight)) - } - fn now() -> Weight { - (4_744_000 as Weight).saturating_add(T::DbWeight::get().reads(1 as Weight)) - } - fn accrue_interest() -> Weight { - (76_626_000 as Weight) - .saturating_add(T::DbWeight::get().reads(8 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn account_id() -> Weight { - (3_126_000 as Weight) - } - fn available_funds() -> Weight { - (16_450_000 as Weight).saturating_add(T::DbWeight::get().reads(2 as Weight)) - } - fn handle_withdrawable() -> Weight { - (20_716_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn handle_depositable() -> Weight { - (40_066_000 as Weight) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn handle_must_liquidate() -> Weight { - (38_744_000 as Weight) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } -} - impl WeightInfo for () { fn create_new_market() -> Weight { (96_881_000 as Weight) @@ -138,4 +80,8 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } + + fn liquidate(positions_count: Weight) -> Weight { + 10000 * positions_count + } } diff --git a/frame/liquidations/src/benchmarking.rs b/frame/liquidations/src/benchmarking.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frame/liquidations/src/lib.rs b/frame/liquidations/src/lib.rs index 32f43d42cc0..d5dd25d043c 100644 --- a/frame/liquidations/src/lib.rs +++ b/frame/liquidations/src/lib.rs @@ -76,7 +76,7 @@ pub mod pallet { type LiquidationStrategyId: Default + FullCodec + WrappingNext + Parameter + Copy; - type OrderId: Default + FullCodec; + type OrderId: Default + FullCodec + sp_std::fmt::Debug; type PalletId: Get; @@ -108,7 +108,7 @@ pub mod pallet { _origin: OriginFor, _configuraiton: LiquidationStrategyConfiguration, ) -> DispatchResultWithPostInfo { - Err(DispatchError::Other("no implemented").into()) + Err(DispatchError::Other("add_liqudation_strategy: no implemented").into()) } } @@ -142,16 +142,9 @@ pub mod pallet { type AccountId = T::AccountId; } + #[derive(Default)] #[pallet::genesis_config] - pub struct GenesisConfig { - _phantom: sp_std::marker::PhantomData, - } - - impl Default for GenesisConfig { - fn default() -> Self { - Self { _phantom: <_>::default() } - } - } + pub struct GenesisConfig; impl Pallet { pub fn create_strategy_id() -> T::LiquidationStrategyId { @@ -184,10 +177,11 @@ pub mod pallet { } #[pallet::genesis_build] - impl GenesisBuild for GenesisConfig { + 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 }), ); @@ -228,7 +222,6 @@ pub mod pallet { "as for now, only auction liquidators implemented", )), }; - if result.is_ok() { Self::deposit_event(Event::::PositionWasSentToLiquidation {}); return result diff --git a/frame/liquidations/src/tests.rs b/frame/liquidations/src/tests.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frame/oracle/src/lib.rs b/frame/oracle/src/lib.rs index 10cf953b605..8ab0c578eda 100644 --- a/frame/oracle/src/lib.rs +++ b/frame/oracle/src/lib.rs @@ -58,10 +58,11 @@ pub mod pallet { use scale_info::TypeInfo; use sp_core::crypto::KeyTypeId; use sp_runtime::{ + helpers_128bit::multiply_by_rational, offchain::{http, Duration}, traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedMul, CheckedSub, Saturating, Zero}, - AccountId32, FixedPointNumber, FixedU128, KeyTypeId as CryptoKeyTypeId, PerThing, Percent, - RuntimeDebug, + AccountId32, ArithmeticError, FixedPointNumber, FixedU128, KeyTypeId as CryptoKeyTypeId, + PerThing, Percent, RuntimeDebug, }; use sp_std::{borrow::ToOwned, fmt::Debug, str, vec, vec::Vec}; @@ -377,12 +378,20 @@ pub mod pallet { type LocalAssets = T::LocalAssets; fn get_price( - asset: Self::AssetId, + asset_id: Self::AssetId, amount: Self::Balance, ) -> Result, DispatchError> { let Price { price, block } = - Prices::::try_get(asset).map_err(|_| Error::::PriceNotFound)?; - Ok(LastPrice { price: price.safe_mul(&amount)?, block }) + Prices::::try_get(asset_id).map_err(|_| Error::::PriceNotFound)?; + let unit = 10_u128 + .checked_pow(Self::LocalAssets::decimals(asset_id)?) + .ok_or(DispatchError::Arithmetic(ArithmeticError::Overflow))?; + let price = multiply_by_rational(price.into(), amount.into(), unit) + .map_err(|_| DispatchError::Arithmetic(ArithmeticError::Overflow))?; + let price = price + .try_into() + .map_err(|_| DispatchError::Arithmetic(ArithmeticError::Overflow))?; + Ok(LastPrice { price, block }) } fn get_twap( @@ -407,6 +416,26 @@ pub mod pallet { let quote = FixedU128::saturating_from_integer(quote); Ok(base.safe_div("e)?) } + + fn get_price_inverse( + asset_id: Self::AssetId, + amount: Self::Balance, + ) -> Result { + // imagine 10^3 == 1_000 costs 4 + // so 1 costs 0, + // and amount of normalized desired is 10 + // 10 * 1_000 / 4 = 2_500 + // so we need 2_500 asset amount to pay for 10 normalized + let unit = 10 ^ (T::LocalAssets::decimals(asset_id))?; + let price_asset_for_unit: u128 = Self::get_price(asset_id, unit.into())?.price.into(); + let amount: u128 = amount.into(); + let result = multiply_by_rational(amount, unit as u128, price_asset_for_unit)?; + let result: u64 = result + .try_into() + .map_err(|_| Into::::into(ArithmeticError::Overflow))?; + + Ok(result.into()) + } } #[pallet::call] diff --git a/frame/oracle/src/tests.rs b/frame/oracle/src/tests.rs index 06295ddce8a..332b11bd930 100644 --- a/frame/oracle/src/tests.rs +++ b/frame/oracle/src/tests.rs @@ -661,6 +661,23 @@ fn ratio_human_case() { }) } +#[test] +fn inverses() { + new_test_ext().execute_with(|| { + let price = Price { price: 1, block: System::block_number() }; + Prices::::insert(13, price); + let inverse = + ::get_price_inverse(13, 1).unwrap(); + assert_eq!(inverse, 1); + + let price = Price { price: 1, block: System::block_number() }; + Prices::::insert(13, price); + let inverse = + ::get_price_inverse(13, 2).unwrap(); + assert_eq!(inverse, 2); + }) +} + #[test] fn ratio_base_is_way_less_smaller() { new_test_ext().execute_with(|| { diff --git a/runtime/composable/src/lib.rs b/runtime/composable/src/lib.rs index e25b1f2f791..3336b9296ca 100644 --- a/runtime/composable/src/lib.rs +++ b/runtime/composable/src/lib.rs @@ -104,8 +104,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 101, - impl_version: 1, + spec_version: 100, + impl_version: 2, apis: RUNTIME_API_VERSIONS, transaction_version: 1, }; diff --git a/runtime/dali/src/lib.rs b/runtime/dali/src/lib.rs index e3ea818b77b..580f6f0c321 100644 --- a/runtime/dali/src/lib.rs +++ b/runtime/dali/src/lib.rs @@ -102,7 +102,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. spec_version: 2001, - impl_version: 1, + impl_version: 2, apis: RUNTIME_API_VERSIONS, transaction_version: 1, }; diff --git a/runtime/picasso/src/lib.rs b/runtime/picasso/src/lib.rs index a4b43dcdb4b..d4e742a0ced 100644 --- a/runtime/picasso/src/lib.rs +++ b/runtime/picasso/src/lib.rs @@ -100,7 +100,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, // `spec_version`, and `authoring_version` are the same between Wasm and native. spec_version: 2001, - impl_version: 1, + impl_version: 2, apis: RUNTIME_API_VERSIONS, transaction_version: 1, };