Skip to content

Commit a8c5bfb

Browse files
CU-1wty1zv fixed lending (#484)
* oracle api clarification Signed-off-by: Dzmitry Lahoda <[email protected]> * fixing pr comments Signed-off-by: Dzmitry Lahoda <[email protected]> * fixed comment Signed-off-by: Dzmitry Lahoda <[email protected]> * crazy fmt issue Signed-off-by: Dzmitry Lahoda <[email protected]> * just something to tirgget build after fail Signed-off-by: Dzmitry Lahoda <[email protected]> * fixed lending Signed-off-by: Dzmitry Lahoda <[email protected]> * fixed price, added ratio test Signed-off-by: Dzmitry Lahoda <[email protected]> * fixed comments of review Signed-off-by: Dzmitry Lahoda <[email protected]> * fixed comments Signed-off-by: Dzmitry Lahoda <[email protected]>
1 parent a26f8d0 commit a8c5bfb

File tree

34 files changed

+961
-740
lines changed

34 files changed

+961
-740
lines changed

.config/cargo_spellcheck.dic

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ tombstoned
2222
u128
2323
Wasm
2424
Xcm
25-
XCM
25+
Dispatchable

frame/assets/src/benchmarking.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ benchmarks! {
3434
let asset_id: T::AssetId = ASSET_ID.into();
3535
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
3636
let amount: T::Balance = TRANSFER_AMOUNT.into();
37-
T::MultiCurrency::mint_into(asset_id, &caller, amount).unwrap();
37+
T::MultiCurrency::mint_into(asset_id, &caller, amount).expect("always can mint in test");
3838
}: _(RawOrigin::Signed(caller), asset_id, dest, amount, true)
3939

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

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

5656
force_transfer_native {
5757
let caller: T::AccountId = FROM_ACCOUNT.into();
5858
let from = T::Lookup::unlookup(FROM_ACCOUNT.into());
5959
let dest = T::Lookup::unlookup(TO_ACCOUNT.into());
6060
let amount: T::Balance = TRANSFER_AMOUNT.into();
61-
T::NativeCurrency::mint_into(&caller, amount).unwrap();
61+
T::NativeCurrency::mint_into(&caller, amount).expect("always can mint in test");
6262
}: _(RawOrigin::Root, from, dest, amount, false)
6363

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

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

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

104104
}
Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,2 @@
1-
use crate::loans::DurationSeconds;
2-
use frame_support::pallet_prelude::*;
3-
use scale_info::TypeInfo;
4-
use sp_runtime::Permill;
5-
6-
#[derive(Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
7-
pub enum AuctionStepFunction {
8-
/// default - direct pass through to dex without steps, just to satisfy defaults and reasonably
9-
/// for testing
10-
LinearDecrease(LinearDecrease),
11-
StairstepExponentialDecrease(StairstepExponentialDecrease),
12-
}
13-
14-
impl Default for AuctionStepFunction {
15-
fn default() -> Self {
16-
Self::LinearDecrease(Default::default())
17-
}
18-
}
19-
20-
#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
21-
pub struct LinearDecrease {
22-
/// Seconds after auction start when the price reaches zero
23-
pub total: DurationSeconds,
24-
}
25-
26-
#[derive(Default, Decode, Encode, Clone, TypeInfo, Debug, PartialEq)]
27-
pub struct StairstepExponentialDecrease {
28-
// Length of time between price drops
29-
pub step: DurationSeconds,
30-
// Per-step multiplicative factor, usually more than 50%, mostly closer to 100%, but not 100%.
31-
// Drop per unit of `step`.
32-
pub cut: Permill,
33-
}
1+
// TODO: how type alias to such generics can be achieved?
2+
// pub type DutchAuction = SellEngine<AuctionStepFunction>;

frame/composable-traits/src/currency.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use codec::FullCodec;
22
use frame_support::pallet_prelude::*;
33
use scale_info::TypeInfo;
4-
use sp_runtime::traits::AtLeast32BitUnsigned;
4+
use sp_runtime::traits::{AtLeast32BitUnsigned, Zero};
55
use sp_std::fmt::Debug;
66

7+
use crate::math::SafeArithmetic;
8+
79
/// really u8, but easy to do math operations
810
pub type Exponent = u32;
911

@@ -77,6 +79,18 @@ impl<
7779
{
7880
}
7981

