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

Fix/linear pricing #1812

Merged
merged 25 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3df5900
feat: restrict epoch closing to liquiduty admin
mustermeiszer Apr 15, 2024
e004106
fix: linear pricing
mustermeiszer Apr 15, 2024
7b4ab74
proof: type unsafety
mustermeiszer Apr 15, 2024
9ad611b
revert: type unsafety
mustermeiszer Apr 15, 2024
9dc56e5
fix: tests and adapt logic to allow dyanmic input
mustermeiszer Apr 16, 2024
ca5ab9d
Merge branch 'main' into fix/linear-pricing
mustermeiszer Apr 16, 2024
c002271
chore: generic removal
mustermeiszer Apr 16, 2024
ec1a96b
chore: cleaner logic
mustermeiszer Apr 16, 2024
2c2edc0
fix: Clippy you annoying thing
mustermeiszer Apr 16, 2024
db35635
fix: cliiiiiippyyyyy
mustermeiszer Apr 16, 2024
b1ef424
fix: cliippyy v1m
mustermeiszer Apr 16, 2024
b4854b1
feat: make PoolAdmin configurable
mustermeiszer May 6, 2024
2aca1dd
fix: benchmarks
mustermeiszer May 6, 2024
c978951
feat: make pricing linear optional
mustermeiszer May 6, 2024
e2dd3ca
Merge remote-tracking branch 'origin/main' into fix/linear-pricing
mustermeiszer May 6, 2024
9265bbb
Fix: linear accrual not overpassing maturity (#1842)
lemunozm May 23, 2024
967584d
increment coverage when loan is at maturity date
lemunozm May 23, 2024
2a691e9
fix division_by_zero issue
lemunozm May 23, 2024
45be927
feat: fix linear pricing migration (#1838)
wischli May 23, 2024
8c01edc
Merge remote-tracking branch 'origin/main' into fix/linear-pricing
mustermeiszer May 24, 2024
3001d9d
fix: make mock accept all origins
mustermeiszer May 24, 2024
b831f99
feat: add test for switching between oracle or settlement price
mustermeiszer May 24, 2024
5fee771
fix: not needed where clause
mustermeiszer May 24, 2024
fcddabf
feat: move additional test
mustermeiszer May 24, 2024
b6976c8
fix: nits
mustermeiszer May 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pallets/loans/src/entities/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
entities::pricing::external::ExternalAmount,
pallet::{Config, Error},
types::RepaidAmount,
PriceOf,
};

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
Expand Down Expand Up @@ -61,6 +62,6 @@ impl<T: Config> RepaidInput<T> {
#[scale_info(skip_type_params(T))]
pub enum PriceCollectionInput<T: Config> {
Empty,
Custom(BoundedBTreeMap<T::PriceId, T::Balance, T::MaxActiveLoansPerPool>),
Custom(BoundedBTreeMap<T::PriceId, PriceOf<T>, T::MaxActiveLoansPerPool>),
FromRegistry,
}
101 changes: 100 additions & 1 deletion pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::{
BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions,
MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, RepaymentSchedule,
},
PriceOf,
};

/// Loan information.
Expand Down Expand Up @@ -290,7 +291,7 @@ impl<T: Config> ActiveLoan<T> {
pub fn present_value_by<Rates>(
&self,
rates: &Rates,
prices: &BTreeMap<T::PriceId, T::Balance>,
prices: &BTreeMap<T::PriceId, PriceOf<T>>,
) -> Result<T::Balance, DispatchError>
where
Rates: RateCollection<T::Rate, T::Balance, T::Balance>,
Expand Down Expand Up @@ -581,3 +582,101 @@ impl<T: Config> TryFrom<(T::PoolId, ActiveLoan<T>)> for ActiveLoanInfo<T> {
})
}
}

