Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loans: Runtime API for updating the portfolio with fake prices #1748

Merged
merged 2 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions libs/mocks/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,6 @@ pub mod pallet {
}
}

impl<DataId, Data> Default for MockDataCollection<DataId, Data> {
fn default() -> Self {
Self(Box::new(|_| {
Err(DispatchError::Other("MockDataCollection: Data not found"))
}))
}
}

impl<DataId, Data> DataCollection<DataId> for MockDataCollection<DataId, Data> {
type Data = Data;

Expand Down
2 changes: 1 addition & 1 deletion libs/traits/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub trait DataRegistry<DataId, CollectionId> {
}

/// Abstration to represent a collection of data in memory
pub trait DataCollection<DataId>: Default {
pub trait DataCollection<DataId> {
/// Represents a data
type Data;

Expand Down
10 changes: 9 additions & 1 deletion pallets/loans/src/entities/input.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use frame_support::RuntimeDebugNoBound;
use frame_support::{storage::bounded_btree_map::BoundedBTreeMap, RuntimeDebugNoBound};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{ArithmeticError, DispatchError};
Expand Down Expand Up @@ -56,3 +56,11 @@ impl<T: Config> RepaidInput<T> {
})
}
}

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub enum PriceCollectionInput<T: Config> {
Empty,
Custom(BoundedBTreeMap<T::PriceId, T::Balance, T::MaxActiveLoansPerPool>),
FromRegistry,
}
18 changes: 12 additions & 6 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use cfg_traits::{
self,
data::DataCollection,
interest::{InterestAccrual, InterestRate, RateCollection},
Seconds, TimeAsSecs,
};
Expand All @@ -14,6 +13,7 @@ use sp_runtime::{
},
DispatchError,
};
use sp_std::collections::btree_map::BTreeMap;

