Skip to content

Commit

Permalink
Loans: borrow & repay method using pricing-based principal (#1455)
Browse files Browse the repository at this point in the history
* main implementation

* minor clean change

* all previous tests passing

* add new tests

* fix benchmarks

* fix integration-tests
  • Loading branch information
lemunozm committed Jul 14, 2023
1 parent 10e23ec commit a3bd197
Show file tree
Hide file tree
Showing 15 changed files with 550 additions and 268 deletions.
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,
}

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(),
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(),
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

0 comments on commit a3bd197

Please sign in to comment.