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: maturity extension support #1445

Merged
merged 31 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e1ebe46
add fields
lemunozm Jun 29, 2023
b3811cf
loan repayment schema
lemunozm Jun 29, 2023
4bb2f5f
external pricing with interest
lemunozm Jul 3, 2023
3a87f75
add type for active interest rates
lemunozm Jul 3, 2023
a9ed030
pricing based on interest rate module
lemunozm Jul 3, 2023
4c291ce
minor changes
lemunozm Jul 3, 2023
d06e00d
update diagram with max borrow amount
lemunozm Jul 3, 2023
e141eb5
update diagram with PR changes
lemunozm Jul 3, 2023
fd8d54f
minor method renamed
lemunozm Jul 3, 2023
2e157b7
rename normalized_debt field
lemunozm Jul 3, 2023
211b496
Merge remote-tracking branch 'origin/main' into loans/principal-and-i…
lemunozm Jul 3, 2023
949449d
rename interest_rate to interest
lemunozm Jul 3, 2023
ecec980
legacy tests passing
lemunozm Jul 4, 2023
039c27c
add new tests for external interest
lemunozm Jul 4, 2023
7f0ced0
support interest rate mutation for external pricing
lemunozm Jul 4, 2023
751e44a
fix benchmarks
lemunozm Jul 4, 2023
8414469
Merge remote-tracking branch 'origin/main' into loans/principal-and-i…
lemunozm Jul 4, 2023
28c06b3
rename unchecked to unscheduled amount
lemunozm Jul 4, 2023
c7f48c6
update diagram with RepaidAmount
lemunozm Jul 4, 2023
7d3ed49
fix integration tests
lemunozm Jul 4, 2023
69a7e5d
minor diagram layout change
lemunozm Jul 4, 2023
1790e88
fix runtime common issue
lemunozm Jul 4, 2023
6a51a73
fix integration tests
lemunozm Jul 5, 2023
9a8c3bf
Merge remote-tracking branch 'origin/main' into loans/principal-and-i…
lemunozm Jul 5, 2023
f73f81c
add missing docs
lemunozm Jul 5, 2023
c118db5
remove Once part from RepayRestriction::FullOnce
lemunozm Jul 5, 2023
9707d06
Merge remote-tracking branch 'origin/main' into loans/principal-and-i…
lemunozm Jul 6, 2023
65987dd
add maturity extension support
lemunozm Jul 6, 2023
2a2961c
fix integration-tests
lemunozm Jul 6, 2023
2f26a68
add tests for wrong mutations
lemunozm Jul 7, 2023
c13ca10
Merge remote-tracking branch 'origin/main' into loans/maturity-extension
lemunozm Jul 10, 2023
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
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,
},
}
Comment on lines +96 to 102
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the migration. But it needs migration "theoretically". Right now we can purge all pallet-loans storage. It only requires a migration if it is used before we can deploy these changes. The same applies to all pending PRs

Copy link
Contributor Author

@lemunozm lemunozm Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, I read you wrong 😆, I pointed to the code that causes the migration. No, there is no migration done, but should not be done by now


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)
lemunozm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
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![],
lemunozm marked this conversation as resolved.
Show resolved Hide resolved
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
Loading