Skip to content

Commit

Permalink
Loans: cashflows simple version (#1867)
Browse files Browse the repository at this point in the history
* updated from #1408

* add testing

* organize and polish cashflow module

* add Runtime API

* update types.md

* make it works without chronoutils

* minor changes

* add borrow check

* fix legacy increase_debt test

* add loan cashflow tests

* compute principal and interest

* correct validation

* add CashflowPayment type

* add variant error

* fix interest computation when months are partial

* remove Rate usage and use weight

* fix start date for cashflows

* rename api name

* fix benchmarks

* taplo fmt

* using a lower discount rate to simply benchmarking

* rewrite doc line

* interest computed at maturity

* remove borrow support

* fix compilation

* None to OnceAtMaturity variant

* compilation fixes

* Loans: multi cashflows fix external loan (#1864)

* correct principal/interest for both kind of loans

* support for external prices

* add external test

* simplify implementation

* start from repayments
  • Loading branch information
lemunozm authored Jun 7, 2024
1 parent 3769a0b commit 2b4f94f
Show file tree
Hide file tree
Showing 24 changed files with 524 additions and 214 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ rev_slice = { version = "0.1.5", default-features = false }
impl-trait-for-tuples = "0.2.1"
num-traits = { version = "0.2", default-features = false }
num_enum = { version = "0.5.3", default-features = false }
chrono = { version = "0.4", default-features = false }

# Cumulus
cumulus-pallet-aura-ext = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" }
Expand Down
1 change: 1 addition & 0 deletions pallets/loans/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pallet-timestamp = { workspace = true, default-features = true }
pallet-uniques = { workspace = true, default-features = true }

cfg-mocks = { workspace = true, default-features = true }
chrono = { workspace = true }

[features]
default = ["std"]
Expand Down
38 changes: 20 additions & 18 deletions pallets/loans/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@
set namespaceSeparator ::
hide methods
enum Maturity {
Fixed::date: Seconds
Fixed::extension: Seconds
}
package cashflow {
enum Maturity {
Fixed::date: Seconds
Fixed::extension: Seconds
}
enum InterestPayments {
None
}
enum InterestPayments {
OnceAtMaturity
}
enum PayDownSchedule {
None
}
enum PayDownSchedule {
None
}
class RepaymentSchedule {
maturity: Maturity
interest_payments: InterestPayments
pay_down_schedule: PayDownSchedule
}
class RepaymentSchedule {
maturity: Maturity
interest_payments: InterestPayments
pay_down_schedule: PayDownSchedule
}
RepaymentSchedule *--> Maturity
RepaymentSchedule *---> PayDownSchedule
RepaymentSchedule *----> InterestPayments
RepaymentSchedule *--> Maturity
RepaymentSchedule *--> PayDownSchedule
RepaymentSchedule *--> InterestPayments
}
enum BorrowRestrictions {
NoWrittenOff
Expand Down
17 changes: 9 additions & 8 deletions pallets/loans/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use cfg_traits::{
benchmarking::FundedPoolBenchmarkHelper,
changes::ChangeGuard,
interest::{CompoundingSchedule, InterestAccrual, InterestRate},
Permissions, PoolWriteOffPolicyMutate, Seconds, TimeAsSecs, ValueProvider,
Permissions, PoolWriteOffPolicyMutate, TimeAsSecs, ValueProvider,
};
use cfg_types::{
adjustments::Adjustment,
Expand All @@ -40,13 +40,12 @@ use crate::{
},
pallet::*,
types::{
cashflow::{InterestPayments, Maturity, PayDownSchedule, RepaymentSchedule},
valuation::{DiscountedCashFlow, ValuationMethod},
BorrowRestrictions, InterestPayments, LoanRestrictions, Maturity, PayDownSchedule,
RepayRestrictions, RepaymentSchedule,
BorrowRestrictions, LoanRestrictions, RepayRestrictions,
},
};

const OFFSET: Seconds = 120;
const COLLECION_ID: u16 = 42;
const COLLATERAL_VALUE: u128 = 1_000_000;
const FUNDS: u128 = 1_000_000_000;
Expand Down Expand Up @@ -125,10 +124,12 @@ where
}

fn base_loan(item_id: T::ItemId) -> LoanInfo<T> {
let maturity_offset = 40 * 365 * 24 * 3600; // 40 years

LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::fixed(T::Time::now() + OFFSET),
interest_payments: InterestPayments::None,
maturity: Maturity::fixed(T::Time::now() + maturity_offset),
interest_payments: InterestPayments::OnceAtMaturity,
pay_down_schedule: PayDownSchedule::None,
},
collateral: (COLLECION_ID.into(), item_id),
Expand All @@ -145,7 +146,7 @@ where
probability_of_default: T::Rate::zero(),
loss_given_default: T::Rate::zero(),
discount_rate: InterestRate::Fixed {
rate_per_year: T::Rate::one(),
rate_per_year: T::Rate::saturating_from_rational(1, 5000),
compounding: CompoundingSchedule::Secondly,
},
}),
Expand Down Expand Up @@ -199,7 +200,7 @@ where
}

