#![cfg_attr(not(feature = "std"), no_std)]
pub mod entities {
pub mod changes;
pub mod input;
pub mod interest;
pub mod loans;
pub mod pricing;
}
pub mod types;
pub mod util;
mod weights;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub use pallet::*;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use cfg_traits::{
self,
changes::ChangeGuard,
data::{DataCollection, DataRegistry},
interest::InterestAccrual,
IntoSeconds, Permissions, PoolInspect, PoolNAV, PoolReserve, PoolWriteOffPolicyMutate,
Seconds, TimeAsSecs,
};
use cfg_types::{
adjustments::Adjustment,
permissions::{PermissionScope, PoolRole, Role},
portfolio::{self, InitialPortfolioValuation, PortfolioValuationUpdateType},
};
use entities::{
changes::{Change, LoanMutation},
input::{PriceCollectionInput, PrincipalInput, RepaidInput},
loans::{self, ActiveLoan, ActiveLoanInfo, LoanInfo},
};
use frame_support::{
pallet_prelude::*,
storage::transactional,
traits::tokens::{
self,
nonfungibles::{Inspect, Transfer},
},
};
use frame_system::pallet_prelude::*;
use parity_scale_codec::HasCompact;
use scale_info::TypeInfo;
use sp_arithmetic::{FixedPointNumber, PerThing};
use sp_runtime::{
traits::{BadOrigin, EnsureAdd, EnsureAddAssign, EnsureInto, One, Zero},
ArithmeticError, FixedPointOperand, TransactionOutcome,
};
use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec};
use types::{
self,
cashflow::CashflowPayment,
policy::{self, WriteOffRule, WriteOffStatus},
BorrowLoanError, CloseLoanError, CreateLoanError, MutationError, RepayLoanError,
WrittenOffError,
};
use super::*;
pub type PortfolioInfoOf<T> = Vec<(<T as Config>::LoanId, ActiveLoanInfo<T>)>;
pub type AssetOf<T> = (<T as Config>::CollectionId, <T as Config>::ItemId);
pub type PriceOf<T> = (<T as Config>::Balance, <T as Config>::Moment);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type RuntimeChange: From<Change<Self>> + TryInto<Change<Self>>;
type CurrencyId: Parameter + Copy + MaxEncodedLen;
type CollectionId: Parameter + Member + Default + TypeInfo + Copy + MaxEncodedLen;
type ItemId: Parameter + Member + Default + TypeInfo + Copy + MaxEncodedLen;
type LoanId: Parameter
+ Member
+ Default
+ TypeInfo
+ MaxEncodedLen
+ Copy
+ EnsureAdd
+ One;
type PriceId: Parameter + Member + TypeInfo + Copy + MaxEncodedLen + Ord;
type Rate: Parameter + Member + FixedPointNumber + TypeInfo + MaxEncodedLen;
type Balance: tokens::Balance + FixedPointOperand;
type Quantity: Parameter + Member + FixedPointNumber + TypeInfo + MaxEncodedLen;
type PerThing: Parameter + Member + PerThing + TypeInfo + MaxEncodedLen;
type Time: TimeAsSecs;
type Moment: Parameter + Member + Copy + IntoSeconds;
type NonFungible: Transfer<Self::AccountId>
+ Inspect<Self::AccountId, CollectionId = Self::CollectionId, ItemId = Self::ItemId>;
type PoolId: Member + Parameter + Default + Copy + HasCompact + MaxEncodedLen;
type Pool: PoolReserve<
Self::AccountId,
Self::CurrencyId,
Balance = Self::Balance,
PoolId = Self::PoolId,
>;
type Permissions: Permissions<
Self::AccountId,
Scope = PermissionScope<Self::PoolId, Self::CurrencyId>,
Role = Role,
Error = DispatchError,
>;
type PriceRegistry: DataRegistry<Self::PriceId, Self::PoolId, Data = PriceOf<Self>>;
type InterestAccrual: InterestAccrual<
Self::Rate,
Self::Balance,
Adjustment<Self::Balance>,
NormalizedDebt = Self::Balance,
>;
type ChangeGuard: ChangeGuard<
PoolId = Self::PoolId,
ChangeId = Self::Hash,
Change = Self::RuntimeChange,
>;
#[pallet::constant]
type MaxActiveLoansPerPool: Get<u32>;
#[pallet::constant]
type MaxWriteOffPolicySize: Get<u32> + Parameter;
type WeightInfo: WeightInfo;
}
#[pallet::storage]
pub(crate) type LastLoanId<T: Config> =
StorageMap<_, Blake2_128Concat, T::PoolId, T::LoanId, ValueQuery>;
#[pallet::storage]
pub type CreatedLoan<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::PoolId,
Blake2_128Concat,
T::LoanId,
loans::CreatedLoan<T>,
OptionQuery,
>;
#[pallet::storage]
pub type ActiveLoans<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::PoolId,
BoundedVec<(T::LoanId, ActiveLoan<T>), T::MaxActiveLoansPerPool>,
ValueQuery,
>;
#[pallet::storage]
pub type ClosedLoan<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::PoolId,
Blake2_128Concat,
T::LoanId,
loans::ClosedLoan<T>,
OptionQuery,
>;
#[pallet::storage]
pub(crate) type WriteOffPolicy<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::PoolId,
BoundedVec<WriteOffRule<T::Rate>, T::MaxWriteOffPolicySize>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn portfolio_valuation)]
pub(crate) type PortfolioValuation<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::PoolId,
portfolio::PortfolioValuation<T::Balance, T::LoanId, T::MaxActiveLoansPerPool>,
ValueQuery,
InitialPortfolioValuation<T::Time>,
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Created {
pool_id: T::PoolId,
loan_id: T::LoanId,
loan_info: LoanInfo<T>,
},
Borrowed {
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: PrincipalInput<T>,
},
Repaid {
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: RepaidInput<T>,
},
WrittenOff {
pool_id: T::PoolId,
loan_id: T::LoanId,
status: WriteOffStatus<T::Rate>,
},
Mutated {
pool_id: T::PoolId,
loan_id: T::LoanId,
mutation: LoanMutation<T::Rate>,
},
Closed {
pool_id: T::PoolId,
loan_id: T::LoanId,
collateral: AssetOf<T>,
},
PortfolioValuationUpdated {
pool_id: T::PoolId,
valuation: T::Balance,
update_type: PortfolioValuationUpdateType,
},
WriteOffPolicyUpdated {
pool_id: T::PoolId,
policy: BoundedVec<WriteOffRule<T::Rate>, T::MaxWriteOffPolicySize>,
},
DebtTransferred {
pool_id: T::PoolId,
from_loan_id: T::LoanId,
to_loan_id: T::LoanId,
repaid_amount: RepaidInput<T>,
borrow_amount: PrincipalInput<T>,
},
DebtIncreased {
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: PrincipalInput<T>,
},
DebtDecreased {
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: RepaidInput<T>,
},
}
#[pallet::error]
pub enum Error<T> {
PoolNotFound,
LoanNotActiveOrNotFound,
NoValidWriteOffRule,
NFTOwnerNotFound,
NotNFTOwner,
NotLoanBorrower,
MaxActiveLoansReached,
NoLoanChangeId,
UnrelatedChangeId,
MismatchedPricingMethod,
SettlementPriceExceedsVariation,
CreateLoanError(CreateLoanError),
BorrowLoanError(BorrowLoanError),
RepayLoanError(RepayLoanError),
WrittenOffError(WrittenOffError),
CloseLoanError(CloseLoanError),
MutationError(MutationError),
TransferDebtToSameLoan,
TransferDebtAmountMismatched,
MaturityDateNeededForValuationMethod,
}
impl<T> From<CreateLoanError> for Error<T> {
fn from(error: CreateLoanError) -> Self {
Error::<T>::CreateLoanError(error)
}
}
impl<T> From<BorrowLoanError> for Error<T> {
fn from(error: BorrowLoanError) -> Self {
Error::<T>::BorrowLoanError(error)
}
}
impl<T> From<RepayLoanError> for Error<T> {
fn from(error: RepayLoanError) -> Self {
Error::<T>::RepayLoanError(error)
}
}
impl<T> From<WrittenOffError> for Error<T> {
fn from(error: WrittenOffError) -> Self {
Error::<T>::WrittenOffError(error)
}
}
impl<T> From<CloseLoanError> for Error<T> {
fn from(error: CloseLoanError) -> Self {
Error::<T>::CloseLoanError(error)
}
}
impl<T> From<MutationError> for Error<T> {
fn from(error: MutationError) -> Self {
Error::<T>::MutationError(error)
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::WeightInfo::create())]
#[pallet::call_index(0)]
pub fn create(
origin: OriginFor<T>,
pool_id: T::PoolId,
info: LoanInfo<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::ensure_role(pool_id, &who, PoolRole::Borrower)?;
Self::ensure_collateral_owner(&who, info.collateral())?;
Self::ensure_pool_exists(pool_id)?;
info.validate(T::Time::now())?;
let collateral = info.collateral();
T::NonFungible::transfer(&collateral.0, &collateral.1, &T::Pool::account_for(pool_id))?;
let loan_id = Self::generate_loan_id(pool_id)?;
CreatedLoan::<T>::insert(pool_id, loan_id, loans::CreatedLoan::new(info.clone(), who));
Self::deposit_event(Event::<T>::Created {
pool_id,
loan_id,
loan_info: info,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::borrow(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(1)]
pub fn borrow(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: PrincipalInput<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let _count = Self::borrow_action(&who, pool_id, loan_id, &amount, false)?;
T::Pool::withdraw(pool_id, who, amount.balance()?)?;
Self::deposit_event(Event::<T>::Borrowed {
pool_id,
loan_id,
amount,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::repay(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(2)]
pub fn repay(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: RepaidInput<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let (amount, _count) = Self::repay_action(&who, pool_id, loan_id, &amount, false)?;
T::Pool::deposit(pool_id, who, amount.repaid_amount()?.total()?)?;
Self::deposit_event(Event::<T>::Repaid {
pool_id,
loan_id,
amount,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::write_off(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(3)]
pub fn write_off(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> DispatchResult {
ensure_signed(origin)?;
let (status, _count) = Self::update_active_loan(pool_id, loan_id, |loan| {
let rule = Self::find_write_off_rule(pool_id, loan)?
.ok_or(Error::<T>::NoValidWriteOffRule)?;
let status = rule.status.compose_max(&loan.write_off_status());
loan.write_off(&status)?;
Ok(status)
})?;
Self::deposit_event(Event::<T>::WrittenOff {
pool_id,
loan_id,
status,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::admin_write_off(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(4)]
pub fn admin_write_off(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
percentage: T::Rate,
penalty: T::Rate,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::ensure_role(pool_id, &who, PoolRole::LoanAdmin)?;
let status = WriteOffStatus {
percentage,
penalty,
};
let (_, _count) = Self::update_active_loan(pool_id, loan_id, |loan| {
let rule = Self::find_write_off_rule(pool_id, loan)?;
Self::ensure_admin_write_off(&status, rule)?;
loan.write_off(&status)?;
Ok(())
})?;
Self::deposit_event(Event::<T>::WrittenOff {
pool_id,
loan_id,
status,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::propose_loan_mutation(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(5)]
pub fn propose_loan_mutation(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
mutation: LoanMutation<T::Rate>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::ensure_role(pool_id, &who, PoolRole::LoanAdmin)?;
let (mut loan, _count) = Self::get_active_loan(pool_id, loan_id)?;
transactional::with_transaction(|| {
let result = loan.mutate_with(mutation.clone());
TransactionOutcome::Rollback(result)
})?;
T::ChangeGuard::note(pool_id, Change::Loan(loan_id, mutation).into())?;
Ok(())
}
#[pallet::weight(T::WeightInfo::apply_loan_mutation(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(6)]
pub fn apply_loan_mutation(
origin: OriginFor<T>,
pool_id: T::PoolId,
change_id: T::Hash,
) -> DispatchResult {
ensure_signed(origin)?;
let Change::Loan(loan_id, mutation) = Self::get_released_change(pool_id, change_id)?
else {
Err(Error::<T>::UnrelatedChangeId)?
};
let (_, _count) = Self::update_active_loan(pool_id, loan_id, |loan| {
loan.mutate_with(mutation.clone())
})?;
Self::deposit_event(Event::<T>::Mutated {
pool_id,
loan_id,
mutation,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::close(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(7)]
pub fn close(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let ((closed_loan, borrower), _count) = match CreatedLoan::<T>::take(pool_id, loan_id) {
Some(created_loan) => (created_loan.close()?, Zero::zero()),
None => {
let (active_loan, count) = Self::take_active_loan(pool_id, loan_id)?;
(active_loan.close(pool_id)?, count)
}
};
Self::ensure_loan_borrower(&who, &borrower)?;
let collateral = closed_loan.collateral();
T::NonFungible::transfer(&collateral.0, &collateral.1, &who)?;
ClosedLoan::<T>::insert(pool_id, loan_id, closed_loan);
Self::deposit_event(Event::<T>::Closed {
pool_id,
loan_id,
collateral,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::propose_write_off_policy())]
#[pallet::call_index(8)]
pub fn propose_write_off_policy(
origin: OriginFor<T>,
pool_id: T::PoolId,
policy: BoundedVec<WriteOffRule<T::Rate>, T::MaxWriteOffPolicySize>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::ensure_role(pool_id, &who, PoolRole::PoolAdmin)?;
Self::ensure_pool_exists(pool_id)?;
T::ChangeGuard::note(pool_id, Change::Policy(policy).into())?;
Ok(())
}
#[pallet::weight(T::WeightInfo::apply_write_off_policy())]
#[pallet::call_index(9)]
pub fn apply_write_off_policy(
origin: OriginFor<T>,
pool_id: T::PoolId,
change_id: T::Hash,
) -> DispatchResult {
ensure_signed(origin)?;
let Change::Policy(policy) = Self::get_released_change(pool_id, change_id)? else {
Err(Error::<T>::UnrelatedChangeId)?
};
Self::update_write_off_policy(pool_id, policy)?;
Ok(())
}
#[pallet::weight(T::WeightInfo::update_portfolio_valuation(
T::MaxActiveLoansPerPool::get()
))]
#[pallet::call_index(10)]
pub fn update_portfolio_valuation(
origin: OriginFor<T>,
pool_id: T::PoolId,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
Self::ensure_pool_exists(pool_id)?;
let (_, count) = Self::update_portfolio_valuation_for_pool(
pool_id,
PriceCollectionInput::FromRegistry,
)?;
Ok(Some(T::WeightInfo::update_portfolio_valuation(count)).into())
}
#[pallet::weight(T::WeightInfo::propose_transfer_debt(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(11)]
pub fn propose_transfer_debt(
origin: OriginFor<T>,
pool_id: T::PoolId,
from_loan_id: T::LoanId,
to_loan_id: T::LoanId,
repaid_amount: RepaidInput<T>,
borrow_amount: PrincipalInput<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
transactional::with_transaction(|| {
let result = Self::transfer_debt_action(
&who,
pool_id,
from_loan_id,
to_loan_id,
repaid_amount.clone(),
borrow_amount.clone(),
false,
);
TransactionOutcome::Rollback(result)
})?;
T::ChangeGuard::note(
pool_id,
Change::TransferDebt(from_loan_id, to_loan_id, repaid_amount, borrow_amount).into(),
)?;
Ok(())
}
#[pallet::weight(T::WeightInfo::apply_transfer_debt(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(12)]
pub fn apply_transfer_debt(
origin: OriginFor<T>,
pool_id: T::PoolId,
change_id: T::Hash,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let Change::TransferDebt(from_loan_id, to_loan_id, repaid_amount, borrow_amount) =
Self::get_released_change(pool_id, change_id)?
else {
Err(Error::<T>::UnrelatedChangeId)?
};
let (repaid_amount, _count) = Self::transfer_debt_action(
&who,
pool_id,
from_loan_id,
to_loan_id,
repaid_amount.clone(),
borrow_amount.clone(),
true,
)?;
Self::deposit_event(Event::<T>::DebtTransferred {
pool_id,
from_loan_id,
to_loan_id,
repaid_amount,
borrow_amount,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::increase_debt(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(13)]
pub fn increase_debt(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: PrincipalInput<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let _count = Self::borrow_action(&who, pool_id, loan_id, &amount, false)?;
Self::deposit_event(Event::<T>::DebtIncreased {
pool_id,
loan_id,
amount,
});
Ok(())
}
#[pallet::weight(T::WeightInfo::increase_debt(T::MaxActiveLoansPerPool::get()))]
#[pallet::call_index(14)]
pub fn decrease_debt(
origin: OriginFor<T>,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: RepaidInput<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let (amount, _count) = Self::repay_action(&who, pool_id, loan_id, &amount, false)?;
Self::deposit_event(Event::<T>::DebtDecreased {
pool_id,
loan_id,
amount,
});
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn borrow_action(
who: &T::AccountId,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: &PrincipalInput<T>,
permissionless: bool,
) -> Result<u32, DispatchError> {
Ok(match CreatedLoan::<T>::take(pool_id, loan_id) {
Some(created_loan) => {
if !permissionless {
Self::ensure_loan_borrower(who, created_loan.borrower())?;
}
let mut active_loan = created_loan.activate(pool_id, amount.clone())?;
active_loan.borrow(amount, pool_id)?;
Self::insert_active_loan(pool_id, loan_id, active_loan)?
}
None => {
Self::update_active_loan(pool_id, loan_id, |loan| {
if !permissionless {
Self::ensure_loan_borrower(who, loan.borrower())?;
}
loan.borrow(amount, pool_id)
})?
.1
}
})
}
fn repay_action(
who: &T::AccountId,
pool_id: T::PoolId,
loan_id: T::LoanId,
amount: &RepaidInput<T>,
permissionless: bool,
) -> Result<(RepaidInput<T>, u32), DispatchError> {
Self::update_active_loan(pool_id, loan_id, |loan| {
if !permissionless {
Self::ensure_loan_borrower(who, loan.borrower())?;
}
loan.repay(amount.clone(), pool_id)
})
}
fn transfer_debt_action(
who: &T::AccountId,
pool_id: T::PoolId,
from_loan_id: T::LoanId,
to_loan_id: T::LoanId,
repaid_amount: RepaidInput<T>,
borrow_amount: PrincipalInput<T>,
permissionless: bool,
) -> Result<(RepaidInput<T>, u32), DispatchError> {
ensure!(
from_loan_id != to_loan_id,
Error::<T>::TransferDebtToSameLoan
);
let repaid_amount =
Self::repay_action(who, pool_id, from_loan_id, &repaid_amount, permissionless)?.0;
ensure!(
borrow_amount.balance()? == repaid_amount.repaid_amount()?.total()?,
Error::<T>::TransferDebtAmountMismatched
);
let count =
Self::borrow_action(who, pool_id, to_loan_id, &borrow_amount, permissionless)?;
Ok((repaid_amount, count))
}
#[cfg(feature = "runtime-benchmarks")]
pub fn expire_action(pool_id: T::PoolId, loan_id: T::LoanId) -> DispatchResult {
Self::update_active_loan(pool_id, loan_id, |loan| {
loan.set_maturity(T::Time::now());
Ok(())
})?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn ensure_role(pool_id: T::PoolId, who: &T::AccountId, role: PoolRole) -> DispatchResult {
T::Permissions::has(
PermissionScope::Pool(pool_id),
who.clone(),
Role::PoolRole(role),
)
.then_some(())
.ok_or_else(|| BadOrigin.into())
}
fn ensure_collateral_owner(
owner: &T::AccountId,
(collection_id, item_id): AssetOf<T>,
) -> DispatchResult {
T::NonFungible::owner(&collection_id, &item_id)
.ok_or(Error::<T>::NFTOwnerNotFound)?
.eq(owner)
.then_some(())
.ok_or_else(|| Error::<T>::NotNFTOwner.into())
}
fn ensure_loan_borrower(owner: &T::AccountId, borrower: &T::AccountId) -> DispatchResult {
ensure!(owner == borrower, Error::<T>::NotLoanBorrower);
Ok(())
}
fn ensure_pool_exists(pool_id: T::PoolId) -> DispatchResult {
ensure!(T::Pool::pool_exists(pool_id), Error::<T>::PoolNotFound);
Ok(())
}
fn ensure_admin_write_off(
status: &WriteOffStatus<T::Rate>,
rule: Option<WriteOffRule<T::Rate>>,
) -> DispatchResult {
let limit = rule.map(|r| r.status).unwrap_or_else(|| status.clone());
ensure!(
status.percentage >= limit.percentage && status.penalty >= limit.penalty,
Error::<T>::from(WrittenOffError::LessThanPolicy)
);
Ok(())
}
fn generate_loan_id(pool_id: T::PoolId) -> Result<T::LoanId, ArithmeticError> {
LastLoanId::<T>::try_mutate(pool_id, |last_loan_id| {
last_loan_id.ensure_add_assign(One::one())?;
Ok(*last_loan_id)
})
}
fn find_write_off_rule(
pool_id: T::PoolId,
loan: &ActiveLoan<T>,
) -> Result<Option<WriteOffRule<T::Rate>>, DispatchError> {
let rules = WriteOffPolicy::<T>::get(pool_id).into_iter();
policy::find_rule(rules, |trigger| {
loan.check_write_off_trigger(trigger, pool_id)
})
}
fn get_released_change(
pool_id: T::PoolId,
change_id: T::Hash,
) -> Result<Change<T>, DispatchError> {
T::ChangeGuard::released(pool_id, change_id)?
.try_into()
.map_err(|_| Error::<T>::NoLoanChangeId.into())
}
pub fn registered_prices(
pool_id: T::PoolId,
) -> Result<BTreeMap<T::PriceId, PriceOf<T>>, DispatchError> {
let collection = T::PriceRegistry::collection(&pool_id)?;
Ok(ActiveLoans::<T>::get(pool_id)
.iter()
.filter_map(|(_, loan)| loan.price_id())
.filter_map(|price_id| {
collection
.get(&price_id)
.map(|price| (price_id, (price.0, price.1)))
.ok()
})
.collect::<BTreeMap<_, _>>())
}
pub fn update_portfolio_valuation_for_pool(
pool_id: T::PoolId,
input_prices: PriceCollectionInput<T>,
) -> Result<(T::Balance, u32), DispatchError> {
let rates = T::InterestAccrual::rates();
let prices = match input_prices {
PriceCollectionInput::Empty => BTreeMap::default(),
PriceCollectionInput::Custom(prices) => prices.into(),
PriceCollectionInput::FromRegistry => Self::registered_prices(pool_id)?,
};
let loans = ActiveLoans::<T>::get(pool_id);
let values = loans
.iter()
.map(|(loan_id, loan)| Ok((*loan_id, loan.present_value_by(&rates, &prices)?)))
.collect::<Result<Vec<_>, DispatchError>>()?;
let portfolio = portfolio::PortfolioValuation::from_values(T::Time::now(), values)?;
let valuation = portfolio.value();
PortfolioValuation::<T>::insert(pool_id, portfolio);
Self::deposit_event(Event::<T>::PortfolioValuationUpdated {
pool_id,
valuation,
update_type: PortfolioValuationUpdateType::Exact,
});
Ok((valuation, loans.len() as u32))
}
fn insert_active_loan(
pool_id: T::PoolId,
loan_id: T::LoanId,
loan: ActiveLoan<T>,
) -> Result<u32, DispatchError> {
PortfolioValuation::<T>::try_mutate(pool_id, |portfolio| {
portfolio.insert_elem(loan_id, loan.present_value(pool_id)?)?;
Self::deposit_event(Event::<T>::PortfolioValuationUpdated {
pool_id,
valuation: portfolio.value(),
update_type: PortfolioValuationUpdateType::Inexact,
});
ActiveLoans::<T>::try_mutate(pool_id, |active_loans| {
active_loans
.try_push((loan_id, loan))
.map_err(|_| Error::<T>::MaxActiveLoansReached)?;
Ok(active_loans.len().ensure_into()?)
})
})
}
fn update_active_loan<F, R>(
pool_id: T::PoolId,
loan_id: T::LoanId,
f: F,
) -> Result<(R, u32), DispatchError>
where
F: FnOnce(&mut ActiveLoan<T>) -> Result<R, DispatchError>,
{
PortfolioValuation::<T>::try_mutate(pool_id, |portfolio| {
ActiveLoans::<T>::try_mutate(pool_id, |active_loans| {
let (_, loan) = active_loans
.iter_mut()
.find(|(id, _)| *id == loan_id)
.ok_or(Error::<T>::LoanNotActiveOrNotFound)?;
let result = f(loan)?;
portfolio.update_elem(loan_id, loan.present_value(pool_id)?)?;
Self::deposit_event(Event::<T>::PortfolioValuationUpdated {
pool_id,
valuation: portfolio.value(),
update_type: PortfolioValuationUpdateType::Inexact,
});
Ok((result, active_loans.len().ensure_into()?))
})
})
}
fn update_write_off_policy(
pool_id: T::PoolId,
policy: BoundedVec<WriteOffRule<T::Rate>, T::MaxWriteOffPolicySize>,
) -> DispatchResult {
WriteOffPolicy::<T>::insert(pool_id, policy.clone());
Self::deposit_event(Event::<T>::WriteOffPolicyUpdated { pool_id, policy });
Ok(())
}
fn take_active_loan(
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> Result<(ActiveLoan<T>, u32), DispatchError> {
ActiveLoans::<T>::try_mutate(pool_id, |active_loans| {
let index = active_loans
.iter()
.position(|(id, _)| *id == loan_id)
.ok_or(Error::<T>::LoanNotActiveOrNotFound)?;
PortfolioValuation::<T>::try_mutate(pool_id, |portfolio| {
portfolio.remove_elem(loan_id)
})?;
Ok((
active_loans.swap_remove(index).1,
active_loans.len().ensure_into()?,
))
})
}
fn get_active_loan(
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> Result<(ActiveLoan<T>, u32), DispatchError> {
let active_loans = ActiveLoans::<T>::get(pool_id);
let count = active_loans.len().ensure_into()?;
let (_, loan) = active_loans
.into_iter()
.find(|(id, _)| *id == loan_id)
.ok_or(Error::<T>::LoanNotActiveOrNotFound)?;
Ok((loan, count))
}
pub fn get_active_loans_info(
pool_id: T::PoolId,
) -> Result<PortfolioInfoOf<T>, DispatchError> {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.map(|(loan_id, loan)| Ok((loan_id, ActiveLoanInfo::try_from((pool_id, loan))?)))
.collect()
}
pub fn get_active_loan_info(
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> Result<Option<ActiveLoanInfo<T>>, DispatchError> {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.find(|(id, _)| *id == loan_id)
.map(|(_, loan)| ActiveLoanInfo::try_from((pool_id, loan)))
.transpose()
}
pub fn expected_cashflows(
pool_id: T::PoolId,
loan_id: T::LoanId,
) -> Result<Vec<CashflowPayment<T::Balance>>, DispatchError> {
ActiveLoans::<T>::get(pool_id)
.into_iter()
.find(|(id, _)| *id == loan_id)
.map(|(_, loan)| loan.expected_cashflows())
.ok_or(Error::<T>::LoanNotActiveOrNotFound)?
}
}
impl<T: Config> PoolNAV<T::PoolId, T::Balance> for Pallet<T> {
type ClassId = T::ItemId;
type RuntimeOrigin = T::RuntimeOrigin;
fn nav(pool_id: T::PoolId) -> Option<(T::Balance, Seconds)> {
let portfolio = PortfolioValuation::<T>::get(pool_id);
Some((portfolio.value(), portfolio.last_updated()))
}
fn update_nav(pool_id: T::PoolId) -> Result<T::Balance, DispatchError> {
Self::update_portfolio_valuation_for_pool(pool_id, PriceCollectionInput::FromRegistry)
.map(|portfolio| portfolio.0)
}
fn initialise(_: OriginFor<T>, _: T::PoolId, _: T::ItemId) -> DispatchResult {
Ok(())
}
}
impl<T: Config> PoolWriteOffPolicyMutate<T::PoolId> for Pallet<T> {
type Policy = BoundedVec<WriteOffRule<T::Rate>, T::MaxWriteOffPolicySize>;
fn update(pool_id: T::PoolId, policy: Self::Policy) -> DispatchResult {
Self::update_write_off_policy(pool_id, policy)
}
#[cfg(feature = "runtime-benchmarks")]
fn worst_case_policy() -> Self::Policy {
use crate::pallet::policy::WriteOffTrigger;
vec![
WriteOffRule::new(
[WriteOffTrigger::PrincipalOverdue(0)],
T::Rate::zero(),
T::Rate::zero(),
);
T::MaxWriteOffPolicySize::get() as usize
]
.try_into()
.unwrap()
}
}
}