Skip to content
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

Merged
merged 64 commits into from
Nov 11, 2022
Merged

Lending protocol #737

merged 64 commits into from
Nov 11, 2022

Conversation

daniel-savu
Copy link
Contributor

@daniel-savu daniel-savu commented Oct 27, 2022

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:

  • Adds comments to the convert function in the currency crate (to clarify how lend-token conversions are made)
  • Adds comments about why the MIN and MAX exchange rate boundaries are required
    • The Rate type used by these fields is a FixedU128 which already implements Default, so no need to implement it
  • Use interlay assets in all loans tests
  • Removes all unused code and dependencies
  • Updates repo to latest master from the orml repo

Closes #740
Closes #741
Closes #742
Closes #752

daniel-savu and others added 30 commits September 19, 2022 18:02
Co-authored-by: Sander Bosma <sanderbosma@gmail.com>
and also distributes rewards based on pToken holdings instead of voucher balance
[package]
authors = ["Parallel Team"]
edition = "2021"
name = "pallet-traits"
Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

@daniel-savu daniel-savu requested a review from sander2 October 31, 2022 10:01
Copy link
Member

@gregdhill gregdhill left a 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.

const CKSM: CurrencyId = PToken(3);
const CKBTC: CurrencyId = PToken(4);
const LEND_KSM: CurrencyId = LendToken(3);
const LendKBTC: CurrencyId = LendToken(4);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const LendKBTC: CurrencyId = LendToken(4);
const Lend_KBTC: CurrencyId = LendToken(4);

Comment on lines 1139 to 1143
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))
}
Copy link
Member

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

Copy link
Contributor Author

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

Comment on lines 1159 to 1163
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))
}
Copy link
Member

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)?;
Copy link
Member

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?

Copy link
Contributor Author

@daniel-savu daniel-savu Nov 4, 2022

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"
Copy link
Member

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?

@daniel-savu
Copy link
Contributor Author

daniel-savu commented Nov 4, 2022

the naming LendToken tracking the "Loans" pallet - I feel like LoanToken would make more sense

@gregdhill My reasoning is that these tokens represent a lending position as opposed to a loan position

@daniel-savu
Copy link
Contributor Author

see outstanding comments

@gregdhill If you mean outstanding comments in other PRs, all of those should be addressed by this PR

@@ -0,0 +1,237 @@
use crate::{
Copy link
Contributor Author

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

@@ -245,6 +241,8 @@ pub mod pallet {
CodecError,
/// Collateral is reserved and cannot be liquidated
CollateralReserved,
/// Arithmetic underflow
ArithmeticUnderflow,
Copy link
Member

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

sander2
sander2 previously approved these changes Nov 4, 2022
// get the equivalent lend_token amount using the internal exchange rate
Loans::recompute_collateral_amount(&underlying_amount)
} else {
// Exampe (DOT to INTR):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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>
Copy link
Member

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:

Suggested change
frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>, CurrencyId = PrimitivesCurrencyId>
frame_system::Config + orml_tokens::Config<Balance = BalanceOf<Self>, CurrencyId = CurrencyId<Self>>

Copy link
Contributor Author

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)

Copy link
Contributor Author

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;

Copy link
Contributor Author

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`

Comment on lines +81 to +89
impl From<Error> for i32 {
fn from(e: Error) -> i32 {
match e {
Error::RuntimeError => 1,
Error::AccountLiquidityError => 2,
Error::MarketStatusError => 3,
}
}
}
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was there in the original implementation, they have these custom RPC errors. As far as I can tell, serializing for json-rpc requires this trait, and since we use JsonRpsee default errors, there's no need for us to implement the trait.

Example in their code, example in our code.

Comment on lines +30 to +32
fn reward_scale() -> u128 {
10_u128.pow(12)
}
Copy link
Member

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?

crates/loans/src/farming.rs Outdated Show resolved Hide resolved

#[pallet::event]
#[pallet::generate_deposit(pub (crate) fn deposit_event)]
pub enum Event<T: Config> {
Copy link
Member

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

Copy link
Contributor Author

@daniel-savu daniel-savu Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue here: #757

Comment on lines 675 to 677
type OnSlash = ();
type OnDeposit = ();
type OnTransfer = ();
Copy link
Member

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?

Copy link
Contributor Author

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()
Copy link
Member

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?

Copy link
Contributor Author

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:

Copy link
Member

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?

/// Loans pallet types

pub type Price = FixedU128;
pub type Timestamp = u64;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub type Timestamp = u64;
pub type Timestamp = Moment;

Comment on lines +157 to +169
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))
}
Copy link
Member

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

Copy link
Contributor Author

@daniel-savu daniel-savu Nov 8, 2022

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.

Copy link
Contributor Author

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

@daniel-savu daniel-savu force-pushed the dan/interbtc-repo-sync branch from 07ba329 to 13e038e Compare November 8, 2022 13:16
@daniel-savu daniel-savu requested a review from gregdhill November 8, 2022 13:33
@gregdhill gregdhill merged commit 4d6d1b3 into master Nov 11, 2022
@gregdhill gregdhill deleted the dan/interbtc-repo-sync branch November 11, 2022 10:43
@sander2 sander2 added this to the Release 1.20 milestone Dec 7, 2022
@nud3l nud3l mentioned this pull request Aug 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants