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

Loans: borrow & repay method using pricing-based principal #1455

Merged
merged 6 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 11 additions & 7 deletions pallets/loans/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ use crate::{
loans::LoanInfo,
pricing::{
internal::{InternalPricing, MaxBorrowAmount},
Pricing,
Pricing, PricingAmount, RepaidPricingAmount,
},
},
pallet::*,
types::{
policy::{WriteOffRule, WriteOffTrigger},
valuation::{DiscountedCashFlow, ValuationMethod},
BorrowRestrictions, InterestPayments, LoanMutation, LoanRestrictions, Maturity,
PayDownSchedule, RepaidAmount, RepayRestrictions, RepaymentSchedule,
PayDownSchedule, RepayRestrictions, RepaymentSchedule,
},
};

Expand Down Expand Up @@ -189,7 +189,7 @@ where
RawOrigin::Signed(borrower).into(),
pool_id,
loan_id,
10.into(),
PricingAmount::Internal(10.into()),
)
.unwrap();
}
Expand All @@ -200,8 +200,8 @@ where
RawOrigin::Signed(borrower).into(),
pool_id,
loan_id,
RepaidAmount {
principal: 10.into(),
RepaidPricingAmount {
principal: PricingAmount::Internal(10.into()),
interest: T::Balance::max_value(),
unscheduled: 0.into(),
},
Expand Down Expand Up @@ -341,7 +341,7 @@ benchmarks! {
let pool_id = Helper::<T>::initialize_active_state(n);
let loan_id = Helper::<T>::create_loan(pool_id, u16::MAX.into());

}: _(RawOrigin::Signed(borrower), pool_id, loan_id, 10.into())
}: _(RawOrigin::Signed(borrower), pool_id, loan_id, PricingAmount::Internal(10.into()))

repay {
let n in 1..Helper::<T>::max_active_loans() - 1;
Expand All @@ -351,7 +351,11 @@ benchmarks! {
let loan_id = Helper::<T>::create_loan(pool_id, u16::MAX.into());
Helper::<T>::borrow_loan(pool_id, loan_id);

let repaid = RepaidAmount { principal: 10.into(), interest: 0.into(), unscheduled: 0.into() };
let repaid = RepaidPricingAmount {
principal: PricingAmount::Internal(10.into()),
interest: 0.into(),
unscheduled: 0.into()
};

}: _(RawOrigin::Signed(borrower), pool_id, loan_id, repaid)

Expand Down
57 changes: 38 additions & 19 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use sp_runtime::{

use super::pricing::{
external::ExternalActivePricing, internal::InternalActivePricing, ActivePricing, Pricing,
PricingAmount, RepaidPricingAmount,
};
use crate::{
pallet::{AssetOf, Config, Error, PriceOf},
Expand Down Expand Up @@ -279,22 +280,28 @@ impl<T: Config> ActiveLoan<T> {
self.write_down(value)
}

fn ensure_can_borrow(&self, amount: T::Balance) -> DispatchResult {
fn ensure_can_borrow(&self, amount: &PricingAmount<T>) -> DispatchResult {
let max_borrow_amount = match &self.pricing {
ActivePricing::Internal(inner) => inner.max_borrow_amount(self.total_borrowed)?,
ActivePricing::External(inner) => inner.max_borrow_amount(amount)?,
ActivePricing::Internal(inner) => {
amount.internal()?;
inner.max_borrow_amount(self.total_borrowed)?
}
ActivePricing::External(inner) => {
let external_amount = amount.external()?;
inner.max_borrow_amount(external_amount)?
}
};

ensure!(
amount <= max_borrow_amount,
amount.balance()? <= max_borrow_amount,
Error::<T>::from(BorrowLoanError::MaxAmountExceeded)
);

ensure!(
match self.restrictions.borrows {
BorrowRestrictions::NotWrittenOff => self.write_off_status().is_none(),
BorrowRestrictions::FullOnce => {
self.total_borrowed.is_zero() && amount == max_borrow_amount
self.total_borrowed.is_zero() && amount.balance()? == max_borrow_amount
}
},
Error::<T>::from(BorrowLoanError::Restriction)
Expand All @@ -309,15 +316,18 @@ impl<T: Config> ActiveLoan<T> {
Ok(())
}

pub fn borrow(&mut self, amount: T::Balance) -> DispatchResult {
pub fn borrow(&mut self, amount: &PricingAmount<T>) -> DispatchResult {
self.ensure_can_borrow(amount)?;

self.total_borrowed.ensure_add_assign(amount)?;
self.total_borrowed.ensure_add_assign(amount.balance()?)?;

match &mut self.pricing {
ActivePricing::Internal(inner) => inner.adjust(Adjustment::Increase(amount))?,
ActivePricing::Internal(inner) => {
inner.adjust(Adjustment::Increase(amount.balance()?))?
}
ActivePricing::External(inner) => {
inner.adjust(Adjustment::Increase(amount), Zero::zero())?
let quantity = amount.external()?.quantity;
inner.adjust(Adjustment::Increase(quantity), Zero::zero())?
}
}

Expand All @@ -332,33 +342,39 @@ impl<T: Config> ActiveLoan<T> {
/// - Checking repay restrictions
fn prepare_repayment(
&self,
mut amount: RepaidAmount<T::Balance>,
) -> Result<RepaidAmount<T::Balance>, DispatchError> {
mut amount: RepaidPricingAmount<T>,
) -> Result<RepaidPricingAmount<T>, DispatchError> {
let (interest_accrued, max_repay_principal) = match &self.pricing {
ActivePricing::Internal(inner) => {
amount.principal.internal()?;

let principal = self
.total_borrowed
.ensure_sub(self.total_repaid.principal)?;

(inner.current_interest(principal)?, principal)
}
ActivePricing::External(inner) => {
(inner.current_interest()?, inner.outstanding_amount()?)
let external_amount = amount.principal.external()?;
let max_repay_principal = inner.max_repay_principal(external_amount)?;

(inner.current_interest()?, max_repay_principal)
}
};

amount.interest = amount.interest.min(interest_accrued);

ensure!(
amount.principal <= max_repay_principal,
amount.principal.balance()? <= max_repay_principal,
Error::<T>::from(RepayLoanError::MaxPrincipalAmountExceeded)
);

ensure!(
match self.restrictions.repayments {
RepayRestrictions::None => true,
RepayRestrictions::Full => {
amount.principal == max_repay_principal && amount.interest == interest_accrued
amount.principal.balance()? == max_repay_principal
&& amount.interest == interest_accrued
}
},
Error::<T>::from(RepayLoanError::Restriction)
Expand All @@ -369,18 +385,21 @@ impl<T: Config> ActiveLoan<T> {

pub fn repay(
&mut self,
amount: RepaidAmount<T::Balance>,
) -> Result<RepaidAmount<T::Balance>, DispatchError> {
amount: RepaidPricingAmount<T>,
) -> Result<RepaidPricingAmount<T>, DispatchError> {
let amount = self.prepare_repayment(amount)?;

self.total_repaid.ensure_add_assign(&amount)?;
self.total_repaid
.ensure_add_assign(&amount.repaid_amount()?)?;

match &mut self.pricing {
ActivePricing::Internal(inner) => {
inner.adjust(Adjustment::Decrease(amount.effective()?))?
let amount = amount.repaid_amount()?.effective()?;
inner.adjust(Adjustment::Decrease(amount))?
}
ActivePricing::External(inner) => {
inner.adjust(Adjustment::Decrease(amount.principal), amount.interest)?
let quantity = amount.principal.external()?.quantity;
inner.adjust(Adjustment::Decrease(quantity), amount.interest)?
}
}

Expand Down
54 changes: 53 additions & 1 deletion pallets/loans/src/entities/pricing.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::RuntimeDebugNoBound;
use scale_info::TypeInfo;
use sp_runtime::{ArithmeticError, DispatchError};

use crate::pallet::Config;
use crate::{
pallet::{Config, Error},
types::RepaidAmount,
};

pub mod external;
pub mod internal;
Expand All @@ -28,3 +32,51 @@ pub enum ActivePricing<T: Config> {
/// Internal attributes
External(external::ExternalActivePricing<T>),
}

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub enum PricingAmount<T: Config> {
Internal(T::Balance),
External(external::ExternalAmount<T>),
}

impl<T: Config> PricingAmount<T> {
pub fn balance(&self) -> Result<T::Balance, ArithmeticError> {
match self {
Self::Internal(amount) => Ok(*amount),
Self::External(external) => external.balance(),
}
}

pub fn internal(&self) -> Result<T::Balance, DispatchError> {
match self {
Self::Internal(amount) => Ok(*amount),
Self::External(_) => Err(Error::<T>::MismatchedPricingMethod.into()),
}
}

pub fn external(&self) -> Result<external::ExternalAmount<T>, DispatchError> {
match self {
Self::Internal(_) => Err(Error::<T>::MismatchedPricingMethod.into()),
Self::External(principal) => Ok(principal.clone()),
}
}
}

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct RepaidPricingAmount<T: Config> {
pub principal: PricingAmount<T>,
pub interest: T::Balance,
pub unscheduled: T::Balance,
}

impl<T: Config> RepaidPricingAmount<T> {
pub fn repaid_amount(&self) -> Result<RepaidAmount<T::Balance>, ArithmeticError> {
Ok(RepaidAmount {
principal: self.principal.balance()?,
interest: self.interest,
unscheduled: self.unscheduled,
})
}
}
77 changes: 50 additions & 27 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,41 @@ use frame_support::{self, ensure, RuntimeDebug, RuntimeDebugNoBound};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{EnsureAdd, EnsureFixedPointNumber, EnsureSub, Zero},
DispatchError, DispatchResult, FixedPointNumber,
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber,
};

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

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct ExternalAmount<T: Config> {
pub quantity: T::Rate,
pub settlement_price: T::Balance,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit pick. Not sure if settlement_price is the right tearm for when repaying? cc @denniswell

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a note: ExternalAmount is used for both: borrow & repay. So if changed there, it's changed for both.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my understanding of what this means, I think the name fits with its purpose.

}

impl<T: Config> ExternalAmount<T> {
pub fn new(quantity: T::Rate, price: T::Balance) -> Self {
Self {
quantity,
settlement_price: price,
}
}

pub fn empty() -> Self {
Self {
quantity: T::Rate::zero(),
settlement_price: T::Balance::zero(),
}
}

pub fn balance(&self) -> Result<T::Balance, ArithmeticError> {
self.quantity.ensure_mul_int(self.settlement_price)
}
}

/// Define the max borrow amount of a loan
#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)]
pub enum MaxBorrowAmount<Quantity> {
Expand Down Expand Up @@ -46,7 +73,7 @@ impl<T: Config> ExternalPricing<T> {
pub fn validate(&self) -> DispatchResult {
if let MaxBorrowAmount::Quantity(quantity) = self.max_borrow_amount {
ensure!(
quantity.frac().is_zero() && quantity > T::Rate::zero(),
quantity.frac().is_zero() && quantity >= T::Rate::zero(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

WHy the change here?

Copy link
Contributor Author

@lemunozm lemunozm Jul 14, 2023

Choose a reason for hiding this comment

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

You can create a loan that can not be borrowed only to repay later unchecked amounts.

Error::<T>::AmountNotNaturalNumber
)
}
Expand Down Expand Up @@ -99,11 +126,6 @@ impl<T: Config> ExternalActivePricing<T> {
Ok(T::PriceRegistry::get(&self.info.price_id)?.1)
}

pub fn outstanding_amount(&self) -> Result<T::Balance, DispatchError> {
let price = self.current_price()?;
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}

pub fn current_interest(&self) -> Result<T::Balance, DispatchError> {
let outstanding_notional = self
.outstanding_quantity
Expand All @@ -114,7 +136,8 @@ impl<T: Config> ExternalActivePricing<T> {
}

pub fn present_value(&self) -> Result<T::Balance, DispatchError> {
self.outstanding_amount()
let price = self.current_price()?;
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}

pub fn present_value_cached<Prices>(&self, cache: &Prices) -> Result<T::Balance, DispatchError>
Expand All @@ -127,42 +150,42 @@ impl<T: Config> ExternalActivePricing<T> {

pub fn max_borrow_amount(
&self,
desired_amount: T::Balance,
amount: ExternalAmount<T>,
) -> Result<T::Balance, DispatchError> {
match self.info.max_borrow_amount {
MaxBorrowAmount::Quantity(quantity) => {
let price = self.current_price()?;
let available = quantity.ensure_sub(self.outstanding_quantity)?;
Ok(available.ensure_mul_int(price)?)
Ok(available.ensure_mul_int(amount.settlement_price)?)
}
MaxBorrowAmount::NoLimit => Ok(desired_amount),
MaxBorrowAmount::NoLimit => Ok(amount.balance()?),
}
}

pub fn max_repay_principal(
&self,
amount: ExternalAmount<T>,
) -> Result<T::Balance, DispatchError> {
Ok(self
.outstanding_quantity
.ensure_mul_int(amount.settlement_price)?)
}

pub fn adjust(
&mut self,
principal_adj: Adjustment<T::Balance>,
quantity_adj: Adjustment<T::Rate>,
interest: T::Balance,
) -> DispatchResult {
let quantity_adj = principal_adj.try_map(|principal| -> Result<_, DispatchError> {
let price = self.current_price()?;

let quantity = T::Rate::ensure_from_rational(principal, price)?;
self.outstanding_quantity = quantity_adj.ensure_add(self.outstanding_quantity)?;

let interest_adj = quantity_adj.try_map(|quantity| -> Result<_, DispatchError> {
ensure!(
quantity.frac().is_zero(),
Error::<T>::AmountNotMultipleOfPrice
quantity.frac().is_zero() && quantity >= T::Rate::zero(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, why the change to allow zero

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's totally fine to repay with 0 of quantity if you want, for example, to pay only interest.

Error::<T>::AmountNotNaturalNumber
);

Ok(quantity)
})?;

self.outstanding_quantity = quantity_adj.ensure_add(self.outstanding_quantity)?;

let interest_adj = quantity_adj.try_map(|quantity| {
quantity
Ok(quantity
.ensure_mul_int(self.info.notional)?
.ensure_add(interest)
.ensure_add(interest)?)
})?;

self.interest.adjust_debt(interest_adj)?;
Expand Down