Skip to content

Commit

Permalink
CU-1wty1zv fixed lending (#484)
Browse files Browse the repository at this point in the history
* oracle api clarification

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixing pr comments

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixed comment

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* crazy fmt issue

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* just something to tirgget build after fail

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixed lending

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixed price, added ratio test

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixed comments of review

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>

* fixed comments

Signed-off-by: Dzmitry Lahoda <dzmitry@lahoda.pro>
  • Loading branch information
dzmitry-lahoda authored Jan 19, 2022
1 parent a26f8d0 commit a8c5bfb
Show file tree
Hide file tree
Showing 34 changed files with 961 additions and 740 deletions.
2 changes: 1 addition & 1 deletion .config/cargo_spellcheck.dic
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ tombstoned
u128
Wasm
Xcm
XCM
Dispatchable
14 changes: 7 additions & 7 deletions frame/assets/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ benchmarks! {
let asset_id: T::AssetId = ASSET_ID.into();
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap();
T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test");
}: _(RawOrigin::Signed(caller), asset_id, dest, amount, true)

transfer_native {
let caller: T::AccountId = whitelisted_caller();
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::NativeCurrency::mint_into(&caller, amount).unwrap();
T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test");
}: _(RawOrigin::Signed(caller), dest, amount, false)

force_transfer {
Expand All @@ -50,30 +50,30 @@ benchmarks! {
let from = T::Lookup::unlookup(FROM_ACCOUNT.into());
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap();
T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test");
}: _(RawOrigin::Root, asset_id, from, dest, amount, false)

force_transfer_native {
let caller: T::AccountId = FROM_ACCOUNT.into();
let from = T::Lookup::unlookup(FROM_ACCOUNT.into());
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::NativeCurrency::mint_into(&caller, amount).unwrap();
T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test");
}: _(RawOrigin::Root, from, dest, amount, false)

transfer_all {
let caller: T::AccountId = whitelisted_caller();
let asset_id: T::AssetId = ASSET_ID.into();
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap();
T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test");
}: _(RawOrigin::Signed(caller), asset_id, dest, false)

transfer_all_native {
let caller: T::AccountId = whitelisted_caller();
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::NativeCurrency::mint_into(&caller, amount).unwrap();
T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test");
}: _(RawOrigin::Signed(caller), dest, false)

mint_initialize {
Expand All @@ -98,7 +98,7 @@ benchmarks! {
let asset_id: T::AssetId = ASSET_ID.into();
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
let amount: T::Balance = TRANSFER_AMOUNT.into();
T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap();
T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test");
}: _(RawOrigin::Root, asset_id, dest, amount)

}
Expand Down
35 changes: 2 additions & 33 deletions frame/composable-traits/src/auction.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,2 @@
use crate::loans::DurationSeconds;
use frame_support::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_runtime::Permill;

#[derive(Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
pub enum AuctionStepFunction {
/// default - direct pass through to dex without steps, just to satisfy defaults and reasonably
/// for testing
LinearDecrease(LinearDecrease),
StairstepExponentialDecrease(StairstepExponentialDecrease),
}

impl Default for AuctionStepFunction {
fn default() -> Self {
Self::LinearDecrease(Default::default())
}
}

#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
pub struct LinearDecrease {
/// Seconds after auction start when the price reaches zero
pub total: DurationSeconds,
}

#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
pub struct StairstepExponentialDecrease {
// Length of time between price drops
pub step: DurationSeconds,
// Per-step multiplicative factor, usually more than 50%, mostly closer to 100%, but not 100%.
// Drop per unit of `step`.
pub cut: Permill,
}
// TODO: how type alias to such generics can be achieved?
// pub type DutchAuction = SellEngine<AuctionStepFunction>;
16 changes: 15 additions & 1 deletion frame/composable-traits/src/currency.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use codec::FullCodec;
use frame_support::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_runtime::traits::AtLeast32BitUnsigned;
use sp_runtime::traits::{AtLeast32BitUnsigned, Zero};
use sp_std::fmt::Debug;

use crate::math::SafeArithmetic;

/// really u8, but easy to do math operations
pub type Exponent = u32;

Expand Down Expand Up @@ -77,6 +79,18 @@ impl<
{
}

/// limited counted number trait which maximal number is more than `u64`, but not more than `u128`,
/// so inner type is either u64 or u128 with helpers for producing `ArithmeticError`s instead of
/// `Option`s.
pub trait MathBalance:
PartialOrd + Zero + SafeArithmetic + Into<u128> + TryFrom<u128> + From<u64> + Copy
{
}
impl<T: PartialOrd + Zero + SafeArithmetic + Into<u128> + TryFrom<u128> + From<u64> + Copy>
MathBalance for T
{
}

// hack to imitate type alias until it is in stable
// named with like implying it is`like` is is necessary to be `AssetId`, but may be not enough (if
// something is `AssetIdLike` than it is not always asset)
Expand Down
91 changes: 70 additions & 21 deletions frame/composable-traits/src/defi.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
//! Common codes for defi pallets

//! Common codes and conventions for DeFi pallets
use codec::{Codec, Decode, Encode, FullCodec};
use frame_support::{pallet_prelude::MaybeSerializeDeserialize, Parameter};
use scale_info::TypeInfo;
use sp_runtime::{
helpers_128bit::multiply_by_rational,
traits::{CheckedAdd, CheckedMul, CheckedSub, Zero},
ArithmeticError, DispatchError, FixedPointOperand, FixedU128,
ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128,
};

use crate::{
currency::{AssetIdLike, BalanceLike},
math::{LiftedFixedBalance, SafeArithmetic},
};
use crate::currency::{AssetIdLike, BalanceLike, MathBalance};

#[derive(Encode, Decode, TypeInfo, Debug, Clone, PartialEq)]
pub struct Take<Balance> {
/// amount of `base`
pub amount: Balance,
/// direction depends on referenced order type
/// either minimal or maximal amount of `quote` for given unit of `base`
pub limit: Balance,
/// either minimal or maximal amount of `quote` for given `base`
/// depending on engine configuration, `limit` can be hard or flexible (change with time)
pub limit: LiftedFixedBalance,
}

impl<Balance: PartialOrd + Zero + SafeArithmetic> Take<Balance> {
impl<Balance: MathBalance> Take<Balance> {
pub fn is_valid(&self) -> bool {
self.amount > Balance::zero() && self.limit > Balance::zero()
self.amount > Balance::zero() && self.limit > Ratio::zero()
}
pub fn new(amount: Balance, limit: Balance) -> Self {

pub fn new(amount: Balance, limit: Ratio) -> Self {
Self { amount, limit }
}

pub fn quote_amount(&self) -> Result<Balance, ArithmeticError> {
self.amount.safe_mul(&self.limit)
pub fn quote_limit_amount(&self) -> Result<Balance, ArithmeticError> {
self.quote_amount(self.amount)
}

pub fn quote_amount(&self, amount: Balance) -> Result<Balance, ArithmeticError> {
let result = multiply_by_rational(amount.into(), self.limit.into_inner(), Ratio::DIV)
.map_err(|_| ArithmeticError::Overflow)?;
result.try_into().map_err(|_| ArithmeticError::Overflow)
}
}

Expand All @@ -42,15 +47,15 @@ pub struct Sell<AssetId, Balance> {
pub take: Take<Balance>,
}

impl<AssetId: PartialEq, Balance: PartialOrd + Zero + SafeArithmetic> Sell<AssetId, Balance> {
impl<AssetId: PartialEq, Balance: MathBalance> Sell<AssetId, Balance> {
pub fn is_valid(&self) -> bool {
self.take.is_valid()
}
pub fn new(
base: AssetId,
quote: AssetId,
base_amount: Balance,
minimal_base_unit_price_in_quote: Balance,
minimal_base_unit_price_in_quote: Ratio,
) -> Self {
Self {
take: Take { amount: base_amount, limit: minimal_base_unit_price_in_quote },
Expand All @@ -70,9 +75,11 @@ impl<AssetId: PartialEq, Balance: PartialOrd + Zero + SafeArithmetic> Sell<Asset
pub struct CurrencyPair<AssetId> {
/// See [Base Currency](https://www.investopedia.com/terms/b/basecurrency.asp).
/// Also can be named `native`(to the market) currency.
/// Usually less stable, can be used as collateral.
pub base: AssetId,
/// Counter currency.
/// Also can be named `price` currency.
/// Usually more stable, may be `borrowable` asset.
pub quote: AssetId,
}

Expand All @@ -90,9 +97,10 @@ impl<AssetId: PartialEq> CurrencyPair<AssetId> {
/// assert_eq!(slice[0], pair.base);
/// assert_eq!(slice[1], pair.quote);
/// ```
/// ```compile_fail
/// ```rust
/// # let pair = composable_traits::defi::CurrencyPair::<u128>::new(13, 42);
/// # let slice = pair.as_slice();
/// // it is copy
/// drop(pair);
/// let _ = slice[0];
/// ```
Expand Down Expand Up @@ -185,6 +193,7 @@ pub trait DeFiComposableConfig: frame_system::Config {
type MayBeAssetId: AssetIdLike + MaybeSerializeDeserialize + Default;

type Balance: BalanceLike
+ MathBalance
+ Default
+ Parameter
+ Codec
Expand All @@ -197,12 +206,52 @@ pub trait DeFiComposableConfig: frame_system::Config {
+ From<u64> // at least 64 bit
+ Zero
+ FixedPointOperand
+ Into<LiftedFixedBalance> // integer part not more than bits in this
+ Into<u128>; // cannot do From<u128>, until LiftedFixedBalance integer part is larger than 128
// bit
}

/// The fixed point number from 0..to max.
/// Unlike `Ratio` it can be more than 1.
/// And unlike `NormalizedCollateralFactor`, it can be less than one.
/// The fixed point number from 0..to max
pub type Rate = FixedU128;

/// Is [1..MAX]
pub type OneOrMoreFixedU128 = FixedU128;

/// The fixed point number of suggested by substrate precision
/// Must be (1.0..MAX] because applied only to price normalized values
pub type MoreThanOneFixedU128 = FixedU128;

/// Must be [0..1]
pub type ZeroToOneFixedU128 = FixedU128;

/// Number like of higher bits, so that amount and balance calculations are done it it with higher
/// precision via fixed point.
/// While this is 128 bit, cannot support u128 because 18 bits are for of mantissa (so maximal
/// integer is 110 bit). Can support u128 if lift upper to use FixedU256 analog.
pub type LiftedFixedBalance = FixedU128;

/// unitless ratio of one thing to other.
pub type Ratio = FixedU128;

#[cfg(test)]
mod tests {
use super::{Ratio, Take};
use sp_runtime::FixedPointNumber;

#[test]
fn take_ratio_half() {
let price = 10;
let amount = 100_u128;
let take = Take::new(amount, Ratio::saturating_from_integer(price));
let result = take.quote_amount(amount / 2).unwrap();
assert_eq!(result, price * amount / 2);
}

#[test]
fn take_ratio_half_amount_half_price() {
let price_part = 50;
let amount = 100_u128;
let take = Take::new(amount, Ratio::saturating_from_rational(price_part, 100));
let result = take.quote_amount(amount).unwrap();
assert_eq!(result, price_part * amount / 100);
}
}
47 changes: 21 additions & 26 deletions frame/composable-traits/src/lending/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,11 @@ use sp_runtime::{
use sp_arithmetic::per_things::Percent;

use crate::{
defi::Rate,
loans::{DurationSeconds, ONE_HOUR},
math::{LiftedFixedBalance, SafeArithmetic},
defi::{LiftedFixedBalance, Rate, ZeroToOneFixedU128},
math::SafeArithmetic,
time::{DurationSeconds, SECONDS_PER_YEAR_NAIVE},
};

/// The fixed point number of suggested by substrate precision
/// Must be (1.0.. because applied only to price normalized values
pub type NormalizedCollateralFactor = FixedU128;

/// Must be [0..1]
/// TODO: implement Ratio as wrapper over FixedU128
pub type Ratio = FixedU128;

/// current notion of year will take away 1/365 from lenders and give away to borrowers (as does no
/// accounts to length of year)
pub const SECONDS_PER_YEAR: DurationSeconds = 365 * 24 * ONE_HOUR;

/// utilization_ratio = total_borrows / (total_cash + total_borrows)
pub fn calc_utilization_ratio(
cash: LiftedFixedBalance,
Expand Down Expand Up @@ -110,9 +98,13 @@ impl InterestRateModel {
}

/// Calculates the current supply interest rate
pub fn get_supply_rate(borrow_rate: Rate, util: Ratio, reserve_factor: Ratio) -> Rate {
pub fn get_supply_rate(
borrow_rate: Rate,
util: ZeroToOneFixedU128,
reserve_factor: ZeroToOneFixedU128,
) -> Rate {
// ((1 - reserve_factor) * borrow_rate) * utilization
let one_minus_reserve_factor = Ratio::one().saturating_sub(reserve_factor);
let one_minus_reserve_factor = ZeroToOneFixedU128::one().saturating_sub(reserve_factor);
let rate_to_pool = borrow_rate.saturating_mul(one_minus_reserve_factor);

rate_to_pool.saturating_mul(util)
Expand Down Expand Up @@ -151,15 +143,18 @@ pub struct JumpModel {
}

impl JumpModel {
pub const MAX_BASE_RATE: Ratio = Ratio::from_inner(100_000_000_000_000_000); // 10%
pub const MAX_JUMP_RATE: Ratio = Ratio::from_inner(300_000_000_000_000_000); // 30%
pub const MAX_FULL_RATE: Ratio = Ratio::from_inner(500_000_000_000_000_000); // 50%
pub const MAX_BASE_RATE: ZeroToOneFixedU128 =
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 10 / 100);
pub const MAX_JUMP_RATE: ZeroToOneFixedU128 =
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 30 / 100);
pub const MAX_FULL_RATE: ZeroToOneFixedU128 =
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 50 / 100);

/// Create a new rate model
pub fn new(
base_rate: Ratio,
jump_rate: Ratio,
full_rate: Ratio,
base_rate: ZeroToOneFixedU128,
jump_rate: ZeroToOneFixedU128,
full_rate: ZeroToOneFixedU128,
target_utilization: Percent,
) -> Option<JumpModel> {
let model = Self { base_rate, jump_rate, full_rate, target_utilization };
Expand Down Expand Up @@ -390,7 +385,7 @@ pub fn accrued_interest(
borrow_rate
.checked_mul_int(amount)?
.checked_mul(delta_time.into())?
.checked_div(SECONDS_PER_YEAR.into())
.checked_div(SECONDS_PER_YEAR_NAIVE.into())
}

/// compounding increment of borrow index
Expand All @@ -402,7 +397,7 @@ pub fn increment_index(
borrow_rate
.safe_mul(&index)?
.safe_mul(&FixedU128::saturating_from_integer(delta_time))?
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))?
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))?
.safe_add(&index)
}

Expand All @@ -412,5 +407,5 @@ pub fn increment_borrow_rate(
) -> Result<Rate, ArithmeticError> {
borrow_rate
.safe_mul(&FixedU128::saturating_from_integer(delta_time))?
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))
}
Loading

0 comments on commit a8c5bfb

Please sign in to comment.