Skip to content

Commit

Permalink
feat: add PoolFees RtApi & AccruedFees event (#1728)
Browse files Browse the repository at this point in the history
* fix: disburse fees on executing previous epoch

* feat: add Accrued event for fixed fees

* feat: add pool fees RtApi

* refactor: add PoolFeesOfBucket explicit RtApi type
  • Loading branch information
wischli authored Feb 12, 2024
1 parent f4f4bcc commit 7f2e1c9
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 6 deletions.
19 changes: 18 additions & 1 deletion libs/types/src/pools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

use cfg_traits::{fee::FeeAmountProration, Seconds};
use cfg_traits::{
fee::{FeeAmountProration, PoolFeeBucket},
Seconds,
};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::FixedPointOperand;
use sp_runtime::{traits::Get, BoundedVec, RuntimeDebug};
use sp_std::vec::Vec;

use crate::fixed_point::FixedPointNumberExtension;

Expand Down Expand Up @@ -249,6 +253,19 @@ pub fn saturated_rate_proration<Rate: FixedPointNumberExtension>(
))
}

/// Represents all active fees of a pool fee bucket
#[derive(Decode, Encode, TypeInfo)]
pub struct PoolFeesOfBucket<FeeId, AccountId, Balance, Rate> {
/// The corresponding pool fee bucket
pub bucket: PoolFeeBucket,
/// The list of active fees for the bucket
pub fees: Vec<PoolFee<AccountId, FeeId, PoolFeeAmounts<Balance, Rate>>>,
}