82+
/// limited counted number trait which maximal number is more than `u64`, but not more than `u128`,
83+
/// so inner type is either u64 or u128 with helpers for producing `ArithmeticError`s instead of
84+
/// `Option`s.
85+
pub trait MathBalance:
86+
PartialOrd + Zero + SafeArithmetic + Into<u128> + TryFrom<u128> + From<u64> + Copy
87+
{
88+
}
89+
impl<T: PartialOrd + Zero + SafeArithmetic + Into<u128> + TryFrom<u128> + From<u64> + Copy>
90+
MathBalance for T
91+
{
92+
}
93+
8094
// hack to imitate type alias until it is in stable
8195
// named with like implying it is`like` is is necessary to be `AssetId`, but may be not enough (if
8296
// something is `AssetIdLike` than it is not always asset)

frame/composable-traits/src/defi.rs

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
1-
//! Common codes for defi pallets
2-
1+
//! Common codes and conventions for DeFi pallets
32
use codec::{Codec, Decode, Encode, FullCodec};
43
use frame_support::{pallet_prelude::MaybeSerializeDeserialize, Parameter};
54
use scale_info::TypeInfo;
65
use sp_runtime::{
6+
helpers_128bit::multiply_by_rational,
77
traits::{CheckedAdd, CheckedMul, CheckedSub, Zero},
8-
ArithmeticError, DispatchError, FixedPointOperand, FixedU128,
8+
ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128,
99
};
1010

11-
use crate::{
12-
currency::{AssetIdLike, BalanceLike},
13-
math::{LiftedFixedBalance, SafeArithmetic},
14-
};
11+
use crate::currency::{AssetIdLike, BalanceLike, MathBalance};
1512

1613
#[derive(Encode, Decode, TypeInfo, Debug, Clone, PartialEq)]
1714
pub struct Take<Balance> {
1815
/// amount of `base`
1916
pub amount: Balance,
2017
/// direction depends on referenced order type
21-
/// either minimal or maximal amount of `quote` for given unit of `base`
22-
pub limit: Balance,
18+
/// either minimal or maximal amount of `quote` for given `base`
19+
/// depending on engine configuration, `limit` can be hard or flexible (change with time)
20+
pub limit: LiftedFixedBalance,
2321
}
2422

