Skip to content

Commit

Permalink
Loans: principal and interest separation (#1435)
Browse files Browse the repository at this point in the history
* add fields

* loan repayment schema

* external pricing with interest

* add type for active interest rates

* pricing based on interest rate module

* minor changes

* update diagram with max borrow amount

* update diagram with PR changes

* minor method renamed

* rename normalized_debt field

* rename interest_rate to interest

* legacy tests passing

* add new tests for external interest

* support interest rate mutation for external pricing

* fix benchmarks

* rename unchecked to unscheduled amount

* update diagram with RepaidAmount

* fix integration tests

* minor diagram layout change

* fix runtime common issue

* fix integration tests

* add missing docs

* remove Once part from RepayRestriction::FullOnce
  • Loading branch information
lemunozm committed Jul 10, 2023
1 parent 33f3aea commit 6fe2b53
Show file tree
Hide file tree
Showing 20 changed files with 950 additions and 308 deletions.
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
```
17 changes: 11 additions & 6 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 @@ -142,9 +142,9 @@ where
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

0 comments on commit 6fe2b53

Please sign in to comment.