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: multiple cashflows #1797

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8ca7dc2
updated from #1408
lemunozm Apr 5, 2024
9945d65
add testing
lemunozm Apr 15, 2024
d129f98
organize and polish cashflow module
lemunozm Apr 19, 2024
a4c790a
add Runtime API
lemunozm Apr 19, 2024
db17ebc
update types.md
lemunozm Apr 19, 2024
6fdf348
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm Apr 23, 2024
85eb05f
make it works without chronoutils
lemunozm Apr 25, 2024
bd61fea
minor changes
lemunozm Apr 25, 2024
abc3ef0
add borrow check
lemunozm Apr 25, 2024
2de713b
fix legacy increase_debt test
lemunozm Apr 25, 2024
87a0d07
add loan cashflow tests
lemunozm Apr 26, 2024
4c56cb2
compute principal and interest
lemunozm Apr 27, 2024
76de6d0
correct validation
lemunozm May 7, 2024
b06d980
add CashflowPayment type
lemunozm May 7, 2024
317990a
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm May 7, 2024
8c41d5d
add variant error
lemunozm May 7, 2024
1d60312
fix interest computation when months are partial
lemunozm May 8, 2024
df15e66
remove Rate usage and use weight
lemunozm May 8, 2024
a60d0fd
fix start date for cashflows
lemunozm May 8, 2024
a864ad4
rename api name
lemunozm May 8, 2024
39eb12d
fix benchmarks
lemunozm May 9, 2024
010469d
taplo fmt
lemunozm May 9, 2024
8216dda
using a lower discount rate to simply benchmarking
lemunozm May 9, 2024
b986de0
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm May 13, 2024
77dada6
rewrite doc line
lemunozm May 13, 2024
3321024
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm May 24, 2024
205fb68
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm Jun 5, 2024
338cece
interest computed at maturity
lemunozm Jun 6, 2024
2c4a180
remove borrow support
lemunozm Jun 6, 2024
d37a4e4
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm Jun 6, 2024
70ea49c
fix compilation
lemunozm Jun 6, 2024
f9db4fc
None to OnceAtMaturity variant
lemunozm Jun 6, 2024
12affcc
compilation fixes
lemunozm Jun 6, 2024
2e79fc2
Merge remote-tracking branch 'origin/main' into loans/multi-cashflows
lemunozm Jun 6, 2024
b7ee635
Loans: multi cashflows fix external loan (#1864)
lemunozm Jun 7, 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
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 @@ -13,6 +13,7 @@ documentation.workspace = true
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
chrono = { workspace = true }
parity-scale-codec = { workspace = true }
scale-info = { workspace = true }

Expand Down
39 changes: 21 additions & 18 deletions pallets/loans/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,31 @@
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 {
None
Monthly::reference_day: u8
}

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
15 changes: 8 additions & 7 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 @@ -124,10 +123,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::Monthly(1),
pay_down_schedule: PayDownSchedule::None,
},
collateral: (COLLECION_ID.into(), item_id),
Expand All @@ -144,7 +145,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
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
80 changes: 49 additions & 31 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,
},
};

Expand Down Expand Up @@ -70,7 +71,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 @@ -222,6 +223,10 @@ impl<T: Config> ActiveLoan<T> {
&self.borrower
}

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

pub fn maturity_date(&self) -> Seconds {
self.schedule.maturity.date()
}
Expand All @@ -237,13 +242,24 @@ 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()?,
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 @@ -345,6 +361,18 @@ impl<T: Config> ActiveLoan<T> {
Error::<T>::from(BorrowLoanError::MaturityDatePassed)
);

let expected_payment = self.schedule.expected_payment(
self.repayments_on_schedule_until,
self.principal()?,
self.pricing.interest().rate(),
now,
)?;

ensure!(
self.total_repaid.effective()? >= expected_payment,
Error::<T>::from(BorrowLoanError::PaymentOverdue)
);

Ok(())
}

Expand All @@ -362,6 +390,8 @@ impl<T: Config> ActiveLoan<T> {
}
}

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

Ok(())
}

Expand All @@ -378,11 +408,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 @@ -440,24 +467,20 @@ impl<T: Config> ActiveLoan<T> {
}

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 @@ -500,10 +523,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 @@ -519,7 +539,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 @@ -555,9 +575,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 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: 14 additions & 2 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub mod pallet {
use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec};
use types::{
self,
cashflow::CashflowPayment,
policy::{self, WriteOffRule, WriteOffStatus},
BorrowLoanError, CloseLoanError, CreateLoanError, MutationError, RepayLoanError,
WrittenOffError,
Expand Down Expand Up @@ -1199,7 +1200,7 @@ pub mod pallet {
) -> Result<PortfolioInfoOf<T>, DispatchError> {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.map(|(loan_id, loan)| Ok((loan_id, (pool_id, loan).try_into()?)))
.map(|(loan_id, loan)| Ok((loan_id, ActiveLoanInfo::try_from((pool_id, loan))?)))
.collect()
}

Expand All @@ -1210,9 +1211,20 @@ pub mod pallet {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.find(|(id, _)| *id == loan_id)
.map(|(_, loan)| (pool_id, loan).try_into())
.map(|(_, loan)| ActiveLoanInfo::try_from((pool_id, loan)))
.transpose()
}

pub fn expected_cashflows(
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> Result<Vec<CashflowPayment<T::Balance>>, DispatchError> {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.find(|(id, _)| *id == loan_id)
.map(|(_, loan)| loan.expected_cashflows())
.ok_or(Error::<T>::LoanNotActiveOrNotFound)?
}
}

// TODO: This implementation can be cleaned once #908 be solved
Expand Down
Loading
Loading