/// Adds `with_linear_pricing` to ExternalPricing struct for migration to v4
pub mod v3 {
use cfg_traits::{interest::InterestRate, Seconds};
use parity_scale_codec::{Decode, Encode};

use crate::{
entities::{
loans::BlockNumberFor,
pricing::external::v3::{ActivePricing, Pricing},
},
types::{LoanRestrictions, RepaidAmount, RepaymentSchedule},
AssetOf, Config,
};

#[derive(Encode, Decode)]
pub struct ActiveLoan<T: Config> {
schedule: RepaymentSchedule,
collateral: AssetOf<T>,
restrictions: LoanRestrictions,
borrower: T::AccountId,
write_off_percentage: T::Rate,
origination_date: Seconds,
pricing: ActivePricing<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
repayments_on_schedule_until: Seconds,
}

impl<T: Config> ActiveLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ActiveLoan<T> {
super::ActiveLoan {
schedule: self.schedule,
collateral: self.collateral,
restrictions: self.restrictions,
borrower: self.borrower,
write_off_percentage: self.write_off_percentage,
origination_date: self.origination_date,
pricing: self.pricing.migrate(with_linear_pricing),
total_borrowed: self.total_borrowed,
total_repaid: self.total_repaid,
repayments_on_schedule_until: self.repayments_on_schedule_until,
}
}
}

#[derive(Encode, Decode)]
pub struct CreatedLoan<T: Config> {
info: LoanInfo<T>,
borrower: T::AccountId,
}

impl<T: Config> CreatedLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::CreatedLoan<T> {
super::CreatedLoan::<T>::new(self.info.migrate(with_linear_pricing), self.borrower)
}
}

#[derive(Encode, Decode)]
pub struct ClosedLoan<T: Config> {
closed_at: BlockNumberFor<T>,
info: LoanInfo<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
}

impl<T: Config> ClosedLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ClosedLoan<T> {
super::ClosedLoan::<T> {
closed_at: self.closed_at,
info: self.info.migrate(with_linear_pricing),
total_borrowed: self.total_borrowed,
total_repaid: self.total_repaid,
}
}
}

#[derive(Encode, Decode)]
pub struct LoanInfo<T: Config> {
pub schedule: RepaymentSchedule,
pub collateral: AssetOf<T>,
pub interest_rate: InterestRate<T::Rate>,
pub pricing: Pricing<T>,
pub restrictions: LoanRestrictions,
}

impl<T: Config> LoanInfo<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::LoanInfo<T> {
super::LoanInfo::<T> {
pricing: self.pricing.migrate(with_linear_pricing),
schedule: self.schedule,
collateral: self.collateral,
interest_rate: self.interest_rate,
restrictions: self.restrictions,
}
}
}
}
160 changes: 144 additions & 16 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use sp_runtime::{
traits::{EnsureAdd, EnsureFixedPointNumber, EnsureSub, Zero},
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber,
};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::{cmp::min, collections::btree_map::BTreeMap};

use crate::{
entities::interest::ActiveInterestRate,
pallet::{Config, Error},
PriceOf,
};

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
Expand Down Expand Up @@ -76,6 +77,9 @@ pub struct ExternalPricing<T: Config> {
/// borrow/repay and the current oracle price.
/// See [`ExternalAmount::settlement_price`].
pub max_price_variation: T::Rate,

/// If the pricing is estimated with a linear pricing model.
pub with_linear_pricing: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

With this, I think now is much more understandable what's happening 🎉

}

impl<T: Config> ExternalPricing<T> {
Expand Down Expand Up @@ -150,23 +154,58 @@ impl<T: Config> ExternalActivePricing<T> {
}
}

