diff --git a/Cargo.lock b/Cargo.lock index fa6c73ec53..7f89778f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf688625d06217d5b1bb0ea9d9c44a1635fd0ee3534466388d18203174f4d11" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1401,19 +1407,28 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time 0.1.45", "wasm-bindgen", "winapi", ] +[[package]] +name = "chronoutil" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154aa5253c981d51e9466afc1e9ce41631197837fd1c41ee931008f229b8a3d7" +dependencies = [ + "chrono", +] + [[package]] name = "cid" version = "0.8.6" @@ -7680,6 +7695,8 @@ dependencies = [ "cfg-primitives", "cfg-traits", "cfg-types", + "chrono", + "chronoutil", "frame-benchmarking", "frame-support", "frame-system", diff --git a/pallets/loans/Cargo.toml b/pallets/loans/Cargo.toml index fe6a03300e..3b91f8afd3 100644 --- a/pallets/loans/Cargo.toml +++ b/pallets/loans/Cargo.toml @@ -25,6 +25,8 @@ cfg-traits = { path = "../../libs/traits", default-features = false } cfg-types = { path = "../../libs/types", default-features = false } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.38" } +chrono = { version = "0.4", default-features = false } +chronoutil = "0.2" strum = { version = "0.24", default-features = false, features = ["derive"] } # Optionals for benchmarking @@ -57,6 +59,7 @@ std = [ "frame-benchmarking/std", "strum/std", "orml-traits/std", + "chrono/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/pallets/loans/src/benchmarking.rs b/pallets/loans/src/benchmarking.rs index 5821e54fb3..49e17a9e48 100644 --- a/pallets/loans/src/benchmarking.rs +++ b/pallets/loans/src/benchmarking.rs @@ -43,9 +43,10 @@ use crate::{ }, pallet::*, types::{ + cashflow::{InterestPayments, Maturity, PayDownSchedule, RepaymentSchedule}, + policy::{WriteOffRule, WriteOffTrigger}, valuation::{DiscountedCashFlow, ValuationMethod}, - BorrowRestrictions, InterestPayments, LoanMutation, LoanRestrictions, Maturity, - PayDownSchedule, RepayRestrictions, RepaymentSchedule, + BorrowRestrictions, LoanMutation, LoanRestrictions, RepayRestrictions, }, }; diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index f81d53ab9b..f2312fc09d 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -24,10 +24,10 @@ use super::pricing::{ use crate::{ pallet::{AssetOf, Config, Error, PriceOf}, types::{ + cashflow::RepaymentSchedule, policy::{WriteOffStatus, WriteOffTrigger}, BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanMutation, LoanRestrictions, MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, - RepaymentSchedule, }, }; @@ -211,6 +211,12 @@ impl ActiveLoan { &self.pricing } + pub fn principal(&self) -> Result { + Ok(self + .total_borrowed + .ensure_sub(self.total_repaid.principal)?) + } + pub fn write_off_status(&self) -> WriteOffStatus { WriteOffStatus { percentage: self.write_off_percentage, @@ -242,6 +248,35 @@ impl ActiveLoan { } ActivePricing::Internal(_) => Ok(false), }, + WriteOffTrigger::InterestOverdue(_overdue_seconds) => match &self.pricing { + // TODO: should be implemented once interest_rate is moved to ActiveLoan + ActivePricing::External(_) => Ok(false), + ActivePricing::Internal(pricing) => { + let cashflows = self.schedule.generate_expected_cashflows( + self.origination_date, + self.principal()?, + pricing.interest.rate(), + )?; + + // TODO(Luis): If from this point, any field of ActiveLoan is needed, I would + // move this code into cashflow.rs + + /* + let now_date = NaiveDateTime::from_timestamp_opt(now as i64, 0) + .ok_or(DispatchError::Other("Invalid now date"))?; + let cashflows_in_past = cashflows + .iter() + .filter(|(d, _)| d.and_hms_opt(0, 0, 0).unwrap() > now_date); + */ + + // TODO: should find the first cashflow that was not paid, + // based on total_repaid_interest and by reducing the cashflows_in_past + // from this, and should the ncompare the first cash flow that was not paid date + // with now - overdue_secs. + + Ok(false) + } + }, } } @@ -314,6 +349,19 @@ impl ActiveLoan { Error::::from(BorrowLoanError::MaturityDatePassed) ); + // If the loan has an interest or pay down schedule other than None, + // then we should only allow borrowing more if no interest or principal + // payments are overdue. + // + // This is required because after borrowing more, it is not possible + // to validate anymore whether previous cashflows matched the repayment + // schedule, as we don't store historic data of the principal. + // + // Therefore, in `borrow()` we set repayments_on_schedule_until to now. + // + // TODO: check total_repaid_interest >= total_expected_interest + // and total_repaid_principal >= total_expected_principal + Ok(()) } @@ -349,10 +397,7 @@ impl ActiveLoan { 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 principal = self.principal()?; (principal, inner.outstanding_interest(principal)?) } @@ -489,7 +534,7 @@ impl ActiveLoan { #[cfg(feature = "runtime-benchmarks")] pub fn set_maturity(&mut self, duration: Moment) { - self.schedule.maturity = crate::types::Maturity::fixed(duration); + self.schedule.maturity = crate::types::cashflow::Maturity::fixed(duration); } } diff --git a/pallets/loans/src/tests/mod.rs b/pallets/loans/src/tests/mod.rs index d5d84b544d..aefde1d172 100644 --- a/pallets/loans/src/tests/mod.rs +++ b/pallets/loans/src/tests/mod.rs @@ -18,12 +18,12 @@ use super::{ }, pallet::{ActiveLoans, Error, LastLoanId, PortfolioValuation}, types::{ + cashflow::{InterestPayments, Maturity, PayDownSchedule, RepaymentSchedule}, policy::{WriteOffRule, WriteOffStatus, WriteOffTrigger}, valuation::{DiscountedCashFlow, ValuationMethod}, BorrowLoanError, BorrowRestrictions, Change, CloseLoanError, CreateLoanError, - InterestPayments, InternalMutation, LoanMutation, LoanRestrictions, Maturity, - MutationError, PayDownSchedule, RepayLoanError, RepayRestrictions, RepaymentSchedule, - WrittenOffError, + InternalMutation, LoanMutation, LoanRestrictions, MutationError, RepayLoanError, + RepayRestrictions, WrittenOffError, }, }; diff --git a/pallets/loans/src/types/cashflow.rs b/pallets/loans/src/types/cashflow.rs new file mode 100644 index 0000000000..2d2ef7c710 --- /dev/null +++ b/pallets/loans/src/types/cashflow.rs @@ -0,0 +1,355 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// This file is part of Centrifuge chain project. + +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). + +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{Moment, SECONDS_PER_YEAR}; +use cfg_traits::interest::InterestRate; +use chrono::{Datelike, NaiveDate, NaiveDateTime}; +use chronoutil::DateRule; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::RuntimeDebug; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber, EnsureSub, EnsureSubAssign}, + ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, +}; + +/// Specify the expected repayments date +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum Maturity { + /// Fixed point in time, in secs + Fixed { + /// Secs when maturity ends + date: Moment, + /// Extension in secs, without special permissions + extension: Moment, + }, +} + +impl Maturity { + pub fn fixed(date: Moment) -> Self { + Self::Fixed { date, extension: 0 } + } + + pub fn date(&self) -> Moment { + match self { + Maturity::Fixed { date, .. } => *date, + } + } + + pub fn is_valid(&self, now: Moment) -> bool { + match self { + Maturity::Fixed { date, .. } => *date > now, + } + } + + pub fn extends(&mut self, value: Moment) -> Result<(), ArithmeticError> { + match self { + Maturity::Fixed { date, extension } => { + date.ensure_add_assign(value)?; + extension.ensure_sub_assign(value) + } + } + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum ReferenceDate { + /// Payments are expected every period relative to a specific date. + /// E.g. if the period is monthly and the specific date is Mar 3, the + /// first interest payment is expected on Apr 3. + Date(Moment), + + /// At the end of the period, e.g. the last day of the month for a monthly + /// period + End, +} + +/// Interest payment periods +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum InterestPayments { + /// All interest is expected to be paid at the maturity date + None, + + /// Interest is expected to be paid monthly + Monthly(ReferenceDate), + + /// Interest is expected to be paid twice per year + SemiAnnually(ReferenceDate), + + /// Interest is expected to be paid once per year + Annually(ReferenceDate), +} + +/// Specify the paydown schedules of the loan +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum PayDownSchedule { + /// The entire borrowed amount is expected to be paid back at the maturity + /// date + None, +} + +/// Specify the repayment schedule of the loan +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub struct RepaymentSchedule { + /// Expected repayments date for remaining debt + pub maturity: Maturity, + + /// Period at which interest is paid + pub interest_payments: InterestPayments, + + /// How much of the initially borrowed amount is paid back during interest + /// payments + pub pay_down_schedule: PayDownSchedule, +} + +impl RepaymentSchedule { + pub fn is_valid(&self, now: Moment) -> bool { + self.maturity.is_valid(now) + } + + // TODO: this should be exposed through a runtime API by (pool_id, loan_id) + pub fn generate_expected_cashflows( + &self, + origination_date: Moment, + principal: Balance, + interest_rate: &InterestRate, + ) -> Result, DispatchError> + where + Balance: FixedPointOperand, + Rate: FixedPointNumber, + { + match self.maturity { + Maturity::Fixed { date, .. } => { + let start = NaiveDateTime::from_timestamp_opt(origination_date as i64, 0) + .ok_or(DispatchError::Other("Invalid origination date"))? + .date(); + + let end = NaiveDateTime::from_timestamp_opt(date as i64, 0) + .ok_or(DispatchError::Other("Invalid maturity date"))? + .date(); + + match &self.interest_payments { + InterestPayments::None => Ok(vec![]), + InterestPayments::Monthly(reference_date) => Self::add_interest_amounts( + Self::get_cashflow_list::( + start, + end, + reference_date.clone(), + )?, + principal, + &self.interest_payments, + interest_rate, + origination_date, + date, + ), + InterestPayments::SemiAnnually(reference_date) => Ok(vec![]), + InterestPayments::Annually(reference_date) => Ok(vec![]), + } + } + } + } + + fn get_cashflow_list( + start: NaiveDate, + end: NaiveDate, + reference_date: ReferenceDate, + ) -> Result, DispatchError> + where + Balance: FixedPointOperand, + Rate: FixedPointNumber, + { + // TODO: once we implement a pay_down_schedule other than `None`, + // we will need to adjust the expected interest amounts based on the + // expected outstanding principal at the time of the interest payment + Ok(match reference_date { + ReferenceDate::Date(reference) => { + let reference_date = NaiveDateTime::from_timestamp_opt(reference as i64, 0) + .ok_or(DispatchError::Other("Invalid origination date"))? + .date(); + + DateRule::monthly(start) + .with_end(end) + .with_rolling_day(reference_date.day()) + .unwrap() + .into_iter() + // There's no interest payment expected on the origination date + .skip(1) + .collect() + } + ReferenceDate::End => DateRule::monthly(start) + .with_end(end) + .with_rolling_day(31) + .unwrap() + .into_iter() + .collect(), + }) + } + + fn add_interest_amounts( + cashflows: Vec, + principal: Balance, + interest_payments: &InterestPayments, + interest_rate: &InterestRate, + origination_date: Moment, + maturity_date: Moment, + ) -> Result, DispatchError> + where + Balance: FixedPointOperand, + Rate: FixedPointNumber, + { + let cashflows_len = cashflows.len(); + cashflows + .into_iter() + .enumerate() + .map(|(i, d)| -> Result<(NaiveDate, Balance), DispatchError> { + let interest_rate_per_sec = interest_rate.per_sec()?; + let amount_per_sec = interest_rate_per_sec.ensure_mul_int(principal)?; + + if i == 0 { + // First cashflow: cashflow date - origination date * interest per day + let dt = d.and_hms_opt(0, 0, 0).ok_or("")?.timestamp() as u64; + return Ok(( + d, + Rate::saturating_from_rational( + dt.ensure_sub(origination_date)?, + SECONDS_PER_YEAR, + ) + .ensure_mul_int(amount_per_sec)?, + )); + } + + if i == cashflows_len { + // Last cashflow: maturity date - cashflow date * interest per day + let dt = d.and_hms_opt(0, 0, 0).ok_or("")?.timestamp() as u64; + return Ok(( + d, + Rate::saturating_from_rational( + maturity_date.ensure_sub(dt)?, + SECONDS_PER_YEAR, + ) + .ensure_mul_int(amount_per_sec)?, + )); + } + + // Inbetween cashflows: interest per year / number of periods per year (e.g. + // divided by 12 for monthly interest payments) + let periods_per_year = match interest_payments { + InterestPayments::None => 0, + InterestPayments::Monthly(_) => 12, + InterestPayments::SemiAnnually(_) => 2, + InterestPayments::Annually(_) => 1, + }; + + let interest_rate_per_period = interest_rate + .per_year() + .ensure_div(Rate::saturating_from_integer(periods_per_year))?; + let amount_per_period = interest_rate_per_period.ensure_mul_int(principal)?; + + Ok((d, amount_per_period)) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use cfg_traits::interest::CompoundingSchedule; + use frame_support::assert_ok; + + use super::*; + + pub type Rate = sp_arithmetic::fixed_point::FixedU128; + + fn from_ymd(year: i32, month: u32, day: u32) -> Moment { + NaiveDate::from_ymd_opt(year, month, day) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .timestamp() as u64 + } + + #[test] + fn cashflow_generation_works() { + assert_ok!( + RepaymentSchedule { + maturity: Maturity::fixed(from_ymd(2022, 12, 1)), + interest_payments: InterestPayments::Monthly(ReferenceDate::End), + pay_down_schedule: PayDownSchedule::None + } + .generate_expected_cashflows( + from_ymd(2022, 6, 1), + 1000, + &InterestRate::Fixed { + rate_per_year: Rate::from_float(0.1), + compounding: CompoundingSchedule::Secondly + } + ), + vec![ + (NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(), 8u128.into()), + (NaiveDate::from_ymd_opt(2022, 7, 31).unwrap(), 8u128.into()), + (NaiveDate::from_ymd_opt(2022, 8, 31).unwrap(), 8u128.into()), + (NaiveDate::from_ymd_opt(2022, 9, 30).unwrap(), 8u128.into()), + (NaiveDate::from_ymd_opt(2022, 10, 31).unwrap(), 8u128.into()), + (NaiveDate::from_ymd_opt(2022, 11, 30).unwrap(), 8u128.into()) + ] + ); + + assert_ok!( + RepaymentSchedule { + maturity: Maturity::fixed(from_ymd(2022, 12, 2)), + interest_payments: InterestPayments::Monthly(ReferenceDate::Date(from_ymd( + 2022, 6, 2 + ))), + pay_down_schedule: PayDownSchedule::None + } + .generate_expected_cashflows( + from_ymd(2022, 6, 2), + 1000, + &InterestRate::Fixed { + rate_per_year: Rate::from_float(0.25), + compounding: CompoundingSchedule::Secondly + } + ), + vec![ + (NaiveDate::from_ymd_opt(2022, 7, 2).unwrap(), 20u128.into()), + (NaiveDate::from_ymd_opt(2022, 8, 2).unwrap(), 20u128.into()), + (NaiveDate::from_ymd_opt(2022, 9, 2).unwrap(), 20u128.into()), + (NaiveDate::from_ymd_opt(2022, 10, 2).unwrap(), 20u128.into()), + (NaiveDate::from_ymd_opt(2022, 11, 2).unwrap(), 20u128.into()) + ] + ); + + assert_ok!( + RepaymentSchedule { + maturity: Maturity::fixed(from_ymd(2023, 6, 1)), + interest_payments: InterestPayments::SemiAnnually(ReferenceDate::End), + pay_down_schedule: PayDownSchedule::None + } + .generate_expected_cashflows( + from_ymd(2022, 6, 1), + 1000, + &InterestRate::Fixed { + rate_per_year: Rate::from_float(0.1), + compounding: CompoundingSchedule::Secondly + } + ), + vec![ + (NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(), 48u128.into()), + ( + NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(), + 48u128.into() + ), + ] + ); + } +} diff --git a/pallets/loans/src/types/mod.rs b/pallets/loans/src/types/mod.rs index eb0e1a4741..6e8302df03 100644 --- a/pallets/loans/src/types/mod.rs +++ b/pallets/loans/src/types/mod.rs @@ -19,14 +19,16 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{storage::bounded_vec::BoundedVec, PalletError, RuntimeDebug}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{EnsureAdd, EnsureAddAssign, EnsureSubAssign, Get}, + traits::{EnsureAdd, Get}, ArithmeticError, }; +pub mod cashflow; pub mod policy; pub mod portfolio; pub mod valuation; +use cashflow::{InterestPayments, Maturity, PayDownSchedule}; use policy::WriteOffRule; use valuation::ValuationMethod; @@ -90,80 +92,6 @@ pub enum MutationError { MaturityExtendedTooMuch, } -/// Specify the expected repayments date -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum Maturity { - /// Fixed point in time, in secs - Fixed { - /// Secs when maturity ends - date: Moment, - /// Extension in secs, without special permissions - extension: Moment, - }, -} - -impl Maturity { - pub fn fixed(date: Moment) -> Self { - Self::Fixed { date, extension: 0 } - } - - pub fn date(&self) -> Moment { - match self { - Maturity::Fixed { date, .. } => *date, - } - } - - pub fn is_valid(&self, now: Moment) -> bool { - match self { - Maturity::Fixed { date, .. } => *date > now, - } - } - - pub fn extends(&mut self, value: Moment) -> Result<(), ArithmeticError> { - match self { - Maturity::Fixed { date, extension } => { - date.ensure_add_assign(value)?; - extension.ensure_sub_assign(value) - } - } - } -} - -/// Interest payment periods -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum InterestPayments { - /// All interest is expected to be paid at the maturity date - None, -} - -/// Specify the paydown schedules of the loan -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum PayDownSchedule { - /// The entire borrowed amount is expected to be paid back at the maturity - /// date - None, -} - -/// Specify the repayment schedule of the loan -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub struct RepaymentSchedule { - /// Expected repayments date for remaining debt - pub maturity: Maturity, - - /// Period at which interest is paid - pub interest_payments: InterestPayments, - - /// How much of the initially borrowed amount is paid back during interest - /// payments - pub pay_down_schedule: PayDownSchedule, -} - -impl RepaymentSchedule { - pub fn is_valid(&self, now: Moment) -> bool { - self.maturity.is_valid(now) - } -} - /// Specify how offer a loan can be borrowed #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum BorrowRestrictions { diff --git a/pallets/loans/src/types/policy.rs b/pallets/loans/src/types/policy.rs index 974c5a2009..4bc17a7d65 100644 --- a/pallets/loans/src/types/policy.rs +++ b/pallets/loans/src/types/policy.rs @@ -42,6 +42,9 @@ pub enum WriteOffTrigger { /// Seconds since the oracle valuation was last updated PriceOutdated(Moment), + + /// Seconds that an interest payment is overdue + InterestOverdue(Moment), } /// Wrapper type to identify equality berween kinds of triggers, @@ -58,6 +61,9 @@ impl PartialEq for UniqueWriteOffTrigger { WriteOffTrigger::PriceOutdated(_) => { matches!(other.0, WriteOffTrigger::PriceOutdated(_)) } + WriteOffTrigger::InterestOverdue(_) => { + matches!(other.0, WriteOffTrigger::InterestOverdue(_)) + } } } } diff --git a/pallets/loans/src/types/valuation.rs b/pallets/loans/src/types/valuation.rs index a6f3d258cf..80ce1c97af 100644 --- a/pallets/loans/src/types/valuation.rs +++ b/pallets/loans/src/types/valuation.rs @@ -52,6 +52,7 @@ impl DiscountedCashFlow { }) } + // TODO: this should account for cashflows from interest payments pub fn compute_present_value( &self, debt: Balance, diff --git a/runtime/integration-tests/src/utils/loans.rs b/runtime/integration-tests/src/utils/loans.rs index 8d10d65f07..ec6d7e0155 100644 --- a/runtime/integration-tests/src/utils/loans.rs +++ b/runtime/integration-tests/src/utils/loans.rs @@ -27,9 +27,9 @@ use pallet_loans::{ }, }, types::{ + cashflow::{InterestPayments, Maturity, PayDownSchedule, RepaymentSchedule}, valuation::{DiscountedCashFlow, ValuationMethod}, - BorrowRestrictions, InterestPayments, LoanRestrictions, Maturity, PayDownSchedule, - RepaidAmount, RepayRestrictions, RepaymentSchedule, + BorrowRestrictions, LoanRestrictions, RepaidAmount, RepayRestrictions, }, Call as LoansCall, };