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 29 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions libs/types/src/adjustments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

use sp_runtime::{
traits::{EnsureAdd, EnsureSub},
ArithmeticError,
};

#[derive(Clone, Copy)]
pub enum Adjustment<Amount> {
Increase(Amount),
Expand All @@ -23,4 +28,33 @@ impl<Amount> Adjustment<Amount> {
Adjustment::Decrease(amount) => amount,
}
}

pub fn map<F>(self, f: F) -> Adjustment<Amount>
where
F: FnOnce(Amount) -> Amount,
{
match self {
Adjustment::Increase(amount) => Adjustment::Increase(f(amount)),
Adjustment::Decrease(amount) => Adjustment::Decrease(f(amount)),
}
}

pub fn try_map<F, E>(self, f: F) -> Result<Adjustment<Amount>, E>
where
F: FnOnce(Amount) -> Result<Amount, E>,
{
match self {
Adjustment::Increase(amount) => f(amount).map(Adjustment::Increase),
Adjustment::Decrease(amount) => f(amount).map(Adjustment::Decrease),
}
}
}

impl<Amount: EnsureAdd + EnsureSub> Adjustment<Amount> {
pub fn ensure_add(self, amount: Amount) -> Result<Amount, ArithmeticError> {
match self {
Adjustment::Increase(inc) => amount.ensure_add(inc),
Adjustment::Decrease(dec) => amount.ensure_sub(dec),
}
}
}
263 changes: 263 additions & 0 deletions pallets/loans/docs/types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
```plantuml
@startuml

set namespaceSeparator ::
hide methods

enum Maturity {
Fixed: Moment
}

enum CalendarEvent {
End
}

enum ReferenceDate{
CalendarDate: CalendarEvent,
OriginationDate
}

ReferenceDate *--> CalendarEvent

enum InterestPayments {
None
Monthly: ReferenceDate
SemiAnnually: ReferenceDate
}

InterestPayments *--> ReferenceDate

enum PayDownSchedule {
None
}

class RepaymentSchedule {
maturity: Maturity
interest_payments: InterestPayments
pay_down_schedule: PayDownSchedule
}

RepaymentSchedule *--> Maturity
RepaymentSchedule *--> PayDownSchedule
RepaymentSchedule *-----> InterestPayments

enum BorrowRestrictions {
NoWrittenOff
FullOnce
}

enum RepayRestrictions {
None
Full
}

class LoanRestrictions {
borrows: BorrowRestrictions
repayments: RepayRestrictions
}

LoanRestrictions *--> BorrowRestrictions
LoanRestrictions *--> RepayRestrictions

enum CompoundingCadence {
Secondly: ReferenceDate
}

CompoundingCadence *--> ReferenceDate

enum InterestRate {
Fixed: Rate, CompoundingCadence
}

InterestRate *--> CompoundingCadence

class RepaidAmount {
principal: Balance
interest: Balance
unscheduled: Balance
}

package portfolio {
class PortfolioValuation {
value: Balance
last_updated: Moment
values: Vec<Tuple<LoanId, Balance>>
}
}

package valuation {
class DiscountedCashFlows {
probability_of_default: Rate
loss_given_default: Rate
discount_rate: Rate
}

ValuationMethod *--> DiscountedCashFlows

enum ValuationMethod {
DiscountedCashFlows: DiscountedCashFlows
OutstandingDebt
}
}

package policy {
class WriteOffStatus {
percentage: Rate
penalty: Rate
}

enum WriteOffTrigger {
PrincipalOverdueDays,
PriceOutdated,
}

class WriteOffRule {
triggers: Vec<WriteOffTrigger>
status: WriteOffStatus
}

WriteOffRule *--> WriteOffTrigger
WriteOffRule *--> WriteOffStatus
}

package interest {
class ActiveInterestRate {
rate: InterestRate,
normalized_debt: Balance,
penalty: Rate
}

ActiveInterestRate *--> InterestRate
}

package pricing {
package internal {
enum MaxBorrowAmount {
UpToTotalBorrows::advance_rate: Rate
UpToOutstandingDebt::advance_rate: Rate
}

class InternalPricing {
collateral_value: Balance
valuation_method: ValuationMethod
max_borrow_amount: MaxBorrowAmount
}

InternalPricing *-l-> MaxBorrowAmount
InternalPricing *-d-> valuation::ValuationMethod

class InternalActivePricing {
info: InternalPricing
interest: ActiveInterestRate
}

InternalActivePricing *-r-> ActiveInterestRate
InternalActivePricing *--> InternalPricing
}

package external {
enum MaxBorrowAmount {
Quantity: Rate
NoLimit
}

class ExternalPricing {
price_id: Price,
max_borrow_quantity: Balance,
notional: Rate,
}

ExternalPricing *-l-> MaxBorrowAmount

class ExternalActivePricing {
info: ExternalPricing
outstanding_quantity: Balance,
interest: ActiveInterestRate
}

ExternalActivePricing *-r-> ActiveInterestRate
ExternalActivePricing *--> ExternalPricing
}

enum Pricing {
Internal: InternalPricing
External: ExternalPricing
}

enum ActivePricing {
Internal: InternalActivePricing
External: ExternalActivePricing
}

Pricing *--> InternalPricing
Pricing *--> ExternalPricing

ActivePricing *----> InternalActivePricing
ActivePricing *--> ExternalActivePricing
}

package loan {
class LoanInfo {
schedule: RepaymentSchedule
collateral: Asset
restrictions: LoanRestrictions
pricing: Pricing
}

class CreatedLoan {
info: LoanInfo
borrower: AccountId
}

class ActiveLoan {
loan_id: LoanId
borrower: AccountId
schedule: RepaymentSchedule
collateral: Asset
restrictions: LoanRestrictions
pricing: ActivePricing
write_off_percentage: Rate
origination_date: Moment
total_borrowed: Balance
total_repaid: RepaidAmount
}

class ClosedLoan {
closed_at: BlockNumber
info: LoanInfo
total_borrowed: Balance
total_repaid: Balance
}

LoanInfo *--> RepaymentSchedule
LoanInfo *-r-> LoanRestrictions
LoanInfo *--> pricing::Pricing
LoanInfo *--> ActiveInterestRate

CreatedLoan *--> LoanInfo

ActiveLoan *--> pricing::ActivePricing
ActiveLoan *-d--> RepaymentSchedule
ActiveLoan *-r-> LoanRestrictions
ActiveLoan *-r-> RepaidAmount

ClosedLoan *--> LoanInfo
}

class Storage <<(P, orange)>> {
CreatedLoan: Map<PoolId, LoanId, CreatedLoan>
ActiveLoans: Map<PoolId, Vec<Tuple<LoanId, ActiveLoan>>>
ClosedLoan: Map<PoolId, LoanId, ClosedLoan>
LastLoanId: Map<PoolId, LoanId>
WriteOffPolicy: Map<PoolId, Vec<WriteOffRule>>
PortfolioValuation: Map<PoolId, PortfolioValuation>
}

Storage *--> "n" CreatedLoan
Storage *--> "n" ActiveLoan
Storage *--> "n" ClosedLoan
Storage *-u-> "n" WriteOffRule
Storage *-u-> "n" PortfolioValuation

@enduml
```
19 changes: 12 additions & 7 deletions pallets/loans/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use frame_support::{
use frame_system::RawOrigin;
use orml_traits::DataFeeder;
use sp_arithmetic::FixedPointNumber;
use sp_runtime::traits::{Get, One, Zero};
use sp_runtime::traits::{Bounded, Get, One, Zero};
use sp_std::{time::Duration, vec};

use crate::{
Expand All @@ -48,7 +48,7 @@ use crate::{
policy::{WriteOffRule, WriteOffTrigger},
valuation::{DiscountedCashFlow, ValuationMethod},
BorrowRestrictions, InterestPayments, LoanMutation, LoanRestrictions, Maturity,
PayDownSchedule, RepayRestrictions, RepaymentSchedule,
PayDownSchedule, RepaidAmount, RepayRestrictions, RepaymentSchedule,
},
};

Expand Down Expand Up @@ -137,14 +137,14 @@ 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,
},
collateral: (COLLECION_ID.into(), item_id),
interest_rate: T::Rate::saturating_from_rational(1, 5000),
pricing: Pricing::Internal(InternalPricing {
collateral_value: COLLATERAL_VALUE.into(),
interest_rate: T::Rate::saturating_from_rational(1, 5000),
max_borrow_amount: MaxBorrowAmount::UpToOutstandingDebt {
advance_rate: T::Rate::one(),
},
Expand Down Expand Up @@ -193,8 +193,11 @@ where
RawOrigin::Signed(borrower).into(),
pool_id,
loan_id,
COLLATERAL_VALUE.into(),
0.into(),
RepaidAmount {
principal: 10.into(),
interest: T::Balance::max_value(),
unscheduled: 0.into(),
},
)
.unwrap();
}
Expand Down Expand Up @@ -338,7 +341,9 @@ benchmarks! {
let loan_id = Helper::<T>::create_loan(pool_id, u16::MAX.into());
Helper::<T>::borrow_loan(pool_id, loan_id);

}: _(RawOrigin::Signed(borrower), pool_id, loan_id, 10.into(), 0.into())
let repaid = RepaidAmount { principal: 10.into(), interest: 0.into(), unscheduled: 0.into() };

}: _(RawOrigin::Signed(borrower), pool_id, loan_id, repaid)

write_off {
let n in 1..Helper::<T>::max_active_loans() - 1;
Expand Down