use crate::{
entities::{
Expand All @@ -24,7 +24,7 @@ use crate::{
Pricing,
},
},
pallet::{AssetOf, Config, Error, PriceOf},
pallet::{AssetOf, Config, Error},
types::{
policy::{WriteOffStatus, WriteOffTrigger},
BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions,
Expand Down Expand Up @@ -229,6 +229,13 @@ impl<T: Config> ActiveLoan<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 write_off_status(&self) -> WriteOffStatus<T::Rate> {
WriteOffStatus {
percentage: self.write_off_percentage,
Expand Down Expand Up @@ -278,15 +285,14 @@ impl<T: Config> ActiveLoan<T> {
/// An optimized version of `ActiveLoan::present_value()` when some input
/// data can be used from cached collections. Instead of fetch the current
/// debt and prices from the pallets,
/// it get the values from caches previously fetched.
pub fn present_value_by<Rates, Prices>(
/// it the values from caches previously fetched.
pub fn present_value_by<Rates>(
&self,
rates: &Rates,
prices: &Prices,
prices: &BTreeMap<T::PriceId, T::Balance>,
) -> Result<T::Balance, DispatchError>
where
Rates: RateCollection<T::Rate, T::Balance, T::Balance>,
Prices: DataCollection<T::PriceId, Data = PriceOf<T>>,
{
let maturity_date = self.schedule.maturity.date();
let value = match &self.pricing {
Expand Down
25 changes: 12 additions & 13 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use cfg_traits::{
self,
data::{DataCollection, DataRegistry},
interest::InterestRate,
IntoSeconds, Seconds, TimeAsSecs,
self, data::DataRegistry, interest::InterestRate, IntoSeconds, Seconds, TimeAsSecs,
};
use cfg_types::adjustments::Adjustment;
use frame_support::{self, ensure, RuntimeDebug, RuntimeDebugNoBound};
Expand All @@ -12,10 +9,11 @@ use sp_runtime::{
traits::{EnsureAdd, EnsureFixedPointNumber, EnsureSub, Zero},
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber,
};
use sp_std::collections::btree_map::BTreeMap;

use crate::{
entities::interest::ActiveInterestRate,
pallet::{Config, Error, PriceOf},
pallet::{Config, Error},
};

#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)]
Expand Down Expand Up @@ -137,6 +135,10 @@ impl<T: Config> ExternalActivePricing<T> {
Ok((self.info, self.interest.deactivate()?))
}

pub fn price_id(&self) -> T::PriceId {
self.info.price_id
}

pub fn has_registered_price(&self, pool_id: T::PoolId) -> bool {
T::PriceRegistry::get(&self.info.price_id, &pool_id).is_ok()
}
Expand Down Expand Up @@ -193,17 +195,14 @@ impl<T: Config> ExternalActivePricing<T> {
self.outstanding_principal(pool_id, maturity)
}

pub fn present_value_cached<Prices>(
pub fn present_value_cached(
&self,
cache: &Prices,
cache: &BTreeMap<T::PriceId, T::Balance>,
maturity: Seconds,
) -> Result<T::Balance, DispatchError>
where
Prices: DataCollection<T::PriceId, Data = PriceOf<T>>,
{
) -> Result<T::Balance, DispatchError> {
let price = match cache.get(&self.info.price_id) {
Ok(data) => data.0,
Err(_) => self.linear_accrual_price(maturity)?,
Some(data) => *data,
None => self.linear_accrual_price(maturity)?,
};
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}
Expand Down
49 changes: 39 additions & 10 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use cfg_traits::{
self, changes::ChangeGuard, data::DataRegistry, interest::InterestAccrual, IntoSeconds,
Permissions, PoolInspect, PoolNAV, PoolReserve, PoolWriteOffPolicyMutate, Seconds,
TimeAsSecs,
self,
changes::ChangeGuard,
data::{DataCollection, DataRegistry},
interest::InterestAccrual,
IntoSeconds, Permissions, PoolInspect, PoolNAV, PoolReserve, PoolWriteOffPolicyMutate,
Seconds, TimeAsSecs,
};
use cfg_types::{
adjustments::Adjustment,
Expand All @@ -84,7 +87,7 @@ pub mod pallet {
};
use entities::{
changes::{Change, LoanMutation},
input::{PrincipalInput, RepaidInput},
input::{PriceCollectionInput, PrincipalInput, RepaidInput},
loans::{self, ActiveLoan, ActiveLoanInfo, LoanInfo},
};
use frame_support::{
Expand All @@ -103,7 +106,7 @@ pub mod pallet {
traits::{BadOrigin, EnsureAdd, EnsureAddAssign, EnsureInto, One, Zero},
ArithmeticError, FixedPointOperand, TransactionOutcome,
};
use sp_std::{vec, vec::Vec};
use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec};
use types::{
self,
policy::{self, WriteOffRule, WriteOffStatus},
Expand Down Expand Up @@ -150,7 +153,7 @@ pub mod pallet {
+ One;

/// Identify a loan in the pallet
type PriceId: Parameter + Member + TypeInfo + Copy + MaxEncodedLen;
type PriceId: Parameter + Member + TypeInfo + Copy + MaxEncodedLen + Ord;

/// Defines the rate type used for math computations
type Rate: Parameter + Member + FixedPointNumber + TypeInfo + MaxEncodedLen;
Expand Down Expand Up @@ -775,7 +778,10 @@ pub mod pallet {
ensure_signed(origin)?;
Self::ensure_pool_exists(pool_id)?;

let (_, count) = Self::update_portfolio_valuation_for_pool(pool_id)?;
let (_, count) = Self::update_portfolio_valuation_for_pool(
pool_id,
PriceCollectionInput::FromRegistry,
)?;

Ok(Some(T::WeightInfo::update_portfolio_valuation(count)).into())
}
Expand Down Expand Up @@ -1051,11 +1057,33 @@ pub mod pallet {
.map_err(|_| Error::<T>::NoLoanChangeId.into())
}

fn update_portfolio_valuation_for_pool(
fn registered_prices(
pool_id: T::PoolId,
) -> Result<BTreeMap<T::PriceId, T::Balance>, 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))
.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 = T::PriceRegistry::collection(&pool_id)?;
let prices = match input_prices {
PriceCollectionInput::Empty => BTreeMap::default(),
PriceCollectionInput::Custom(prices) => prices.into(),
PriceCollectionInput::FromRegistry => Self::registered_prices(pool_id)?,
Comment on lines +1082 to +1084
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benfit of using the PriceCollectionInput here vs just using a BTreeMap as the parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@lemunozm lemunozm Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty is just sugar for Custom([]) and could be removed. But the caller should be able to choose between Custom and FromRegistry.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer @lemunozm's proposal over just using a BTreeMap as it is more explicit which is always helpful for runtime API which can be expected to be used by externals as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, you changed the valuation parameters of the runtime-api. Sounds good!

The most important thing though would be to mimic closing an epoch with

  • faked prices
  • Optional

Returning:

  • Fullfillment amounts for each tranche for both invest and redeem
  • New pool-reserve
  • New Tranche prices

Copy link
Contributor Author

@lemunozm lemunozm Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I guess somehow this should be amalgamated with some other pallet-pool-system calls to get this, right?

Then I propose to create another in the PoolSystem RuntimeAPI, maybe called simulate_close_epoch or similar, where all these calls are performed to return all the required values above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I'll do in another PR pool-related.

};

let loans = ActiveLoans::<T>::get(pool_id);
let values = loans
.iter()
Expand Down Expand Up @@ -1208,7 +1236,8 @@ pub mod pallet {
}

fn update_nav(pool_id: T::PoolId) -> Result<T::Balance, DispatchError> {
Ok(Self::update_portfolio_valuation_for_pool(pool_id)?.0)
Self::update_portfolio_valuation_for_pool(pool_id, PriceCollectionInput::FromRegistry)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick comprehension question: Our rather newer NAV calculation runtime API calls Loans::update_nav(pool_id) such that the Pool Fees NAV calculation can be based on that. Say the loans NAV is updated with a price collection input in block i and in the next block i+1 we are querying the NAV via PoolsApi::nav(pool_id) which performs Loans::update_nav(pool_id) with an update based on PriceCollectionInput::FromRegistry. IIUC, the prices from the update in block i are still cached. What I want to find out is if Loans::update_nav can make the Loans NAV less accurate if it was previously based on a custom price list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was previously based on the price registry, so both lines above are exactly identical. From a pool fees perspective, nothing has changed. The only open door (right now) is for the RuntimeAPI.

.map(|portfolio| portfolio.0)
}

fn initialise(_: OriginFor<T>, _: T::PoolId, _: T::ItemId) -> DispatchResult {
Expand Down
18 changes: 16 additions & 2 deletions runtime/altair/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2144,7 +2144,7 @@ impl fp_rpc::ConvertTransaction<sp_runtime::OpaqueExtrinsic> for TransactionConv

#[cfg(not(feature = "disable-runtime-api"))]
mod __runtime_api_use {
pub use pallet_loans::entities::loans::ActiveLoanInfo;
pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo};
}

#[cfg(not(feature = "disable-runtime-api"))]
Expand Down Expand Up @@ -2356,7 +2356,14 @@ impl_runtime_apis! {
}
}

impl runtime_common::apis::LoansApi<Block, PoolId, LoanId, ActiveLoanInfo<Runtime>> for Runtime {
impl runtime_common::apis::LoansApi<
Block,
PoolId,
LoanId,
ActiveLoanInfo<Runtime>,
Balance,
PriceCollectionInput<Runtime>
> for Runtime {
fn portfolio(
pool_id: PoolId
) -> Vec<(LoanId, ActiveLoanInfo<Runtime>)> {
Expand All @@ -2369,6 +2376,13 @@ impl_runtime_apis! {
) -> Option<ActiveLoanInfo<Runtime>> {
Loans::get_active_loan_info(pool_id, loan_id).ok().flatten()
}

fn portfolio_valuation(
pool_id: PoolId,
input_prices: PriceCollectionInput<Runtime>
) -> Result<Balance, DispatchError> {
Ok(Loans::update_portfolio_valuation_for_pool(pool_id, input_prices)?.0)
}
}


Expand Down
18 changes: 16 additions & 2 deletions runtime/centrifuge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2193,7 +2193,7 @@ impl fp_rpc::ConvertTransaction<sp_runtime::OpaqueExtrinsic> for TransactionConv

#[cfg(not(feature = "disable-runtime-api"))]
mod __runtime_api_use {
pub use pallet_loans::entities::loans::ActiveLoanInfo;
pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo};
}

#[cfg(not(feature = "disable-runtime-api"))]
Expand Down Expand Up @@ -2407,7 +2407,14 @@ impl_runtime_apis! {
}

// LoansApi
impl runtime_common::apis::LoansApi<Block, PoolId, LoanId, ActiveLoanInfo<Runtime>> for Runtime {
impl runtime_common::apis::LoansApi<
Block,
PoolId,
LoanId,
ActiveLoanInfo<Runtime>,
Balance,
PriceCollectionInput<Runtime>
> for Runtime {
fn portfolio(
pool_id: PoolId
) -> Vec<(LoanId, ActiveLoanInfo<Runtime>)> {
Expand All @@ -2420,6 +2427,13 @@ impl_runtime_apis! {
) -> Option<ActiveLoanInfo<Runtime>> {
Loans::get_active_loan_info(pool_id, loan_id).ok().flatten()
}

fn portfolio_valuation(
pool_id: PoolId,
input_prices: PriceCollectionInput<Runtime>
) -> Result<Balance, DispatchError> {
Ok(Loans::update_portfolio_valuation_for_pool(pool_id, input_prices)?.0)
}
}

// Investment Runtime APIs
Expand Down
6 changes: 5 additions & 1 deletion runtime/common/src/apis/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@

use parity_scale_codec::Codec;
use sp_api::decl_runtime_apis;
use sp_runtime::DispatchError;
use sp_std::vec::Vec;

decl_runtime_apis! {
/// Runtime API for the rewards pallet.
pub trait LoansApi<PoolId, LoanId, Loan>
pub trait LoansApi<PoolId, LoanId, Loan, Balance, PriceCollectionInput>
where
PoolId: Codec,
LoanId: Codec,
Loan: Codec,
Balance: Codec,
PriceCollectionInput: Codec
{
fn portfolio(pool_id: PoolId) -> Vec<(LoanId, Loan)>;
fn portfolio_loan(pool_id: PoolId, loan_id: LoanId) -> Option<Loan>;
fn portfolio_valuation(pool_id: PoolId, input_prices: PriceCollectionInput) -> Result<Balance, DispatchError>;
}
}
18 changes: 16 additions & 2 deletions runtime/development/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,7 @@ impl fp_rpc::ConvertTransaction<sp_runtime::OpaqueExtrinsic> for TransactionConv

#[cfg(not(feature = "disable-runtime-api"))]
mod __runtime_api_use {
pub use pallet_loans::entities::loans::ActiveLoanInfo;
pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo};
}

#[cfg(not(feature = "disable-runtime-api"))]
Expand Down Expand Up @@ -2449,7 +2449,14 @@ impl_runtime_apis! {
}

// LoansApi
impl runtime_common::apis::LoansApi<Block, PoolId, LoanId, ActiveLoanInfo<Runtime>> for Runtime {
impl runtime_common::apis::LoansApi<
Block,
PoolId,
LoanId,
ActiveLoanInfo<Runtime>,
Balance,
PriceCollectionInput<Runtime>
> for Runtime {
fn portfolio(
pool_id: PoolId
) -> Vec<(LoanId, ActiveLoanInfo<Runtime>)> {
Expand All @@ -2462,6 +2469,13 @@ impl_runtime_apis! {
) -> Option<ActiveLoanInfo<Runtime>> {
Loans::get_active_loan_info(pool_id, loan_id).ok().flatten()
}

fn portfolio_valuation(
pool_id: PoolId,
input_prices: PriceCollectionInput<Runtime>
) -> Result<Balance, DispatchError> {
Ok(Loans::update_portfolio_valuation_for_pool(pool_id, input_prices)?.0)
}
}

// Investment Runtime APIs
Expand Down
Loading
Loading