Skip to content

Commit

Permalink
Pricing ongoing loans (#884)
Browse files Browse the repository at this point in the history
* Minor organization changes

* First implementation attempt

* ensure_role no longer as a macro

* update UTs with new behaviour

* Fixed benchmarks

* Use renormalize_debt() to price active loan

* Update error variant
  • Loading branch information
lemunozm authored Aug 18, 2022
1 parent 4773efc commit 03483ee
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 119 deletions.
9 changes: 7 additions & 2 deletions pallets/loans/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,7 @@ where
(500 * CURRENCY).into(),
);

// add borrower role and price admin and risk admin role
make_free_cfg_balance::<T>(borrower::<T>());
make_free_cfg_balance::<T>(risk_admin::<T>());
assert_ok!(<T as pallet_pools::Config>::Permission::add(
PermissionScope::Pool(pool_id.into()),
borrower::<T>(),
Expand All @@ -188,6 +186,13 @@ where
borrower::<T>(),
Role::PoolRole(PoolRole::PricingAdmin)
));
assert_ok!(<T as pallet_pools::Config>::Permission::add(
PermissionScope::Pool(pool_id.into()),
borrower::<T>(),
Role::PoolRole(PoolRole::LoanAdmin)
));

make_free_cfg_balance::<T>(risk_admin::<T>());
assert_ok!(<T as pallet_pools::Config>::Permission::add(
PermissionScope::Pool(pool_id.into()),
risk_admin::<T>(),
Expand Down
151 changes: 96 additions & 55 deletions pallets/loans/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,24 @@
//! Module provides loan related functions
use super::*;
use common_types::{Adjustment, PoolLocator};
use sp_runtime::ArithmeticError;
use sp_runtime::{traits::BadOrigin, ArithmeticError};

impl<T: Config> Pallet<T> {
/// returns the account_id of the loan pallet
pub fn account_id() -> T::AccountId {
T::LoansPalletId::get().into_account_truncating()
}

pub fn ensure_role(
pool_id: PoolIdOf<T>,
sender: T::AccountId,
role: PoolRole,
) -> Result<(), BadOrigin> {
T::Permission::has(PermissionScope::Pool(pool_id), sender, Role::PoolRole(role))
.then_some(())
.ok_or(BadOrigin)
}

/// check if the given loan belongs to the owner provided
pub(crate) fn check_loan_owner(
pool_id: PoolIdOf<T>,
Expand Down Expand Up @@ -141,69 +151,100 @@ impl<T: Config> Pallet<T> {
Ok(loan_id)
}

pub(crate) fn price_loan(
pub(crate) fn price_created_loan(
pool_id: PoolIdOf<T>,
loan_id: T::LoanId,
interest_rate_per_sec: T::Rate,
loan_type: LoanType<T::Rate, T::Balance>,
) -> Result<u32, DispatchError> {
Loan::<T>::try_mutate(pool_id, loan_id, |loan| -> Result<u32, DispatchError> {
let loan = loan.as_mut().ok_or(Error::<T>::MissingLoan)?;
let now = Self::now();
ensure!(loan_type.is_valid(now), Error::<T>::LoanValueInvalid);

// ensure loan is created or priced but not yet borrowed against
ensure!(
loan.status == LoanStatus::Created || loan.status == LoanStatus::Active,
Error::<T>::LoanIsActive
);
let mut active_loans = ActiveLoans::<T>::get(pool_id);
if loan.status == LoanStatus::Active {
if let Some((idx, active_loan)) = active_loans
.iter()
.enumerate()
.find(|(_, active_loan)| active_loan.loan_id == loan_id)
{
ensure!(
active_loan.total_borrowed == Zero::zero(),
Error::<T>::LoanIsActive
);
active_loans.remove(idx);
}
}
ensure!(
interest_rate_per_sec >= One::one(),
Error::<T>::LoanValueInvalid
);

// ensure loan_type is valid
let now = Self::now();
ensure!(loan_type.is_valid(now), Error::<T>::LoanValueInvalid);
let active_loan = PricedLoanDetails {
loan_id,
loan_type,
interest_rate_per_sec,
origination_date: None,
normalized_debt: Zero::zero(),
total_borrowed: Zero::zero(),
total_repaid: Zero::zero(),
write_off_status: WriteOffStatus::None,
last_updated: now,
};
T::InterestAccrual::reference_rate(interest_rate_per_sec);

let mut active_loans = ActiveLoans::<T>::get(pool_id);
active_loans
.try_push(active_loan)
.map_err(|_| Error::<T>::TooManyActiveLoans)?;
let count = active_loans.len();
ActiveLoans::<T>::insert(pool_id, active_loans);

// ensure interest_rate_per_sec >= one
ensure!(
interest_rate_per_sec >= One::one(),
Error::<T>::LoanValueInvalid
);
Ok(count.try_into().unwrap())
}

let active_loan = PricedLoanDetails {
loan_id,
loan_type,
interest_rate_per_sec,
origination_date: None,
normalized_debt: Zero::zero(),
total_borrowed: Zero::zero(),
total_repaid: Zero::zero(),
write_off_status: WriteOffStatus::None,
last_updated: Self::now(),
};
T::InterestAccrual::reference_rate(interest_rate_per_sec);

active_loans
.try_push(active_loan)
.map_err(|_| Error::<T>::TooManyActiveLoans)?;
let count = active_loans.len();
ActiveLoans::<T>::insert(pool_id, active_loans);

// update the loan status
loan.status = LoanStatus::Active;

Ok(count.try_into().unwrap())
})
pub(crate) fn price_active_loan(
pool_id: PoolIdOf<T>,
loan_id: T::LoanId,
interest_rate_per_sec: T::Rate,
loan_type: LoanType<T::Rate, T::Balance>,
) -> Result<u32, DispatchError> {
let now = Self::now();
ensure!(loan_type.is_valid(now), Error::<T>::LoanValueInvalid);

ensure!(
interest_rate_per_sec >= One::one(),
Error::<T>::LoanValueInvalid
);

Self::try_mutate_active_loan(
pool_id,
loan_id,
|active_loan| -> Result<(), DispatchError> {
let old_debt = T::InterestAccrual::previous_debt(
active_loan.interest_rate_per_sec,
active_loan.normalized_debt,
active_loan.last_updated,
)?;

// calculate old present_value
let write_off_groups = PoolWriteOffGroups::<T>::get(pool_id);
let old_pv = active_loan
.present_value(old_debt, &write_off_groups, active_loan.last_updated)
.ok_or(Error::<T>::LoanPresentValueFailed)?;

// calculate new normalized debt without amount
let normalized_debt = T::InterestAccrual::renormalize_debt(
active_loan.interest_rate_per_sec,
interest_rate_per_sec,
active_loan.normalized_debt,
)?;

active_loan.loan_type = loan_type;
active_loan.interest_rate_per_sec = interest_rate_per_sec;
active_loan.normalized_debt = normalized_debt;
active_loan.last_updated = now;

let new_debt = T::InterestAccrual::current_debt(
active_loan.interest_rate_per_sec,
active_loan.normalized_debt,
)?;

// calculate new present_value
let new_pv = active_loan
.present_value(new_debt, &write_off_groups, now)
.ok_or(Error::<T>::LoanPresentValueFailed)?;

Self::update_nav_with_updated_present_value(pool_id, new_pv, old_pv)
},
)?;

Ok(ActiveLoans::<T>::get(pool_id).len().try_into().unwrap())
}

// try to close a given loan.
Expand Down
69 changes: 41 additions & 28 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ pub mod pallet {
use frame_system::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_arithmetic::FixedPointNumber;
use sp_runtime::traits::BadOrigin;

#[pallet::pallet]
#[pallet::generate_store(pub (super) trait Store)]
Expand Down Expand Up @@ -296,13 +295,13 @@ pub mod pallet {
ValueOverflow,
/// Emits when principal debt calculation failed due to overflow
NormalizedDebtOverflow,
/// Emits when tries to update an active loan
LoanIsActive,
/// Emits when tries to price a closed loan
LoanIsClosed,
/// Emits when loan type given is not valid
LoanTypeInvalid,
/// Emits when operation is done on an inactive loan
LoanNotActive,
// Emits when borrow and repay happens in the same block
/// Emits when borrow and repay happens in the same block
RepayTooEarly,
/// Emits when the NFT owner is not found
NFTOwnerNotFound,
Expand Down Expand Up @@ -353,7 +352,7 @@ pub mod pallet {
loan_nft_class_id: T::ClassId,
) -> DispatchResult {
// ensure the sender has the pool admin role
ensure_role!(pool_id, origin, PoolRole::PoolAdmin);
Self::ensure_role(pool_id, ensure_signed(origin)?, PoolRole::PoolAdmin)?;

// ensure pool exists
ensure!(T::Pool::pool_exists(pool_id), Error::<T>::PoolMissing);
Expand Down Expand Up @@ -393,7 +392,8 @@ pub mod pallet {
collateral: AssetOf<T>,
) -> DispatchResult {
// ensure borrower is whitelisted.
let owner = ensure_role!(pool_id, origin, PoolRole::Borrower);
let owner = ensure_signed(origin)?;
Self::ensure_role(pool_id, owner.clone(), PoolRole::Borrower)?;
let loan_id = Self::create_loan(pool_id, owner, collateral)?;
Self::deposit_event(Event::<T>::Created {
pool_id,
Expand Down Expand Up @@ -509,7 +509,7 @@ pub mod pallet {

/// Set pricing for the loan with loan specific details like Rate, Loan type
///
/// LoanStatus must be in Created state.
/// LoanStatus must be in Created or Active state.
/// Once activated, loan owner can start loan related functions like Borrow, Repay, Close
#[pallet::weight(<T as Config>::WeightInfo::price(T::MaxActiveLoansPerPool::get()))]
pub fn price(
Expand All @@ -519,16 +519,45 @@ pub mod pallet {
interest_rate_per_sec: T::Rate,
loan_type: LoanType<T::Rate, T::Balance>,
) -> DispatchResultWithPostInfo {
// ensure sender has the pricing admin role in the pool
ensure_role!(pool_id, origin, PoolRole::PricingAdmin);
let owner = ensure_signed(origin)?;

let active_count =
Self::price_loan(pool_id, loan_id, interest_rate_per_sec, loan_type)?;
Loan::<T>::try_mutate(pool_id, loan_id, |loan| -> Result<u32, DispatchError> {
let loan = loan.as_mut().ok_or(Error::<T>::MissingLoan)?;

match loan.status {
LoanStatus::Created => {
Self::ensure_role(pool_id, owner, PoolRole::PricingAdmin)?;
let active_count = Self::price_created_loan(
pool_id,
loan_id,
interest_rate_per_sec,
loan_type,
);

loan.status = LoanStatus::Active;
active_count
}
LoanStatus::Active => {
Self::ensure_role(pool_id, owner, PoolRole::LoanAdmin)?;
Self::price_active_loan(
pool_id,
loan_id,
interest_rate_per_sec,
loan_type,
)
}
LoanStatus::Closed { .. } => Err(Error::<T>::LoanIsClosed)?,
}
})?;

Self::deposit_event(Event::<T>::Priced {
pool_id,
loan_id,
interest_rate_per_sec,
loan_type,
});

Ok(Some(T::WeightInfo::price(active_count)).into())
}

Expand Down Expand Up @@ -570,7 +599,7 @@ pub mod pallet {
group: WriteOffGroup<T::Rate>,
) -> DispatchResult {
// ensure sender has the risk admin role in the pool
ensure_role!(pool_id, origin, PoolRole::LoanAdmin);
Self::ensure_role(pool_id, ensure_signed(origin)?, PoolRole::LoanAdmin)?;
let write_off_group_index = Self::add_write_off_group_to_pool(pool_id, group)?;
Self::deposit_event(Event::<T>::WriteOffGroupAdded {
pool_id,
Expand Down Expand Up @@ -634,7 +663,7 @@ pub mod pallet {
penalty_interest_rate_per_sec: T::Rate,
) -> DispatchResultWithPostInfo {
// ensure this is a call from risk admin
ensure_role!(pool_id, origin, PoolRole::LoanAdmin);
Self::ensure_role(pool_id, ensure_signed(origin)?, PoolRole::LoanAdmin)?;

// try to write off
let (active_count, (.., percentage, penalty_interest_rate_per_sec)) =
Expand Down Expand Up @@ -693,19 +722,3 @@ impl<T: Config> TPoolNav<PoolIdOf<T>, T::Balance> for Pallet<T> {
Self::initialise_pool(origin, pool_id, class_id)
}
}

#[macro_export]
macro_rules! ensure_role {
( $pool_id:expr, $origin:expr, $role:expr $(,)? ) => {{
let sender = ensure_signed($origin)?;
ensure!(
T::Permission::has(
PermissionScope::Pool($pool_id),
sender.clone(),
Role::PoolRole($role)
),
BadOrigin
);
sender
}};
}
Loading

0 comments on commit 03483ee

Please sign in to comment.