fn linear_accrual_price(&self, maturity: Seconds) -> Result<T::Balance, DispatchError> {
Ok(cfg_utils::math::y_coord_in_rect(
(self.settlement_price_updated, self.latest_settlement_price),
(maturity, self.info.notional),
T::Time::now(),
)?)
fn maybe_with_linear_accrual_price(
&self,
maturity: Seconds,
price: T::Balance,
price_last_updated: Seconds,
) -> Result<T::Balance, DispatchError> {
if self.info.with_linear_pricing {
if min(price_last_updated, maturity) == maturity {
// We can not have 2 'xs' with different 'y' in a rect.
// That only happens at maturity
return Ok(self.info.notional);
}

Ok(cfg_utils::math::y_coord_in_rect(
(min(price_last_updated, maturity), price),
(maturity, self.info.notional),
min(T::Time::now(), maturity),
)?)
} else {
Ok(price)
}
}

pub fn current_price(
&self,
pool_id: T::PoolId,
maturity: Seconds,
) -> Result<T::Balance, DispatchError> {
Ok(match T::PriceRegistry::get(&self.info.price_id, &pool_id) {
Ok(data) => data.0,
Err(_) => self.linear_accrual_price(maturity)?,
})
self.current_price_inner(
maturity,
T::PriceRegistry::get(&self.info.price_id, &pool_id).ok(),
)
}

fn current_price_inner(
Copy link
Contributor

Choose a reason for hiding this comment

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

The current on-chain behavior is:

  • If oracle is found, use oracle price
  • If not found, use linear accrual price.

That behavior can not be modeled right now. We need to choose between:

  • Use oracle price or if not use settlement price (with_linear_pricing = false)
  • Use always linear accrual price (with_linear_pricing = true)

Do we want this? Don't we still want to model the current behavior?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, we want to change that too.

Get the price:

  • IF oracle → use oracle
  • ELSE → use settlement price

Use the price:

  • IF linear pricing → use linear pricing
  • ELSE → just use price

Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between "Get the price" and "Use the price"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Get is either orale or settlement. Use is just for using whatever came out of the get.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would add new 2 test cases to check a loan configured with use_linear_price = true always uses the linear accrual with oracle value and with settlement price

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, after the changes of this PR all loans comes with use_linear_price = true, so we should check the false version instead

&self,
maturity: Seconds,
oracle: Option<PriceOf<T>>,
) -> Result<T::Balance, DispatchError> {
if let Some((oracle_price, oracle_provided_at)) = oracle {
self.maybe_with_linear_accrual_price(
maturity,
oracle_price,
oracle_provided_at.into_seconds(),
)
} else {
self.maybe_with_linear_accrual_price(
maturity,
self.latest_settlement_price,
self.settlement_price_updated,
)
}
}

pub fn outstanding_principal(
Expand Down Expand Up @@ -197,13 +236,10 @@ impl<T: Config> ExternalActivePricing<T> {

pub fn present_value_cached(
&self,
cache: &BTreeMap<T::PriceId, T::Balance>,
cache: &BTreeMap<T::PriceId, PriceOf<T>>,
maturity: Seconds,
) -> Result<T::Balance, DispatchError> {
let price = match cache.get(&self.info.price_id) {
Some(data) => *data,
None => self.linear_accrual_price(maturity)?,
};
let price = self.current_price_inner(maturity, cache.get(&self.info.price_id).copied())?;
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}

Expand Down Expand Up @@ -288,3 +324,95 @@ impl<T: Config> ExternalActivePricing<T> {
Ok(())
}
}

/// Adds `with_linear_pricing` to ExternalPricing struct for migration to v4
pub mod v3 {
use cfg_traits::Seconds;
use parity_scale_codec::{Decode, Encode};

use crate::{
entities::{
interest::ActiveInterestRate,
pricing::{external::MaxBorrowAmount, internal, internal::InternalActivePricing},
},
Config,
};

#[derive(Encode, Decode)]
pub enum Pricing<T: Config> {
Internal(internal::InternalPricing<T>),
External(ExternalPricing<T>),
}

impl<T: Config> Pricing<T> {
pub fn migrate(self, with_linear_pricing: bool) -> crate::entities::pricing::Pricing<T> {
match self {
Pricing::Internal(i) => crate::entities::pricing::Pricing::Internal(i),
Pricing::External(e) => {
crate::entities::pricing::Pricing::External(e.migrate(with_linear_pricing))
}
}
}
}

#[derive(Encode, Decode)]
pub struct ExternalPricing<T: Config> {
pub price_id: T::PriceId,
pub max_borrow_amount: MaxBorrowAmount<T::Quantity>,
pub notional: T::Balance,
pub max_price_variation: T::Rate,
}

#[derive(Encode, Decode)]
pub enum ActivePricing<T: Config> {
Internal(InternalActivePricing<T>),
External(ExternalActivePricing<T>),
}

impl<T: Config> ActivePricing<T> {
pub fn migrate(
self,
with_linear_pricing: bool,
) -> crate::entities::pricing::ActivePricing<T> {
match self {
ActivePricing::Internal(i) => crate::entities::pricing::ActivePricing::Internal(i),
ActivePricing::External(e) => crate::entities::pricing::ActivePricing::External(
e.migrate(with_linear_pricing),
),
}
}
}

#[derive(Encode, Decode)]
pub struct ExternalActivePricing<T: Config> {
info: ExternalPricing<T>,
outstanding_quantity: T::Quantity,
pub interest: ActiveInterestRate<T>,
latest_settlement_price: T::Balance,
settlement_price_updated: Seconds,
}

impl<T: Config> ExternalActivePricing<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ExternalActivePricing<T> {
super::ExternalActivePricing {
info: self.info.migrate(with_linear_pricing),
outstanding_quantity: self.outstanding_quantity,
interest: self.interest,
latest_settlement_price: self.latest_settlement_price,
settlement_price_updated: self.settlement_price_updated,
}
}
}

impl<T: Config> ExternalPricing<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ExternalPricing<T> {
super::ExternalPricing {
price_id: self.price_id,
max_borrow_amount: self.max_borrow_amount,
notional: self.notional,
max_price_variation: self.max_price_variation,
with_linear_pricing,
}
}
}
}
12 changes: 6 additions & 6 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub mod pallet {
pub type AssetOf<T> = (<T as Config>::CollectionId, <T as Config>::ItemId);
pub type PriceOf<T> = (<T as Config>::Balance, <T as Config>::Moment);

const STORAGE_VERSION: StorageVersion = StorageVersion::new(3);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -169,7 +169,7 @@ pub mod pallet {
type Time: TimeAsSecs;

/// Generic time type
type Moment: Parameter + Member + IntoSeconds;
type Moment: Parameter + Member + Copy + IntoSeconds;

/// Used to mint, transfer, and inspect assets.
type NonFungible: Transfer<Self::AccountId>
Expand Down Expand Up @@ -232,7 +232,7 @@ pub mod pallet {

/// Storage for loans that has been created but are not still active.
#[pallet::storage]
pub(crate) type CreatedLoan<T: Config> = StorageDoubleMap<
pub type CreatedLoan<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::PoolId,
Expand All @@ -259,7 +259,7 @@ pub mod pallet {
/// No mutations are expected in this storage.
/// Loans are stored here for historical purposes.
#[pallet::storage]
pub(crate) type ClosedLoan<T: Config> = StorageDoubleMap<
pub type ClosedLoan<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::PoolId,
Expand Down Expand Up @@ -1050,15 +1050,15 @@ pub mod pallet {

pub fn registered_prices(
pool_id: T::PoolId,
) -> Result<BTreeMap<T::PriceId, T::Balance>, DispatchError> {
) -> Result<BTreeMap<T::PriceId, PriceOf<T>>, DispatchError> {
let collection = T::PriceRegistry::collection(&pool_id)?;
Ok(ActiveLoans::<T>::get(pool_id)
.iter()
.filter_map(|(_, loan)| loan.price_id())
.filter_map(|price_id| {
collection
.get(&price_id)
.map(|price| (price_id, price.0))
.map(|price| (price_id, (price.0, price.1)))
.ok()
})
.collect::<BTreeMap<_, _>>())
Expand Down
Loading
Loading