fn create_mutation() -> LoanMutation<T::Rate> {
LoanMutation::InterestPayments(InterestPayments::None)
LoanMutation::InterestPayments(InterestPayments::OnceAtMaturity)
}

fn propose_mutation(pool_id: T::PoolId, loan_id: T::LoanId) -> T::Hash {
Expand Down
5 changes: 3 additions & 2 deletions pallets/loans/src/entities/changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use crate::{
entities::input::{PrincipalInput, RepaidInput},
pallet::Config,
types::{
policy::WriteOffRule, valuation::ValuationMethod, InterestPayments, Maturity,
PayDownSchedule,
cashflow::{InterestPayments, Maturity, PayDownSchedule},
policy::WriteOffRule,
valuation::ValuationMethod,
},
};

Expand Down
78 changes: 45 additions & 33 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use sp_runtime::{
},
DispatchError,
};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::{collections::btree_map::BTreeMap, vec::Vec};

use crate::{
entities::{
Expand All @@ -27,9 +27,10 @@ use crate::{
},
pallet::{AssetOf, Config, Error},
types::{
cashflow::{CashflowPayment, RepaymentSchedule},
policy::{WriteOffStatus, WriteOffTrigger},
BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions,
MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, RepaymentSchedule,
MutationError, RepaidAmount, RepayLoanError, RepayRestrictions,
},
PriceOf,
};
Expand Down Expand Up @@ -71,7 +72,7 @@ impl<T: Config> LoanInfo<T> {
T::InterestAccrual::validate_rate(&self.interest_rate)?;

ensure!(
self.schedule.is_valid(now),
self.schedule.is_valid(now)?,
Error::<T>::from(CreateLoanError::InvalidRepaymentSchedule)
);

Expand Down Expand Up @@ -223,6 +224,10 @@ impl<T: Config> ActiveLoan<T> {
&self.borrower
}

pub fn origination_date(&self) -> Seconds {
self.origination_date
}

pub fn maturity_date(&self) -> Option<Seconds> {
self.schedule.maturity.date()
}
Expand All @@ -238,13 +243,28 @@ impl<T: Config> ActiveLoan<T> {
}
}

pub fn principal(&self) -> Result<T::Balance, DispatchError> {
Ok(self
.total_borrowed
.ensure_sub(self.total_repaid.principal)?)
}

pub fn expected_cashflows(&self) -> Result<Vec<CashflowPayment<T::Balance>>, DispatchError> {
self.schedule.generate_cashflows(
self.repayments_on_schedule_until,
self.principal()?,
match &self.pricing {
ActivePricing::Internal(_) => self.principal()?,
ActivePricing::External(inner) => inner.outstanding_notional_principal()?,
},
self.pricing.interest().rate(),
)
}

pub fn write_off_status(&self) -> WriteOffStatus<T::Rate> {
WriteOffStatus {
percentage: self.write_off_percentage,
penalty: match &self.pricing {
ActivePricing::Internal(inner) => inner.interest.penalty(),
ActivePricing::External(inner) => inner.interest.penalty(),
},
penalty: self.pricing.interest().penalty(),
}
}

Expand Down Expand Up @@ -364,6 +384,8 @@ impl<T: Config> ActiveLoan<T> {
}
}

self.repayments_on_schedule_until = T::Time::now();

Ok(())
}

Expand All @@ -380,11 +402,8 @@ impl<T: Config> ActiveLoan<T> {
) -> Result<RepaidInput<T>, DispatchError> {
let (max_repay_principal, outstanding_interest) = match &self.pricing {
ActivePricing::Internal(inner) => {
amount.principal.internal()?;

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

(principal, inner.outstanding_interest(principal)?)
}
Expand Down Expand Up @@ -438,28 +457,26 @@ impl<T: Config> ActiveLoan<T> {
}
}

self.repayments_on_schedule_until = T::Time::now();

Ok(amount)
}

pub fn write_off(&mut self, new_status: &WriteOffStatus<T::Rate>) -> DispatchResult {
let penalty = new_status.penalty;
match &mut self.pricing {
ActivePricing::Internal(inner) => inner.interest.set_penalty(penalty)?,
ActivePricing::External(inner) => inner.interest.set_penalty(penalty)?,
}
self.pricing
.interest_mut()
.set_penalty(new_status.penalty)?;

self.write_off_percentage = new_status.percentage;

Ok(())
}

