Skip to content

Commit

Permalink
Loans: maturity extension support (#1445)
Browse files Browse the repository at this point in the history
* add maturity extension support

* fix integration-tests

* add tests for wrong mutations
  • Loading branch information
lemunozm authored Jul 11, 2023
1 parent 6fe2b53 commit 85aef36
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 50 deletions.
2 changes: 1 addition & 1 deletion pallets/loans/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ where
fn base_loan(item_id: T::ItemId) -> LoanInfo<T> {
LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed((T::Time::now() + OFFSET).as_secs()),
maturity: Maturity::fixed((T::Time::now() + OFFSET).as_secs()),
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand Down
7 changes: 6 additions & 1 deletion pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ impl<T: Config> ActiveLoan<T> {
pub fn mutate_with(&mut self, mutation: LoanMutation<T::Rate>) -> DispatchResult {
match mutation {
LoanMutation::Maturity(maturity) => self.schedule.maturity = maturity,
LoanMutation::MaturityExtension(extension) => self
.schedule
.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)?,
Expand All @@ -458,6 +463,6 @@ impl<T: Config> ActiveLoan<T> {

#[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::Maturity::fixed(duration);
}
}
2 changes: 1 addition & 1 deletion pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -986,8 +986,8 @@ pub mod pallet {
Ok((loan, count))
}

#[cfg(feature = "runtime-benchmarks")]
/// Set the maturity date of the loan to this instant.
#[cfg(feature = "runtime-benchmarks")]
pub fn expire(pool_id: T::PoolId, loan_id: T::LoanId) -> DispatchResult {
Self::update_active_loan(pool_id, loan_id, |loan| {
loan.set_maturity(T::Time::now().as_secs());
Expand Down
2 changes: 1 addition & 1 deletion pallets/loans/src/tests/create_loan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ fn with_wrong_schedule() {

let loan = LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed(now().as_secs()),
maturity: Maturity::fixed(now().as_secs()),
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand Down
132 changes: 93 additions & 39 deletions pallets/loans/src/tests/mutate_loan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,48 +91,95 @@ fn with_wrong_permissions() {
});
}

#[test]
fn with_wrong_dcf_mutation() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_internal_loan());
util::borrow_loan(loan_id, 0);
mod wrong_mutation {
use super::*;

let mutation =
LoanMutation::Internal(InternalMutation::DiscountRate(Rate::from_float(0.5)));
#[test]
fn with_dcf() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_internal_loan());
util::borrow_loan(loan_id, 0);

config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
Error::<Runtime>::MutationError(MutationError::DiscountedCashFlowExpected)
);
});
}
let mutation =
LoanMutation::Internal(InternalMutation::DiscountRate(Rate::from_float(0.5)));

#[test]
fn with_wrong_interest_rate() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_internal_loan());
util::borrow_loan(loan_id, 0);
config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
Error::<Runtime>::MutationError(MutationError::DiscountedCashFlowExpected)
);
});
}

#[test]
fn with_internal() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_external_loan());
util::borrow_loan(loan_id, 0);

let mutation =
LoanMutation::Internal(InternalMutation::DiscountRate(Rate::from_float(0.5)));

// Too high
let mutation = LoanMutation::InterestRate(Rate::from_float(3.0));
config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
Error::<Runtime>::MutationError(MutationError::InternalPricingExpected)
);
});
}

#[test]
fn with_maturity_extension() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_internal_loan());
util::borrow_loan(loan_id, 0);

let mutation = LoanMutation::MaturityExtension(YEAR.as_secs());

config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
pallet_interest_accrual::Error::<Runtime>::InvalidRate
);
});
config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
Error::<Runtime>::MutationError(MutationError::MaturityExtendedTooMuch)
);
});
}

#[test]
fn with_interest_rate() {
new_test_ext().execute_with(|| {
let loan_id = util::create_loan(util::base_internal_loan());
util::borrow_loan(loan_id, 0);

// Too high
let mutation = LoanMutation::InterestRate(Rate::from_float(3.0));

config_mocks(loan_id, &mutation);
assert_noop!(
Loans::propose_loan_mutation(
RuntimeOrigin::signed(LOAN_ADMIN),
POOL_A,
loan_id,
mutation,
),
pallet_interest_accrual::Error::<Runtime>::InvalidRate
);
});
}
}

