use cfg_traits::{
self,
interest::{InterestAccrual, InterestRate, RateCollection},
Seconds, TimeAsSecs,
};
use cfg_types::adjustments::Adjustment;
use frame_support::{ensure, pallet_prelude::DispatchResult, RuntimeDebugNoBound};
use frame_system::pallet_prelude::BlockNumberFor;
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
BlockNumberProvider, EnsureAdd, EnsureAddAssign, EnsureFixedPointNumber, EnsureSub, Zero,
},
DispatchError,
};
use sp_std::{collections::btree_map::BTreeMap, vec::Vec};
use crate::{
entities::{
changes::LoanMutation,
input::{PrincipalInput, RepaidInput},
pricing::{
external::ExternalActivePricing, internal::InternalActivePricing, ActivePricing,
Pricing,
},
},
pallet::{AssetOf, Config, Error},
types::{
cashflow::{CashflowPayment, RepaymentSchedule},
policy::{WriteOffStatus, WriteOffTrigger},
BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions,
MutationError, RepaidAmount, RepayLoanError, RepayRestrictions,
},
PriceOf,
};
#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct LoanInfo<T: Config> {
pub schedule: RepaymentSchedule,
pub collateral: AssetOf<T>,
pub interest_rate: InterestRate<T::Rate>,
pub pricing: Pricing<T>,
pub restrictions: LoanRestrictions,
}
impl<T: Config> LoanInfo<T> {
pub fn collateral(&self) -> AssetOf<T> {
self.collateral
}
pub fn validate(&self, now: Seconds) -> DispatchResult {
match &self.pricing {
Pricing::Internal(pricing) => pricing.validate()?,
Pricing::External(pricing) => pricing.validate()?,
}
T::InterestAccrual::validate_rate(&self.interest_rate)?;
ensure!(
self.schedule.is_valid(now)?,
Error::<T>::from(CreateLoanError::InvalidRepaymentSchedule)
);
Ok(())
}
}
#[derive(Encode, Decode, Clone, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct CreatedLoan<T: Config> {
info: LoanInfo<T>,
borrower: T::AccountId,
}
impl<T: Config> CreatedLoan<T> {
pub fn new(info: LoanInfo<T>, borrower: T::AccountId) -> Self {
Self { info, borrower }
}
pub fn borrower(&self) -> &T::AccountId {
&self.borrower
}
pub fn activate(
self,
pool_id: T::PoolId,
initial_amount: PrincipalInput<T>,
) -> Result<ActiveLoan<T>, DispatchError> {
ActiveLoan::new(
pool_id,
self.info,
self.borrower,
initial_amount,
T::Time::now(),
)
}
pub fn close(self) -> Result<(ClosedLoan<T>, T::AccountId), DispatchError> {
let loan = ClosedLoan {
closed_at: frame_system::Pallet::<T>::current_block_number(),
info: self.info,
total_borrowed: Zero::zero(),
total_repaid: Default::default(),
};
Ok((loan, self.borrower))
}
}
#[derive(Encode, Decode, Clone, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct ClosedLoan<T: Config> {
closed_at: BlockNumberFor<T>,
info: LoanInfo<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
}
impl<T: Config> ClosedLoan<T> {
pub fn collateral(&self) -> AssetOf<T> {
self.info.collateral
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct ActiveLoan<T: Config> {
schedule: RepaymentSchedule,
collateral: AssetOf<T>,
restrictions: LoanRestrictions,
borrower: T::AccountId,
write_off_percentage: T::Rate,
origination_date: Seconds,
pricing: ActivePricing<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
repayments_on_schedule_until: Seconds,
}
impl<T: Config> ActiveLoan<T> {
pub fn new(
pool_id: T::PoolId,
info: LoanInfo<T>,
borrower: T::AccountId,
initial_amount: PrincipalInput<T>,
now: Seconds,
) -> Result<Self, DispatchError> {
Ok(ActiveLoan {
schedule: info.schedule,
collateral: info.collateral,
borrower,
write_off_percentage: T::Rate::zero(),
origination_date: now,
pricing: match info.pricing {
Pricing::Internal(inner) => ActivePricing::Internal(
InternalActivePricing::activate(inner, info.interest_rate)?,
),
Pricing::External(inner) => {
ActivePricing::External(ExternalActivePricing::activate(
inner,
info.interest_rate,
pool_id,
initial_amount.external()?,
info.restrictions.borrows == BorrowRestrictions::OraclePriceRequired,
)?)
}
},
restrictions: info.restrictions,
total_borrowed: T::Balance::zero(),
total_repaid: RepaidAmount::default(),
repayments_on_schedule_until: now,
})
}
pub fn borrower(&self) -> &T::AccountId {
&self.borrower
}
pub fn origination_date(&self) -> Seconds {
self.origination_date
}
pub fn maturity_date(&self) -> Option<Seconds> {
self.schedule.maturity.date()
}
pub fn pricing(&self) -> &ActivePricing<T> {
&self.pricing
}
pub fn price_id(&self) -> Option<T::PriceId> {
match &self.pricing {
ActivePricing::Internal(_) => None,
ActivePricing::External(inner) => Some(inner.price_id()),
}
}
pub fn principal(&self) -> Result<T::Balance, DispatchError> {
Ok(self
.total_borrowed
.ensure_sub(self.total_repaid.principal)?)
}
pub fn expected_cashflows(&self) -> Result<Vec<CashflowPayment<T::Balance>>, DispatchError> {
self.schedule.generate_cashflows(
self.repayments_on_schedule_until,
self.principal()?,
match &self.pricing {
ActivePricing::Internal(_) => self.principal()?,
ActivePricing::External(inner) => inner.outstanding_notional_principal()?,
},
self.pricing.interest().rate(),
)
}
pub fn write_off_status(&self) -> WriteOffStatus<T::Rate> {
WriteOffStatus {
percentage: self.write_off_percentage,
penalty: self.pricing.interest().penalty(),
}
}
fn write_down(&self, value: T::Balance) -> Result<T::Balance, DispatchError> {
Ok(value.ensure_sub(self.write_off_percentage.ensure_mul_int(value)?)?)
}
pub fn check_write_off_trigger(
&self,
trigger: &WriteOffTrigger,
pool_id: T::PoolId,
) -> Result<bool, DispatchError> {
let now = T::Time::now();
match trigger {
WriteOffTrigger::PrincipalOverdue(overdue_secs) => match self.maturity_date() {
Some(maturity) => Ok(now >= maturity.ensure_add(*overdue_secs)?),
None => Ok(false),
},
WriteOffTrigger::PriceOutdated(secs) => match &self.pricing {
ActivePricing::External(pricing) => {
Ok(now >= pricing.last_updated(pool_id).ensure_add(*secs)?)
}
ActivePricing::Internal(_) => Ok(false),
},
}
}
pub fn present_value(&self, pool_id: T::PoolId) -> Result<T::Balance, DispatchError> {
let maturity_date = self.schedule.maturity.date();
let value = match &self.pricing {
ActivePricing::Internal(inner) => {
inner.present_value(self.origination_date, maturity_date)?
}
ActivePricing::External(inner) => inner.present_value(pool_id, maturity_date)?,
};
self.write_down(value)
}
pub fn present_value_by<Rates>(
&self,
rates: &Rates,
prices: &BTreeMap<T::PriceId, PriceOf<T>>,
) -> Result<T::Balance, DispatchError>
where
Rates: RateCollection<T::Rate, T::Balance, T::Balance>,
{
let maturity_date = self.schedule.maturity.date();
let value = match &self.pricing {
ActivePricing::Internal(inner) => {
inner.present_value_cached(rates, self.origination_date, maturity_date)?
}
ActivePricing::External(inner) => inner.present_value_cached(prices, maturity_date)?,
};
self.write_down(value)
}
fn ensure_can_borrow(&self, amount: &PrincipalInput<T>, pool_id: T::PoolId) -> DispatchResult {
let max_borrow_amount = match &self.pricing {
ActivePricing::Internal(inner) => {
amount.internal()?;
inner.max_borrow_amount(self.total_borrowed)?
}
ActivePricing::External(inner) => {
let external_amount = amount.external()?;
inner.max_borrow_amount(external_amount, pool_id)?
}
};
ensure!(
amount.balance()? <= max_borrow_amount,
Error::<T>::from(BorrowLoanError::MaxAmountExceeded)
);
ensure!(
match self.restrictions.borrows {
BorrowRestrictions::NotWrittenOff => self.write_off_status().is_none(),
BorrowRestrictions::FullOnce => {
self.total_borrowed.is_zero() && amount.balance()? == max_borrow_amount
}
BorrowRestrictions::OraclePriceRequired => {
match &self.pricing {
ActivePricing::Internal(_) => true,
ActivePricing::External(inner) => inner.has_registered_price(pool_id),
}
}
},
Error::<T>::from(BorrowLoanError::Restriction)
);
let now = T::Time::now();
ensure!(
self.schedule.maturity.is_valid(now),
Error::<T>::from(BorrowLoanError::MaturityDatePassed)
);
Ok(())
}
pub fn borrow(&mut self, amount: &PrincipalInput<T>, pool_id: T::PoolId) -> DispatchResult {
self.ensure_can_borrow(amount, pool_id)?;
self.total_borrowed.ensure_add_assign(amount.balance()?)?;
match &mut self.pricing {
ActivePricing::Internal(inner) => {
inner.adjust(Adjustment::Increase(amount.balance()?))?
}
ActivePricing::External(inner) => {
inner.adjust(Adjustment::Increase(amount.external()?), Zero::zero())?
}
}
self.repayments_on_schedule_until = T::Time::now();
Ok(())
}
fn prepare_repayment(
&self,
mut amount: RepaidInput<T>,
pool_id: T::PoolId,
) -> Result<RepaidInput<T>, DispatchError> {
let (max_repay_principal, outstanding_interest) = match &self.pricing {
ActivePricing::Internal(inner) => {
let _ = amount.principal.internal()?;
let principal = self.principal()?;
(principal, inner.outstanding_interest(principal)?)
}
ActivePricing::External(inner) => {
let external_amount = amount.principal.external()?;
let max_repay_principal = inner.max_repay_principal(external_amount, pool_id)?;
(max_repay_principal, inner.outstanding_interest()?)
}
};
amount.interest = amount.interest.min(outstanding_interest);
ensure!(
amount.principal.balance()? <= max_repay_principal,
Error::<T>::from(RepayLoanError::MaxPrincipalAmountExceeded)
);
ensure!(
match self.restrictions.repayments {
RepayRestrictions::None => true,
RepayRestrictions::Full => {
amount.principal.balance()? == max_repay_principal
&& amount.interest == outstanding_interest
}
},
Error::<T>::from(RepayLoanError::Restriction)
);
Ok(amount)
}
pub fn repay(
&mut self,
amount: RepaidInput<T>,
pool_id: T::PoolId,
) -> Result<RepaidInput<T>, DispatchError> {
let amount = self.prepare_repayment(amount, pool_id)?;
self.total_repaid
.ensure_add_assign(&amount.repaid_amount()?)?;
match &mut self.pricing {
ActivePricing::Internal(inner) => {
let amount = amount.repaid_amount()?.effective()?;
inner.adjust(Adjustment::Decrease(amount))?
}
ActivePricing::External(inner) => {
let principal = amount.principal.external()?;
inner.adjust(Adjustment::Decrease(principal), amount.interest)?;
}
}
self.repayments_on_schedule_until = T::Time::now();
Ok(amount)
}
pub fn write_off(&mut self, new_status: &WriteOffStatus<T::Rate>) -> DispatchResult {
self.pricing
.interest_mut()
.set_penalty(new_status.penalty)?;
self.write_off_percentage = new_status.percentage;
Ok(())
}
fn ensure_can_close(&self) -> DispatchResult {
ensure!(
!self.pricing.interest().has_debt(),
Error::<T>::from(CloseLoanError::NotFullyRepaid)
);
Ok(())
}
pub fn close(self, pool_id: T::PoolId) -> Result<(ClosedLoan<T>, T::AccountId), DispatchError> {
self.ensure_can_close()?;
let (pricing, interest_rate) = match self.pricing {
ActivePricing::Internal(inner) => {
let (pricing, interest_rate) = inner.deactivate()?;
(Pricing::Internal(pricing), interest_rate)
}
ActivePricing::External(inner) => {
let (pricing, interest_rate) = inner.deactivate(pool_id)?;
(Pricing::External(pricing), interest_rate)
}
};
let loan = ClosedLoan {
closed_at: frame_system::Pallet::<T>::current_block_number(),
info: LoanInfo {
pricing,
collateral: self.collateral,
interest_rate,
schedule: self.schedule,
restrictions: self.restrictions,
},
total_borrowed: self.total_borrowed,
total_repaid: self.total_repaid,
};
Ok((loan, self.borrower))
}
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) => self.pricing.interest_mut().set_base_rate(rate)?,
LoanMutation::InterestPayments(payments) => self.schedule.interest_payments = payments,
LoanMutation::PayDownSchedule(schedule) => self.schedule.pay_down_schedule = schedule,
LoanMutation::Internal(mutation) => match &mut self.pricing {
ActivePricing::Internal(inner) => inner.mutate_with(mutation)?,
ActivePricing::External(_) => {
Err(Error::<T>::from(MutationError::InternalPricingExpected))?
}
},
};
Ok(())
}
#[cfg(feature = "runtime-benchmarks")]
pub fn set_maturity(&mut self, duration: Seconds) {
self.schedule.maturity = crate::types::cashflow::Maturity::fixed(duration);
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct ActiveLoanInfo<T: Config> {
pub active_loan: ActiveLoan<T>,
pub present_value: T::Balance,
pub outstanding_principal: T::Balance,
pub outstanding_interest: T::Balance,
pub current_price: Option<T::Balance>,
}
impl<T: Config> TryFrom<(T::PoolId, ActiveLoan<T>)> for ActiveLoanInfo<T> {
type Error = DispatchError;
fn try_from((pool_id, active_loan): (T::PoolId, ActiveLoan<T>)) -> Result<Self, Self::Error> {
let present_value = active_loan.present_value(pool_id)?;
Ok(match &active_loan.pricing {
ActivePricing::Internal(inner) => {
let principal = active_loan.principal()?;
Self {
present_value,
outstanding_principal: principal,
outstanding_interest: inner.outstanding_interest(principal)?,
current_price: None,
active_loan,
}
}
ActivePricing::External(inner) => {
let maturity = active_loan.maturity_date();
Self {
present_value,
outstanding_principal: inner.outstanding_priced_principal(pool_id, maturity)?,
outstanding_interest: inner.outstanding_interest()?,
current_price: Some(inner.current_price(pool_id, maturity)?),
active_loan,
}
}
})
}
}
pub mod v3 {
use cfg_traits::{interest::InterestRate, Seconds};
use parity_scale_codec::{Decode, Encode};
use crate::{
entities::{
loans::BlockNumberFor,
pricing::external::v3::{ActivePricing, Pricing},
},
types::{cashflow::RepaymentSchedule, LoanRestrictions, RepaidAmount},
AssetOf, Config,
};
#[derive(Encode, Decode)]
pub struct ActiveLoan<T: Config> {
schedule: RepaymentSchedule,
collateral: AssetOf<T>,
restrictions: LoanRestrictions,
borrower: T::AccountId,
write_off_percentage: T::Rate,
origination_date: Seconds,
pricing: ActivePricing<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
repayments_on_schedule_until: Seconds,
}
impl<T: Config> ActiveLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ActiveLoan<T> {
super::ActiveLoan {
schedule: self.schedule,
collateral: self.collateral,
restrictions: self.restrictions,
borrower: self.borrower,
write_off_percentage: self.write_off_percentage,
origination_date: self.origination_date,
pricing: self.pricing.migrate(with_linear_pricing),
total_borrowed: self.total_borrowed,
total_repaid: self.total_repaid,
repayments_on_schedule_until: self.repayments_on_schedule_until,
}
}
}
#[derive(Encode, Decode)]
pub struct CreatedLoan<T: Config> {
info: LoanInfo<T>,
borrower: T::AccountId,
}
impl<T: Config> CreatedLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::CreatedLoan<T> {
super::CreatedLoan::<T>::new(self.info.migrate(with_linear_pricing), self.borrower)
}
}
#[derive(Encode, Decode)]
pub struct ClosedLoan<T: Config> {
closed_at: BlockNumberFor<T>,
info: LoanInfo<T>,
total_borrowed: T::Balance,
total_repaid: RepaidAmount<T::Balance>,
}
impl<T: Config> ClosedLoan<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::ClosedLoan<T> {
super::ClosedLoan::<T> {
closed_at: self.closed_at,
info: self.info.migrate(with_linear_pricing),
total_borrowed: self.total_borrowed,
total_repaid: self.total_repaid,
}
}
}
#[derive(Encode, Decode)]
pub struct LoanInfo<T: Config> {
pub schedule: RepaymentSchedule,
pub collateral: AssetOf<T>,
pub interest_rate: InterestRate<T::Rate>,
pub pricing: Pricing<T>,
pub restrictions: LoanRestrictions,
}
impl<T: Config> LoanInfo<T> {
pub fn migrate(self, with_linear_pricing: bool) -> super::LoanInfo<T> {
super::LoanInfo::<T> {
pricing: self.pricing.migrate(with_linear_pricing),
schedule: self.schedule,
collateral: self.collateral,
interest_rate: self.interest_rate,
restrictions: self.restrictions,
}
}
}
}