fn ensure_can_close(&self) -> DispatchResult {
let can_close = match &self.pricing {
ActivePricing::Internal(inner) => !inner.interest.has_debt(),
ActivePricing::External(inner) => !inner.interest.has_debt(),
};

ensure!(can_close, Error::<T>::from(CloseLoanError::NotFullyRepaid));
ensure!(
!self.pricing.interest().has_debt(),
Error::<T>::from(CloseLoanError::NotFullyRepaid)
);

Ok(())
}
Expand Down Expand Up @@ -502,10 +519,7 @@ impl<T: Config> ActiveLoan<T> {
.maturity
.extends(extension)
.map_err(|_| Error::<T>::from(MutationError::MaturityExtendedTooMuch))?,
LoanMutation::InterestRate(rate) => match &mut self.pricing {
ActivePricing::Internal(inner) => inner.interest.set_base_rate(rate)?,
ActivePricing::External(inner) => inner.interest.set_base_rate(rate)?,
},
LoanMutation::InterestRate(rate) => self.pricing.interest_mut().set_base_rate(rate)?,
LoanMutation::InterestPayments(payments) => self.schedule.interest_payments = payments,
LoanMutation::PayDownSchedule(schedule) => self.schedule.pay_down_schedule = schedule,
LoanMutation::Internal(mutation) => match &mut self.pricing {
Expand All @@ -521,7 +535,7 @@ impl<T: Config> ActiveLoan<T> {

#[cfg(feature = "runtime-benchmarks")]
pub fn set_maturity(&mut self, duration: Seconds) {
self.schedule.maturity = crate::types::Maturity::fixed(duration);
self.schedule.maturity = crate::types::cashflow::Maturity::fixed(duration);
}
}

Expand Down Expand Up @@ -557,9 +571,7 @@ impl<T: Config> TryFrom<(T::PoolId, ActiveLoan<T>)> for ActiveLoanInfo<T> {

Ok(match &active_loan.pricing {
ActivePricing::Internal(inner) => {
let principal = active_loan
.total_borrowed
.ensure_sub(active_loan.total_repaid.principal)?;
let principal = active_loan.principal()?;

Self {
present_value,
Expand All @@ -574,7 +586,7 @@ impl<T: Config> TryFrom<(T::PoolId, ActiveLoan<T>)> for ActiveLoanInfo<T> {

Self {
present_value,
outstanding_principal: inner.outstanding_principal(pool_id, maturity)?,
outstanding_principal: inner.outstanding_priced_principal(pool_id, maturity)?,
outstanding_interest: inner.outstanding_interest()?,
current_price: Some(inner.current_price(pool_id, maturity)?),
active_loan,
Expand All @@ -594,7 +606,7 @@ pub mod v3 {
loans::BlockNumberFor,
pricing::external::v3::{ActivePricing, Pricing},
},
types::{LoanRestrictions, RepaidAmount, RepaymentSchedule},
types::{cashflow::RepaymentSchedule, LoanRestrictions, RepaidAmount},
AssetOf, Config,
};

Expand Down
18 changes: 17 additions & 1 deletion pallets/loans/src/entities/pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use frame_support::RuntimeDebugNoBound;
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;

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

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

impl<T: Config> ActivePricing<T> {
pub fn interest(&self) -> &ActiveInterestRate<T> {
match self {
Self::Internal(inner) => &inner.interest,
Self::External(inner) => &inner.interest,
}
}

pub fn interest_mut(&mut self) -> &mut ActiveInterestRate<T> {
match self {
Self::Internal(inner) => &mut inner.interest,
Self::External(inner) => &mut inner.interest,
}
}
}
16 changes: 9 additions & 7 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,13 @@ impl<T: Config> ExternalActivePricing<T> {
}
}

pub fn outstanding_principal(
pub fn outstanding_notional_principal(&self) -> Result<T::Balance, DispatchError> {
Ok(self
.outstanding_quantity
.ensure_mul_int(self.info.notional)?)
}

pub fn outstanding_priced_principal(
&self,
pool_id: T::PoolId,
maturity: Option<Seconds>,
Expand All @@ -218,20 +224,16 @@ impl<T: Config> ExternalActivePricing<T> {
}

pub fn outstanding_interest(&self) -> Result<T::Balance, DispatchError> {
let outstanding_notional = self
.outstanding_quantity
.ensure_mul_int(self.info.notional)?;

let debt = self.interest.current_debt()?;
Ok(debt.ensure_sub(outstanding_notional)?)
Ok(debt.ensure_sub(self.outstanding_notional_principal()?)?)
}

pub fn present_value(
&self,
pool_id: T::PoolId,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
self.outstanding_principal(pool_id, maturity)
self.outstanding_priced_principal(pool_id, maturity)
}

pub fn present_value_cached(
Expand Down
Loading

0 comments on commit 2b4f94f

Please sign in to comment.