owned this note
owned this note
Published
Linked with GitHub
# `pallet-loans` refactor specs
### Overview main of changes from the previous `pallet-loans` version
- `create()` and `price()` a loan is now the same action: `create()`.
- `reprice()` no longer exists. Instead we add a new method `modify()` that allows to modify some properties of an active loan. **Not still implemented**.
- Loan NFT no longer exists (and consecuently, `initialize_pool()` is no longer needed neither).
- `WrittenOff` policy changed.
- When borrow/repay is allowed is determined by `BorrowRestrictions` and `RepayRestrictions`.
### TODO
- [ ] Specify `modify`/`modify_intent`.
- [ ] ActiveLoanMutations storage: [thread](https://kflabs.slack.com/archives/C04HJN404RK/p1675093607773169), [issue](https://github.com/centrifuge/centrifuge-chain/issues/1193)
- [ ] Portfolio valuation in another pallet: [issue](https://github.com/centrifuge/centrifuge-chain/issues/908)
- [ ] Flexible interest rates: [issue](https://github.com/centrifuge/centrifuge-chain/issues/1097)
#### Open questions
- How to lock collateral NFTs?
- Check whether NFTs v2 fixes this
- What should be validated when creating loans?
## Loan states
```plantuml
@startuml
state Created : -CreatedLoans storage
state Active : -ActiveLoans storage \n-Each action here requires update Nav
state Closed: -ClosedLoans storage
[*] --> Created: create()
Created --> Active : borrow()
Created --> Closed : close()
Active -left-> Closed : close()
Active --> Active : borrow() / repay() / write_*() / modify_*()
```
#### State description
- `Created`: The loan has been created and is ready to be first-time borrowed.
- `Active`: The loan has been borrowed and each action over it modifies the portfolio valuation associated to it. An active loan can be always borrowed/repaid/written-*/modified if the required *restrictions* allow that.
- `Closed`: The loan is closed and no more actions can be performed over it. Any Created loan can be closed and any `Active` loan can be closed too if they are fully repaid
These states, can be determined from the storage used.
`Active` loans will be stored in a `BoundedVec` to be fastly iterable when computing the portfolio valuation of that pool.
## Storage
Previous:
```plantuml
@startuml
hide methods
class LoanDetails {
collateral: Asset
status: LoanStatus
}
enum LoanStatus {
Created,
Active,
Closed,
}
LoanDetails *--> LoanStatus
class PricedLoanDetails {
loan_id: LoanId,
loan_type: LoanType,
interest_rate_per_sec: Rate,
origination_date: Option<Moment>,
normalized_debt: NormalizedBebt,
total_borrowed: Balance,
total_repaid: Balance,
write_off_status: WriteOffStatus<Rate>,
last_updated: Moment,
}
enum LoanType {
BulletLoan,
CreditLine,
CreditLineWithMaturity,
}
class BulletLoan {
advance_rate: Rate
probabilisty_of_default: Rate
loss_given_default: Rate
value: Balance
discount_rate: Rate
madurity_date: Rate
}
class CreditLine {
advance_rate: Rate
value: Balance
}
class CreditLineWithMadurity {
advance_rate: Rate
probability_of_default: Rate
loss_given_default: Rate
value: Balance
discount_rate: Rate
madurity_rate: Rate
}
enum WriteOffStatus {
None
WrittenOff::write_off_index: u32
WrittenOffByAdmin::percentage: Rate
WrittenOffByAdmin::penalty_interest_rate_per_sec: Rate
}
PricedLoanDetails *--> LoanType
PricedLoanDetails *--> WriteOffStatus
LoanType *--> BulletLoan
LoanType *--> CreditLine
LoanType *--> CreditLineWithMadurity
class WriteOffGroup {
percentage: Rate
overdue_days: u64
penalty_interest_rate_per_sec: Rate
}
class NavDetails {
latest: Balance
last_update: Moment
}
class Storage <<(P, orange)>> {
Loan: Map<PoolId, LoanId, LoanDetails>
ActiveLoans: Map<PoolId, Vec<PricedLoanDetails>>
ClosedLoan: Map<PoolId, LoanId, PricedLoanDetails>
NextLoanId: Map<PoolId, u128>
PoolNAV: Map<PoolId, NAVDetails>
PoolWriteOffGroups: Map<PoolId, Vec<WriteOffGroup>>
}
Storage *--> "n" LoanDetails
Storage *--> "n" PricedLoanDetails
Storage *--> "n" WriteOffGroup
Storage *--> "n" NavDetails
@enduml
```
---
New:
```plantuml
@startuml
hide methods
enum Maturity {
Fixed: Moment
}
enum InterestPayments {
None
}
enum PayDownSchedule {
None
}
class RepaymentSchedule {
maturity: Maturity
interest_payments: InterestPayments
pay_down_schedule: PayDownSchedule
}
RepaymentSchedule *--> Maturity
RepaymentSchedule *--> InterestPayments
RepaymentSchedule *--> PayDownSchedule
enum MaxBorrowAmount {
UpToTotalBorrows::advance_rate: Rate
UpToOutstandingDebt::advance_rate: Rate
}
enum BorrowRestrictions {
None
}
enum RepayRestrictions {
None
}
class LoanRestrictions {
max_borrow_amount: MaxBorrowAmount
borrows: BorrowRestrictions
repayments: RepayRestrictions
}
LoanRestrictions *---> BorrowRestrictions
LoanRestrictions *--> RepayRestrictions
LoanRestrictions *---> MaxBorrowAmount
class DiscountedCashFlows {
probability_of_default: Rate
loss_given_default: Rate
discount_rate: Rate
}
ValuationMethod *--> DiscountedCashFlows
enum ValuationMethod {
DiscountedCashFlows: DiscounredCashFlows
OutstandingDebt
}
class LoanInfo {
schedule: RepaymentSchedule
collateral: Asset
collateral_value: Balance
valuations_method: ValuationMethod
restrictions: LoanRestrictions
interest_rate_per_sec: Rate
}
LoanInfo *--> RepaymentSchedule
LoanInfo *--> ValuationMethod
LoanInfo *--> LoanRestrictions
class WriteOffStatus {
percentage: Rate
penalty: Rate
}
class CreatedLoan {
info: LoanInfo
borrower: AccountId
}
class ActiveLoan {
loan_id: LoanId
borrower: AccountId
info: LoanInfo
write_off_status: WriteOffStatus
origination_date: Moment
normalized_debt: Balance
total_borrowed: Balance
total_repaid: Balance
}
class ClosedLoan {
closed_at: BlockNumber
info: LoanInfo
total_borrowed: Balance
total_repaid: Balance
}
CreatedLoan *---> LoanInfo
ActiveLoan *---> LoanInfo
ActiveLoan *--> WriteOffStatus
ClosedLoan *---> LoanInfo
class WriteOffState {
overdue_days: u32
percentage: Rate
penalty: Rate
}
class PortfolioValuation {
value: Balance
last_updated: Moment
}
class Storage <<(P, orange)>> {
CreatedLoan: Map<PoolId, LoanId, CreatedLoan>
ActiveLoans: Map<PoolId, Vec<Tuple<ActiveLoan, Moment>>>
ClosedLoan: Map<PoolId, LoanId, ClosedLoan>
LastLoanId: Map<PoolId, LoanId>
WriteOffPolicy: Map<PoolId, Vec<WriteOffState>>
PortfolioValuation: Map<PoolId, PortfolioValuation>
}
Storage *--> "n" CreatedLoan
Storage *--> "n" ActiveLoan
Storage *--> "n" ClosedLoan
Storage *-left-> "n" WriteOffState
Storage *-right-> "n" PortfolioValuation
@enduml
```
`CreatedLoan`, `ActiveLoan`, and `ClosedLoan`, are wrappers over `LoanInfo` adding extra information needed in each stage.
## Loan actions
Actions related to one loan.
### `create()`:
Creates a loan. A loan contains both, the proposed borrowed information and the pricing information.
- Pre:
- The origin should be the owner of the collateral.
- Check permisions for `Borrower`.
- The collateral must not be already used.
- The pool should exists.
- The loan info content should be valid.
- Post:
- `CreatedLoans` storage will contain the loan information.
- Transfer collateral to pool.
- The loan state is **`Created`**.
### `borrow(amount)`:
- Pre:
- The loan must be **`Created`** or **`Active`**.
- The origin should be borrower who has created the loan.
- It should ensure `BorrowRestrictions`.
- `maturity_date` is > `now()`.
- `amount` <= `max_borrow_amount()`.
- Post:
- If first time:
- `origination_date` is set to `now()`.
- `CreatedLoans` storage will no longer contain the loan information.
- `ActiveLoans` storage will contain the loan information.
- `total_borrowed` is increased with `amount`.
- `normalized_debt` updated.
- Portfolio valuation approximately updated.
- Withdraw amount from pool.
- The loan state is **`Active`**.
### `repay(amount)`:
- Pre:
- The loan must be **`Active`**.
- The origin should be borrower who has created the loan.
- It should ensure `RepayRestrictions`.
- amount must be clamp to the `current_debt`
- Post:
- `total_repayed` is increased with `min(amount, current_debt)`.
- `normalized_debt` updated.
- Portfolio valuation approximately updated.
- Deposit amount to pool.
### `modify()` (and `modify_intent()` version):
- Pre: **TODO: WIP**
- Post: **TODO: WIP**
### `write_off()`:
- Pre:
- The loan state is **`Active`**.
- `maturity_date` is > `now()`.
- Post:
- Portfolio valuation approximately updated.
- Write off status updated.
### `write_off_admin()`:
- Pre:
- The loan state is **`Active`**.
- The `Role` must be `LoanAdmin`.
- `maturity_date` is > `now()`.
- `percentage` and `penalty` must be higher than the current policy
- Post:
- Portfolio valuation approximately updated.
- Write off status updated.
### `close()`:
- Pre:
- The loan state is **`Created`** or **`Active`**.
- The origin should be borrower who has created the loan.
- The loan must be fully repaid.
- Post:
- `CreatedLoans` storage will no longer contain the loan information.
- `ActiveLoans` storage will no longer contain the loan information.
- `ClosedLoans` storage will contain the loan information.
- Transfer collateral to owner.
- The loan state is **`Closed`**.
## Pool actions
Actions that are related to all loans in the same pool
### `update_write_off_policy()`:
- Pre:
- `Role` should be p PoolAdmin`.
- Pool should exists
- Post:
- Insert the policy in the pool.
### `update_portfolio_valuation()`:
- Pre:
- Pool should exists
- Post:
- Portfolio valuation exaclty updated.
# Appendix
## Write off requirements
- The policy set the minimum percentage/penalty values.
- An admin can write down/up whenever they want if there is no policy applied.
- write_off() only writes down.
## Loan modifications
- If the loan is created but not active. A modification is just closing the loan and open it again with the changed properties.
- If the loan is active. Each actions should go thorugh `PoolChangeGuard`. This is achieve in two stages:
- The caller calls extrinsic `modify_intention()` that calls `PoolChangeGuard::note()` and would generate an event with a `ChangeId` of that change intention.
- Once the conditions are satisfied, the caller should call `modify(change_id)` that calls `PoolChangeGuard::released()` in order to check if the change is ready. If it is, it makes that change effective.