/// Represent all active fees of a pool divided by buckets
pub type PoolFeesList<FeeId, AccountId, Balance, Rate> =
Vec<PoolFeesOfBucket<FeeId, AccountId, Balance, Rate>>;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
33 changes: 31 additions & 2 deletions pallets/pool-fees/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub mod pallet {
use cfg_types::{
pools::{
PayableFeeAmount, PoolFee, PoolFeeAmount, PoolFeeAmounts, PoolFeeEditor, PoolFeeInfo,
PoolFeeType,
PoolFeeType, PoolFeesList, PoolFeesOfBucket,
},
portfolio,
portfolio::{InitialPortfolioValuation, PortfolioValuationUpdateType},
Expand Down Expand Up @@ -269,6 +269,13 @@ pub mod pallet {
amount: T::Balance,
destination: T::AccountId,
},
/// A fixed pool fee accrued
Accrued {
pool_id: T::PoolId,
fee_id: T::FeeId,
pending: T::Balance,
disbursement: T::Balance,
},
/// The portfolio valuation for a pool was updated.
PortfolioValuationUpdated {
pool_id: T::PoolId,
Expand Down Expand Up @@ -641,6 +648,16 @@ pub mod pallet {
fee.amounts.payable =
PayableFeeAmount::UpTo(payable.ensure_sub(disbursement)?)
};

// Dispatch event for fixed fees
if let PoolFeeType::Fixed { .. } = fee.amounts.fee_type {
Self::deposit_event(Event::<T>::Accrued {
pool_id,
fee_id: fee.id,
pending: fee.amounts.pending,
disbursement: fee.amounts.disbursement,
});
}
}
Ok::<(), DispatchError>(())
})?;
Expand Down Expand Up @@ -709,7 +726,7 @@ pub mod pallet {
/// ```ignore
/// NAV(PoolFees) = sum(pending_fee_amount) = sum(epoch_amount - disbursement)
/// ```
fn update_portfolio_valuation_for_pool(
pub fn update_portfolio_valuation_for_pool(
pool_id: T::PoolId,
reserve: &mut T::Balance,
) -> Result<(T::Balance, u32), DispatchError> {
Expand Down Expand Up @@ -772,6 +789,18 @@ pub mod pallet {

Ok(())
}

// Returns all fees of a pool divided by the buckets
pub fn get_pool_fees(
pool_id: T::PoolId,
) -> PoolFeesList<T::FeeId, T::AccountId, T::Balance, T::Rate> {
PoolFeeBucket::iter()
.map(|bucket| PoolFeesOfBucket {
bucket,
fees: ActiveFees::<T>::get(pool_id, bucket).into_inner(),
})
.collect()
}
}

impl<T: Config> PoolFees for Pallet<T> {
Expand Down
11 changes: 10 additions & 1 deletion pallets/pool-fees/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use sp_std::vec::Vec;

use crate::{
pallet as pallet_pool_fees, pallet::AssetsUnderManagement, types::Change, ActiveFees, Event,
FeeIds, FeeIdsToPoolBucket, LastFeeId, PoolFeeInfoOf, PoolFeeOf,
Event::Accrued, FeeIds, FeeIdsToPoolBucket, LastFeeId, PoolFeeInfoOf, PoolFeeOf,
};

pub const SECONDS: u64 = 1000;
Expand Down Expand Up @@ -368,6 +368,15 @@ pub fn assert_pending_fee(
assert_eq!(PoolFees::get_active_fee(fee_id), Ok(pending_fee));
}

pub fn assert_accrued_event_did_not_dispatch() {
assert!(!System::events().iter().any(|e| {
match e.event {
RuntimeEvent::PoolFees(Accrued { .. }) => true,
_ => false,
}
}));
}

pub(crate) fn init_mocks() {
#[cfg(not(feature = "runtime-benchmarks"))]
MockIsAdmin::mock_check(|(pool_id, admin)| pool_id == POOL && admin == ADMIN);
Expand Down
47 changes: 45 additions & 2 deletions pallets/pool-fees/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,15 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
System::assert_has_event(
Event::Accrued {
pool_id: POOL,
fee_id,
pending: 0,
disbursement: fee_amount,
}
.into(),
);

assert_eq!(*res_post_fees, res_pre_fees - fee_amount);
assert_eq!(get_disbursements(), vec![fee_amount]);
Expand All @@ -515,6 +524,8 @@ mod disbursements {
let res_pre_fees = NAV / 100;
let res_post_fees = &mut res_pre_fees.clone();
let annual_rate = Rate::saturating_from_rational(1, 10);
let disbursement = NAV / 100;
let pending = NAV / 100 * 9;

let fee = new_fee(PoolFeeType::Fixed {
limit: PoolFeeAmount::ShareOfPortfolioValuation(annual_rate),
Expand All @@ -528,6 +539,15 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
System::assert_has_event(
Event::Accrued {
pool_id: POOL,
fee_id,
pending,
disbursement,
}
.into(),
);

assert_eq!(*res_post_fees, 0);
assert_eq!(get_disbursements(), vec![res_pre_fees]);
Expand Down Expand Up @@ -566,6 +586,15 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
System::assert_has_event(
Event::Accrued {
pool_id: POOL,
fee_id,
pending: 0,
disbursement: fee_amount,
}
.into(),
);

assert_eq!(*res_post_fees, res_pre_fees - fee_amount);
assert_eq!(get_disbursements(), vec![fee_amount]);
Expand Down Expand Up @@ -597,6 +626,15 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
System::assert_has_event(
Event::Accrued {
pool_id: POOL,
fee_id,
pending: res_pre_fees,
disbursement: res_pre_fees,
}
.into(),
);

assert_eq!(*res_post_fees, 0);
assert_eq!(get_disbursements(), vec![res_pre_fees]);
Expand Down Expand Up @@ -629,7 +667,8 @@ mod disbursements {

mod share_of_portfolio {
use super::*;
use crate::mock::assert_pending_fee;
use crate::mock::{assert_accrued_event_did_not_dispatch, assert_pending_fee};

#[test]
fn empty_charge_scfs() {
ExtBuilder::default().set_aum(NAV).build().execute_with(|| {
Expand All @@ -651,6 +690,7 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
assert_accrued_event_did_not_dispatch();

assert_eq!(*res_post_fees, res_pre_fees);
assert_eq!(get_disbursements().into_iter().sum::<Balance>(), 0);
Expand Down Expand Up @@ -688,6 +728,7 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
assert_accrued_event_did_not_dispatch();

assert_eq!(*res_post_fees, res_pre_fees - charged_amount);
assert_eq!(get_disbursements(), vec![charged_amount]);
Expand Down Expand Up @@ -826,7 +867,7 @@ mod disbursements {
use cfg_traits::EpochTransitionHook;

use super::*;
use crate::mock::assert_pending_fee;
use crate::mock::{assert_accrued_event_did_not_dispatch, assert_pending_fee};

#[test]
fn empty_charge_scfa() {
Expand All @@ -849,6 +890,7 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
assert_accrued_event_did_not_dispatch();

assert_eq!(*res_post_fees, res_pre_fees);
assert_eq!(get_disbursements().into_iter().sum::<Balance>(), 0);
Expand Down Expand Up @@ -886,6 +928,7 @@ mod disbursements {
res_post_fees
));
assert_eq!(AssetsUnderManagement::<Runtime>::get(POOL), NAV + 100);
assert_accrued_event_did_not_dispatch();

assert_eq!(*res_post_fees, res_pre_fees - charged_amount);
assert_eq!(get_disbursements(), vec![charged_amount]);
Expand Down
2 changes: 2 additions & 0 deletions pallets/pool-system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,8 @@ pub mod pallet {
// Get the orders
let orders = Self::summarize_orders(&pool.tranches, &epoch_tranche_prices)?;
if orders.all_are_zero() {
T::OnEpochTransition::on_execution_pre_fulfillments(pool_id)?;

pool.tranches.combine_with_mut_residual_top(
&epoch_tranche_prices,
|tranche, price| {
Expand Down
9 changes: 9 additions & 0 deletions runtime/altair/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,15 @@ impl_runtime_apis! {
}
}

// PoolFeesApi
impl runtime_common::apis::PoolFeesApi<Block, PoolId, PoolFeeId, AccountId, Balance, Rate> for Runtime {
fn list_fees(pool_id: PoolId) -> Option<cfg_types::pools::PoolFeesList<PoolFeeId, AccountId, Balance, Rate>> {
let pool = pallet_pool_system::Pool::<Runtime>::get(pool_id)?;
PoolFees::update_portfolio_valuation_for_pool(pool_id, &mut pool.reserve.total.clone()).ok()?;
Some(PoolFees::get_pool_fees(pool_id))
}
}

// Frontier APIs
impl fp_rpc::EthereumRuntimeRPCApi<Block> for Runtime {
fn chain_id() -> u64 {
Expand Down
12 changes: 12 additions & 0 deletions runtime/centrifuge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,7 @@ impl_runtime_apis! {
}
}

// LoansApi
impl runtime_common::apis::LoansApi<Block, PoolId, LoanId, ActiveLoanInfo<Runtime>> for Runtime {
fn portfolio(
pool_id: PoolId
Expand All @@ -2253,18 +2254,29 @@ impl_runtime_apis! {
}
}

// AccountConversionApi
impl runtime_common::apis::AccountConversionApi<Block, AccountId> for Runtime {
fn conversion_of(location: ::xcm::v3::MultiLocation) -> Option<AccountId> {
AccountConverter::<Runtime, LocationToAccountId>::try_convert(location).ok()
}
}

// OrderBookApi
impl runtime_common::apis::OrderBookApi<Block, CurrencyId, Balance> for Runtime {
fn min_fulfillment_amount(currency_id: CurrencyId) -> Option<Balance> {
OrderBook::min_fulfillment_amount(currency_id).ok()
}
}

// PoolFeesApi
impl runtime_common::apis::PoolFeesApi<Block, PoolId, PoolFeeId, AccountId, Balance, Rate> for Runtime {
fn list_fees(pool_id: PoolId) -> Option<cfg_types::pools::PoolFeesList<PoolFeeId, AccountId, Balance, Rate>> {
let pool = pallet_pool_system::Pool::<Runtime>::get(pool_id)?;
PoolFees::update_portfolio_valuation_for_pool(pool_id, &mut pool.reserve.total.clone()).ok()?;
Some(PoolFees::get_pool_fees(pool_id))
}
}

// Frontier APIs
impl fp_rpc::EthereumRuntimeRPCApi<Block> for Runtime {
fn chain_id() -> u64 {
Expand Down
2 changes: 2 additions & 0 deletions runtime/common/src/apis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub use anchors::*;
pub use investments::*;
pub use loans::*;
pub use order_book::*;
pub use pool_fees::*;
pub use pools::*;
pub use rewards::*;

Expand All @@ -24,5 +25,6 @@ mod anchors;
mod investments;
mod loans;
mod order_book;
mod pool_fees;
mod pools;
mod rewards;
33 changes: 33 additions & 0 deletions runtime/common/src/apis/pool_fees.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2023 Centrifuge Foundation (centrifuge.io).

// Centrifuge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version (see http://www.gnu.org/licenses).

// Centrifuge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

use cfg_types::pools::PoolFeesList;
use parity_scale_codec::Codec;
use sp_api::decl_runtime_apis;

decl_runtime_apis! {
/// Runtime for pallet-pool-fees.
///
/// Note: The runtime api is pallet specific, while the RPC methods
/// are more focused on domain-specific logic
pub trait PoolFeesApi<PoolId, FeeId, AccountId, Balance, Rate>
where
PoolId: Codec,
FeeId: Codec,
AccountId: Codec,
Balance: Codec,
Rate: Codec,
{
/// Simulate update of active fees and returns as list divided by buckets
fn list_fees(pool_id: PoolId) -> Option<PoolFeesList<FeeId, AccountId, Balance, Rate>>;
}
}
Loading

0 comments on commit 7f2e1c9

Please sign in to comment.