-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lending protocol #737
Lending protocol #737
Conversation
Co-authored-by: Sander Bosma <sanderbosma@gmail.com>
and also distributes rewards based on pToken holdings instead of voucher balance
Lending Protocol
[package] | ||
authors = ["Parallel Team"] | ||
edition = "2021" | ||
name = "pallet-traits" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking about moving the contents of this pallet into other pallets and remove pallet-traits
, but since both the currency
and loans
pallets depend on it, I'm not sure it's possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to merge this pallet yea, what are the dependencies exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main traits pallet_traits
exports are LoansApi and OracleApi.
The currency
pallet uses both traits to define the default CurrencyConversion
trait implementation, so it depends on pallet_traits
. The oracle
pallet depends on currency
. If I moved OracleApi
to the oracle
pallet then I'd get a circular dependency between currency
and oracle
.
The other solution would be to move the entire contents of pallet_traits
to the currency
pallet but this would add some responsibility creep to currency
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like mostly non-invasive code changes to what was already reviewed so I'm mostly happy with this (see outstanding comments). My only gripe is the naming LendToken
tracking the "Loans" pallet - I feel like LoanToken
would make more sense but I'm not gonna block on this.
crates/loans/src/benchmarking.rs
Outdated
const CKSM: CurrencyId = PToken(3); | ||
const CKBTC: CurrencyId = PToken(4); | ||
const LEND_KSM: CurrencyId = LendToken(3); | ||
const LendKBTC: CurrencyId = LendToken(4); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const LendKBTC: CurrencyId = LendToken(4); | |
const Lend_KBTC: CurrencyId = LendToken(4); |
crates/loans/src/lib.rs
Outdated
if total_collateral_value > total_borrow_value { | ||
Ok((total_collateral_value - total_borrow_value, FixedU128::zero())) | ||
} else { | ||
Ok((FixedU128::zero(), total_borrow_value - total_collateral_value)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably would use checked or saturated math here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done and I'll create an issue, there are other places without failsafe math too
crates/loans/src/lib.rs
Outdated
if total_collateral_value > total_borrow_value { | ||
Ok((total_collateral_value - total_borrow_value, FixedU128::zero())) | ||
} else { | ||
Ok((FixedU128::zero(), total_borrow_value - total_collateral_value)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also checked / saturated math here
let amount = Amount::new(orml_tokens::Pallet::<T>::free_balance(ptoken_id, account_id), ptoken_id); | ||
/// Free lending tokens (lend_tokens) of an account, given the underlying | ||
pub fn free_lend_tokens(asset_id: AssetIdOf<T>, account_id: &T::AccountId) -> Result<Amount<T>, DispatchError> { | ||
let lend_token_id = Self::lend_token_id(asset_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth returning an error if asset_id
is already a LendToken
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An error is returned if the asset_id
is already a LendToken: Error::<T>::MarketDoesNotExist
(as in, there is no market for lending out the given asset, so a LendToken for the asset_id
doesn't exist). This happens in Self::lend_token_id(...)
[package] | ||
authors = ["Parallel Team"] | ||
edition = "2021" | ||
name = "pallet-traits" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to merge this pallet yea, what are the dependencies exactly?
@gregdhill My reasoning is that these tokens represent a lending position as opposed to a loan position |
@gregdhill If you mean outstanding comments in other PRs, all of those should be addressed by this PR |
@@ -0,0 +1,237 @@ | |||
use crate::{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is not new, it's just a rename of the previous ptokens.rs
test file
crates/loans/src/lib.rs
Outdated
@@ -245,6 +241,8 @@ pub mod pallet { | |||
CodecError, | |||
/// Collateral is reserved and cannot be liquidated | |||
CollateralReserved, | |||
/// Arithmetic underflow | |||
ArithmeticUnderflow, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's not redefine this, instead let's return ArithmeticError::Underflow
as we do in other pallets
crates/currency/src/lib.rs
Outdated
// get the equivalent lend_token amount using the internal exchange rate | ||
Loans::recompute_collateral_amount(&underlying_amount) | ||
} else { | ||
// Exampe (DOT to INTR): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Exampe (DOT to INTR): | |
// Example (DOT to INTR): |
@@ -42,7 +83,9 @@ pub mod pallet { | |||
/// ## Configuration | |||
/// The pallet's configuration trait. | |||
#[pallet::config] | |||
pub trait Config: frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>> { | |||
pub trait Config: | |||
frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>, CurrencyId = PrimitivesCurrencyId> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not use the concrete type:
frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>, CurrencyId = PrimitivesCurrencyId> | |
frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>, CurrencyId = CurrencyId<Self>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This somehow triggers a compiler bug: https://stackoverflow.com/questions/66631251/why-do-i-get-an-overflow-evaluating-the-requirement-error-for-a-simple-trait-i
error[E0275]: overflow evaluating the requirement `<T as orml_tokens::Config>::CurrencyId == _`
--> crates/currency/src/amount.rs:18:46
|
18 | #[cfg_attr(feature = "testing-utils", derive(Copy))]
| ^^^^
|
= note: this error originates in the derive macro `Copy` (in Nightly builds, run with -Z macro-backtrace for more info)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's because CurrencyId
is defined like this, so there might be a reference loop?
pub type CurrencyId<T> = <T as orml_tokens::Config>::CurrencyId;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I specified the concrete type in the first place was this error:
error[E0599]: no method named `is_lend_token` found for associated type `<T as orml_tokens::Config>::CurrencyId` in the current scope
--> crates/currency/src/lib.rs:46:30
|
46 | if amount.currency().is_lend_token() && to.is_lend_token() {
| ^^^^^^^^^^^^^ method not found in `<T as orml_tokens::Config>::CurrencyId`
impl From<Error> for i32 { | ||
fn from(e: Error) -> i32 { | ||
match e { | ||
Error::RuntimeError => 1, | ||
Error::AccountLiquidityError => 2, | ||
Error::MarketStatusError => 3, | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this trait implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn reward_scale() -> u128 { | ||
10_u128.pow(12) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should refactor this pallet to use FixedPoint
math instead (to be consistent with the rest of our code), can you make a ticket?
|
||
#[pallet::event] | ||
#[pallet::generate_deposit(pub (crate) fn deposit_event)] | ||
pub enum Event<T: Config> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should switch to named events before this is live ideally to limit downstream (squid) changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created issue here: #757
type OnSlash = (); | ||
type OnDeposit = (); | ||
type OnTransfer = (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these not need to be set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes they do... which is exactly why I need to add more integration tests. Seems like I had only added these to the mock runtime
|
||
impl ConvertToBigUint for u128 { | ||
fn get_big_uint(&self) -> BigUint { | ||
self.to_biguint().unwrap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this safe to do? When can to_biguint
fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This snippet is unchanged from the original code. It can fail if the u128
doesn't fit inside a u64
during the conversion:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When let's make it return Result, as we do for other type conversions?
primitives/src/lib.rs
Outdated
/// Loans pallet types | ||
|
||
pub type Price = FixedU128; | ||
pub type Timestamp = u64; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub type Timestamp = u64; | |
pub type Timestamp = Moment; |
fn accrued_interest(borrow_rate: Rate, amount: BalanceOf<T>, delta_time: Timestamp) -> Option<BalanceOf<T>> { | ||
borrow_rate | ||
.checked_mul_int(amount)? | ||
.checked_mul(delta_time.into())? | ||
.checked_div(SECONDS_PER_YEAR.into()) | ||
} | ||
|
||
fn increment_index(borrow_rate: Rate, index: Rate, delta_time: Timestamp) -> Option<Rate> { | ||
borrow_rate | ||
.checked_mul(&index)? | ||
.checked_mul(&FixedU128::saturating_from_integer(delta_time))? | ||
.checked_div(&FixedU128::saturating_from_integer(SECONDS_PER_YEAR)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we dividing by SECONDS_PER_YEAR
? I can see these two functions are only called via RPC so I can only assume this is used to determine APY? Although I think there would be an implicit assumption that Timestamp
is stored in seconds.
On a related note I can see that Substrate uses the "Julian" year, perhaps that is a standard we should consider adopting also elsewhere: https://github.com/paritytech/substrate/blob/ded44948e2d5a398abcb4e342b0513cb690961bb/frame/staking/src/inflation.rs#L42
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how interest accrual was implemented by Parallel (the functions are used everywhere, not just in RPCs), but ultimately it doesn't matter that much. The interest rate model that decides interest rates probably also assumes a fixed-length year. We should double-check but it might actually simplify things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a nice-to-have issue: #758
07ba329
to
13e038e
Compare
Adds lending functionality to the parachain, based on a fork of Parallel Finance that was adapted to have orml-token conforming lend-tokens. This enables usage of these lend-tokens as interbtc vault collateral.
Notes since the last review:
convert
function in the currency crate (to clarify how lend-token conversions are made)Rate
type used by these fields is aFixedU128
which already implementsDefault
, so no need to implement itmaster
from the orml repoCloses #740
Closes #741
Closes #742
Closes #752