#[test]
Expand All @@ -157,7 +204,10 @@ fn with_successful_mutation_application() {
new_test_ext().execute_with(|| {
let loan = LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed((now() + YEAR).as_secs()),
maturity: Maturity::Fixed {
date: (now() + YEAR).as_secs(),
extension: YEAR.as_secs(),
},
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand All @@ -179,7 +229,11 @@ fn with_successful_mutation_application() {
let mutations = vec![
// LoanMutation::InterestPayments(..), No changes, only one variant
// LoanMutation::PayDownSchedule(..), No changes, only one variant
LoanMutation::Maturity(Maturity::Fixed((now() + YEAR * 2).as_secs())),
LoanMutation::Maturity(Maturity::Fixed {
date: (now() + YEAR * 2).as_secs(),
extension: (YEAR * 2).as_secs(),
}),
LoanMutation::MaturityExtension(YEAR.as_secs()),
LoanMutation::InterestRate(Rate::from_float(0.5)),
LoanMutation::Internal(InternalMutation::ProbabilityOfDefault(Rate::from_float(
0.5,
Expand Down
7 changes: 5 additions & 2 deletions pallets/loans/src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ pub fn base_internal_pricing() -> InternalPricing<Runtime> {
pub fn base_internal_loan() -> LoanInfo<Runtime> {
LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed((now() + YEAR).as_secs()),
maturity: Maturity::Fixed {
date: (now() + YEAR).as_secs(),
extension: (YEAR / 2).as_secs(),
},
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand All @@ -99,7 +102,7 @@ pub fn base_external_pricing() -> ExternalPricing<Runtime> {
pub fn base_external_loan() -> LoanInfo<Runtime> {
LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed((now() + YEAR).as_secs()),
maturity: Maturity::fixed((now() + YEAR).as_secs()),
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand Down
29 changes: 25 additions & 4 deletions pallets/loans/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{storage::bounded_vec::BoundedVec, PalletError, RuntimeDebug};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{EnsureAdd, Get},
traits::{EnsureAdd, EnsureAddAssign, EnsureSubAssign, Get},
ArithmeticError,
};

Expand Down Expand Up @@ -85,25 +85,45 @@ pub enum MutationError {
DiscountedCashFlowExpected,
/// Emits when a modification expect the loan to have an iternal pricing.
InternalPricingExpected,
/// Maturity extensions exceed max extension allowed.
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(Moment),
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(moment) => *moment,
Maturity::Fixed { date, .. } => *date,
}
}

pub fn is_valid(&self, now: Moment) -> bool {
match self {
Maturity::Fixed(moment) => *moment > now,
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)
}
}
}
}
Expand Down Expand Up @@ -186,6 +206,7 @@ pub enum InternalMutation<Rate> {
#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)]
pub enum LoanMutation<Rate> {
Maturity(Maturity),
MaturityExtension(Moment),
InterestRate(Rate),
InterestPayments(InterestPayments),
PayDownSchedule(PayDownSchedule),
Expand Down
1 change: 1 addition & 0 deletions runtime/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ pub mod changes {
// <https://docs.google.com/spreadsheets/d/1RJ5RLobAdumXUK7k_ugxy2eDAwI5akvtuqUM2Tyn5ts>
LoansChangeOf::<T>::Loan(_, loan_mutation) => match loan_mutation {
LoanMutation::Maturity(_) => vec![week, blocked],
LoanMutation::MaturityExtension(_) => vec![],
LoanMutation::InterestPayments(_) => vec![week, blocked],
LoanMutation::PayDownSchedule(_) => vec![week, blocked],
LoanMutation::InterestRate(_) => vec![epoch],
Expand Down
2 changes: 1 addition & 1 deletion runtime/integration-tests/src/utils/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub fn issue_default_loan(
) -> Vec<RuntimeCall> {
let loan_info = LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed(maturity),
maturity: Maturity::fixed(maturity),
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
Expand Down

0 comments on commit 85aef36

Please sign in to comment.