25-
impl<Balance: PartialOrd + Zero + SafeArithmetic> Take<Balance> {
23+
impl<Balance: MathBalance> Take<Balance> {
2624
pub fn is_valid(&self) -> bool {
27-
self.amount > Balance::zero() && self.limit > Balance::zero()
25+
self.amount > Balance::zero() && self.limit > Ratio::zero()
2826
}
29-
pub fn new(amount: Balance, limit: Balance) -> Self {
27+
28+
pub fn new(amount: Balance, limit: Ratio) -> Self {
3029
Self { amount, limit }
3130
}
3231

33-
pub fn quote_amount(&self) -> Result<Balance, ArithmeticError> {
34-
self.amount.safe_mul(&self.limit)
32+
pub fn quote_limit_amount(&self) -> Result<Balance, ArithmeticError> {
33+
self.quote_amount(self.amount)
34+
}
35+
36+
pub fn quote_amount(&self, amount: Balance) -> Result<Balance, ArithmeticError> {
37+
let result = multiply_by_rational(amount.into(), self.limit.into_inner(), Ratio::DIV)
38+
.map_err(|_| ArithmeticError::Overflow)?;
39+
result.try_into().map_err(|_| ArithmeticError::Overflow)
3540
}
3641
}
3742

@@ -42,15 +47,15 @@ pub struct Sell<AssetId, Balance> {
4247
pub take: Take<Balance>,
4348
}
4449

45-
impl<AssetId: PartialEq, Balance: PartialOrd + Zero + SafeArithmetic> Sell<AssetId, Balance> {
50+
impl<AssetId: PartialEq, Balance: MathBalance> Sell<AssetId, Balance> {
4651
pub fn is_valid(&self) -> bool {
4752
self.take.is_valid()
4853
}
4954
pub fn new(
5055
base: AssetId,
5156
quote: AssetId,
5257
base_amount: Balance,
53-
minimal_base_unit_price_in_quote: Balance,
58+
minimal_base_unit_price_in_quote: Ratio,
5459
) -> Self {
5560
Self {
5661
take: Take { amount: base_amount, limit: minimal_base_unit_price_in_quote },
@@ -70,9 +75,11 @@ impl<AssetId: PartialEq, Balance: PartialOrd + Zero + SafeArithmetic> Sell<Asset
7075
pub struct CurrencyPair<AssetId> {
7176
/// See [Base Currency](https://www.investopedia.com/terms/b/basecurrency.asp).
7277
/// Also can be named `native`(to the market) currency.
78+
/// Usually less stable, can be used as collateral.
7379
pub base: AssetId,
7480
/// Counter currency.
7581
/// Also can be named `price` currency.
82+
/// Usually more stable, may be `borrowable` asset.
7683
pub quote: AssetId,
7784
}
7885

@@ -90,9 +97,10 @@ impl<AssetId: PartialEq> CurrencyPair<AssetId> {
9097
/// assert_eq!(slice[0], pair.base);
9198
/// assert_eq!(slice[1], pair.quote);
9299
/// ```
93-
/// ```compile_fail
100+
/// ```rust
94101
/// # let pair = composable_traits::defi::CurrencyPair::<u128>::new(13, 42);
95102
/// # let slice = pair.as_slice();
103+
/// // it is copy
96104
/// drop(pair);
97105
/// let _ = slice[0];
98106
/// ```
@@ -185,6 +193,7 @@ pub trait DeFiComposableConfig: frame_system::Config {
185193
type MayBeAssetId: AssetIdLike + MaybeSerializeDeserialize + Default;
186194

187195
type Balance: BalanceLike
196+
+ MathBalance
188197
+ Default
189198
+ Parameter
190199
+ Codec
@@ -197,12 +206,52 @@ pub trait DeFiComposableConfig: frame_system::Config {
197206
+ From<u64> // at least 64 bit
198207
+ Zero
199208
+ FixedPointOperand
200-
+ Into<LiftedFixedBalance> // integer part not more than bits in this
201209
+ Into<u128>; // cannot do From<u128>, until LiftedFixedBalance integer part is larger than 128
202210
// bit
203211
}
204212

205-
/// The fixed point number from 0..to max.
206-
/// Unlike `Ratio` it can be more than 1.
207-
/// And unlike `NormalizedCollateralFactor`, it can be less than one.
213+
/// The fixed point number from 0..to max
208214
pub type Rate = FixedU128;
215+
216+
/// Is [1..MAX]
217+
pub type OneOrMoreFixedU128 = FixedU128;
218+
219+
/// The fixed point number of suggested by substrate precision
220+
/// Must be (1.0..MAX] because applied only to price normalized values
221+
pub type MoreThanOneFixedU128 = FixedU128;
222+
223+
/// Must be [0..1]
224+
pub type ZeroToOneFixedU128 = FixedU128;
225+
226+
/// Number like of higher bits, so that amount and balance calculations are done it it with higher
227+
/// precision via fixed point.
228+
/// While this is 128 bit, cannot support u128 because 18 bits are for of mantissa (so maximal
229+
/// integer is 110 bit). Can support u128 if lift upper to use FixedU256 analog.
230+
pub type LiftedFixedBalance = FixedU128;
231+
232+
/// unitless ratio of one thing to other.
233+
pub type Ratio = FixedU128;
234+
235+
#[cfg(test)]
236+
mod tests {
237+
use super::{Ratio, Take};
238+
use sp_runtime::FixedPointNumber;
239+
240+
#[test]
241+
fn take_ratio_half() {
242+
let price = 10;
243+
let amount = 100_u128;
244+
let take = Take::new(amount, Ratio::saturating_from_integer(price));
245+
let result = take.quote_amount(amount / 2).unwrap();
246+
assert_eq!(result, price * amount / 2);
247+
}
248+
249+
#[test]
250+
fn take_ratio_half_amount_half_price() {
251+
let price_part = 50;
252+
let amount = 100_u128;
253+
let take = Take::new(amount, Ratio::saturating_from_rational(price_part, 100));
254+
let result = take.quote_amount(amount).unwrap();
255+
assert_eq!(result, price_part * amount / 100);
256+
}
257+
}

frame/composable-traits/src/lending/math.rs

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,11 @@ use sp_runtime::{
1212
use sp_arithmetic::per_things::Percent;
1313

1414
use crate::{
15-
defi::Rate,
16-
loans::{DurationSeconds, ONE_HOUR},
17-
math::{LiftedFixedBalance, SafeArithmetic},
15+
defi::{LiftedFixedBalance, Rate, ZeroToOneFixedU128},
16+
math::SafeArithmetic,
17+
time::{DurationSeconds, SECONDS_PER_YEAR_NAIVE},
1818
};
1919

20-
/// The fixed point number of suggested by substrate precision
21-
/// Must be (1.0.. because applied only to price normalized values
22-
pub type NormalizedCollateralFactor = FixedU128;
23-
24-
/// Must be [0..1]
25-
/// TODO: implement Ratio as wrapper over FixedU128
26-
pub type Ratio = FixedU128;
27-
28-
/// current notion of year will take away 1/365 from lenders and give away to borrowers (as does no
29-
/// accounts to length of year)
30-
pub const SECONDS_PER_YEAR: DurationSeconds = 365 * 24 * ONE_HOUR;
31-
3220
/// utilization_ratio = total_borrows / (total_cash + total_borrows)
3321
pub fn calc_utilization_ratio(
3422
cash: LiftedFixedBalance,
@@ -110,9 +98,13 @@ impl InterestRateModel {
11098
}
11199

112100
/// Calculates the current supply interest rate
113-
pub fn get_supply_rate(borrow_rate: Rate, util: Ratio, reserve_factor: Ratio) -> Rate {
101+
pub fn get_supply_rate(
102+
borrow_rate: Rate,
103+
util: ZeroToOneFixedU128,
104+
reserve_factor: ZeroToOneFixedU128,
105+
) -> Rate {
114106
// ((1 - reserve_factor) * borrow_rate) * utilization
115-
let one_minus_reserve_factor = Ratio::one().saturating_sub(reserve_factor);
107+
let one_minus_reserve_factor = ZeroToOneFixedU128::one().saturating_sub(reserve_factor);
116108
let rate_to_pool = borrow_rate.saturating_mul(one_minus_reserve_factor);
117109

118110
rate_to_pool.saturating_mul(util)
@@ -151,15 +143,18 @@ pub struct JumpModel {
151143
}
152144

153145
impl JumpModel {
154-
pub const MAX_BASE_RATE: Ratio = Ratio::from_inner(100_000_000_000_000_000); // 10%
155-
pub const MAX_JUMP_RATE: Ratio = Ratio::from_inner(300_000_000_000_000_000); // 30%
156-
pub const MAX_FULL_RATE: Ratio = Ratio::from_inner(500_000_000_000_000_000); // 50%
146+
pub const MAX_BASE_RATE: ZeroToOneFixedU128 =
147+
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 10 / 100);
148+
pub const MAX_JUMP_RATE: ZeroToOneFixedU128 =
149+
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 30 / 100);
150+
pub const MAX_FULL_RATE: ZeroToOneFixedU128 =
151+
ZeroToOneFixedU128::from_inner(ZeroToOneFixedU128::DIV * 50 / 100);
157152

158153
/// Create a new rate model
159154
pub fn new(
160-
base_rate: Ratio,
161-
jump_rate: Ratio,
162-
full_rate: Ratio,
155+
base_rate: ZeroToOneFixedU128,
156+
jump_rate: ZeroToOneFixedU128,
157+
full_rate: ZeroToOneFixedU128,
163158
target_utilization: Percent,
164159
) -> Option<JumpModel> {
165160
let model = Self { base_rate, jump_rate, full_rate, target_utilization };
@@ -390,7 +385,7 @@ pub fn accrued_interest(
390385
borrow_rate
391386
.checked_mul_int(amount)?
392387
.checked_mul(delta_time.into())?
393-
.checked_div(SECONDS_PER_YEAR.into())
388+
.checked_div(SECONDS_PER_YEAR_NAIVE.into())
394389
}
395390

396391
/// compounding increment of borrow index
@@ -402,7 +397,7 @@ pub fn increment_index(
402397
borrow_rate
403398
.safe_mul(&index)?
404399
.safe_mul(&FixedU128::saturating_from_integer(delta_time))?
405-
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))?
400+
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))?
406401
.safe_add(&index)
407402
}
408403

@@ -412,5 +407,5 @@ pub fn increment_borrow_rate(
412407
) -> Result<Rate, ArithmeticError> {
413408
borrow_rate
414409
.safe_mul(&FixedU128::saturating_from_integer(delta_time))?
415-
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR))
410+
.safe_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR_NAIVE))
416411
}

0 commit comments

Comments
 (0)