#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::or_fun_call)]
use cfg_traits::{Permissions, PoolInspect, PoolMutate, PoolNAV, PoolReserve, Seconds, TimeAsSecs};
use cfg_types::{
orders::SummarizedOrders,
permissions::{PermissionScope, PoolRole, Role},
};
use frame_support::{
dispatch::DispatchResult,
ensure,
pallet_prelude::RuntimeDebug,
traits::{
fungibles::{Inspect, Mutate},
ReservableCurrency,
},
transactional, BoundedVec,
};
use frame_system::pallet_prelude::{BlockNumberFor, *};
use orml_traits::{
asset_registry::{Inspect as OrmlInspect, Mutate as OrmlMutate},
Change,
};
pub use pallet::*;
use parity_scale_codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use pool_types::{
changes::{NotedPoolChange, PoolChangeProposal},
PoolChanges, PoolDepositInfo, PoolDetails, PoolEssence, PoolLocator, ScheduledUpdateDetails,
};
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
pub use solution::*;
use sp_arithmetic::traits::BaseArithmetic;
use sp_runtime::{
traits::{
AccountIdConversion, AtLeast32BitUnsigned, CheckedAdd, CheckedSub, EnsureAdd,
EnsureAddAssign, EnsureFixedPointNumber, EnsureSub, EnsureSubAssign, Get, One, Saturating,
Zero,
},
DispatchError, FixedPointNumber, FixedPointOperand, Perquintill, TokenError,
};
use sp_std::{cmp::Ordering, vec::Vec};
use tranches::{
EpochExecutionTranche, EpochExecutionTranches, Tranche, TrancheSolution, TrancheType,
TrancheUpdate, Tranches,
};
pub use weights::*;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
mod impls;
#[cfg(test)]
mod mock;
pub mod pool_types;
mod solution;
#[cfg(test)]
mod tests;
pub mod tranches;
pub mod weights;
#[allow(dead_code)]
pub type EpochExecutionTrancheOf<T> = EpochExecutionTranche<
<T as Config>::Balance,
<T as Config>::BalanceRatio,
<T as Config>::TrancheWeight,
<T as Config>::TrancheCurrency,
>;
#[allow(dead_code)]
pub type EpochExecutionTranchesOf<T> = EpochExecutionTranches<
<T as Config>::Balance,
<T as Config>::BalanceRatio,
<T as Config>::TrancheWeight,
<T as Config>::TrancheCurrency,
<T as Config>::MaxTranches,
>;
pub type TranchesOf<T> = Tranches<
<T as Config>::Balance,
<T as Config>::Rate,
<T as Config>::TrancheWeight,
<T as Config>::TrancheCurrency,
<T as Config>::TrancheId,
<T as Config>::PoolId,
<T as Config>::MaxTranches,
>;
#[allow(dead_code)]
pub type TrancheOf<T> = Tranche<
<T as Config>::Balance,
<T as Config>::Rate,
<T as Config>::TrancheWeight,
<T as Config>::TrancheCurrency,
>;
pub type PoolDetailsOf<T> = PoolDetails<
<T as Config>::CurrencyId,
<T as Config>::TrancheCurrency,
<T as Config>::EpochId,
<T as Config>::Balance,
<T as Config>::Rate,
<T as Config>::TrancheWeight,
<T as Config>::TrancheId,
<T as Config>::PoolId,
<T as Config>::MaxTranches,
>;
type EpochExecutionInfoOf<T> = EpochExecutionInfo<
<T as Config>::Balance,
<T as Config>::BalanceRatio,
<T as Config>::EpochId,
<T as Config>::TrancheWeight,
BlockNumberFor<T>,
<T as Config>::TrancheCurrency,
<T as Config>::MaxTranches,
>;
type PoolDepositOf<T> =
PoolDepositInfo<<T as frame_system::Config>::AccountId, <T as Config>::Balance>;
type ScheduledUpdateDetailsOf<T> = ScheduledUpdateDetails<
<T as Config>::Rate,
<T as Config>::StringLimit,
<T as Config>::MaxTranches,
>;
pub type PoolChangesOf<T> =
PoolChanges<<T as Config>::Rate, <T as Config>::StringLimit, <T as Config>::MaxTranches>;
pub type PoolEssenceOf<T> = PoolEssence<
<T as Config>::CurrencyId,
<T as Config>::Balance,
<T as Config>::TrancheCurrency,
<T as Config>::Rate,
<T as Config>::StringLimit,
>;
#[derive(Encode, Decode, TypeInfo, PartialEq, Eq, MaxEncodedLen, RuntimeDebug)]
#[repr(u32)]
pub enum Release {
V0,
V1,
}
impl Default for Release {
fn default() -> Self {
Self::V0
}
}
#[frame_support::pallet]
pub mod pallet {
use cfg_traits::{
fee::{PoolFeeBucket, PoolFeesInspect, PoolFeesMutate},
investments::{OrderManager, TrancheCurrency as TrancheCurrencyT},
EpochTransitionHook, PoolUpdateGuard,
};
use cfg_types::{
orders::{FulfillmentWithPrice, TotalOrder},
pools::PoolFeeInfo,
tokens::CustomMetadata,
};
use frame_support::{
pallet_prelude::*,
sp_runtime::traits::Convert,
traits::{tokens::Preservation, Contains, EnsureOriginWithArg},
PalletId,
};
use rev_slice::SliceExt;
use sp_runtime::{traits::BadOrigin, ArithmeticError};
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AdminOrigin: EnsureOriginWithArg<Self::RuntimeOrigin, Self::PoolId>;
type Balance: Member
+ Parameter
+ AtLeast32BitUnsigned
+ Default
+ Copy
+ MaxEncodedLen
+ FixedPointOperand
+ From<u64>
+ From<u128>
+ TypeInfo
+ TryInto<u64>;
type TrancheWeight: Parameter
+ Copy
+ Convert<Self::TrancheWeight, Self::Balance>
+ From<u128>;
type BalanceRatio: Member
+ Parameter
+ Default
+ Copy
+ TypeInfo
+ FixedPointNumber<Inner = Self::Balance>
+ MaxEncodedLen;
type Rate: Member
+ Parameter
+ Default
+ Copy
+ TypeInfo
+ FixedPointNumber<Inner = Self::Balance>
+ MaxEncodedLen;
#[pallet::constant]
type PalletId: Get<PalletId>;
#[pallet::constant]
type PalletIndex: Get<u8>;
type PoolId: Member
+ Parameter
+ Default
+ Copy
+ HasCompact
+ MaxEncodedLen
+ core::fmt::Debug;
type TrancheId: Member
+ Parameter
+ Default
+ Copy
+ MaxEncodedLen
+ TypeInfo
+ From<[u8; 16]>;
type EpochId: Member
+ Parameter
+ Default
+ Copy
+ AtLeast32BitUnsigned
+ HasCompact
+ MaxEncodedLen
+ TypeInfo
+ Into<u32>;
type CurrencyId: Parameter + Copy + MaxEncodedLen;
type RuntimeChange: Parameter + Member + MaxEncodedLen + TypeInfo + Into<PoolChangeProposal>;
type PoolCurrency: Contains<Self::CurrencyId>;
type UpdateGuard: PoolUpdateGuard<
PoolDetails = PoolDetailsOf<Self>,
ScheduledUpdateDetails = ScheduledUpdateDetailsOf<Self>,
Moment = Seconds,
>;
type AssetRegistry: OrmlMutate<
AssetId = Self::CurrencyId,
Balance = Self::Balance,
CustomMetadata = CustomMetadata,
StringLimit = Self::StringLimit,
>;
type Currency: ReservableCurrency<Self::AccountId, Balance = Self::Balance>;
type Tokens: Mutate<Self::AccountId>
+ Inspect<Self::AccountId, AssetId = Self::CurrencyId, Balance = Self::Balance>;
type Permission: Permissions<
Self::AccountId,
Scope = PermissionScope<Self::PoolId, Self::CurrencyId>,
Role = Role<Self::TrancheId>,
Error = DispatchError,
>;
type AssetsUnderManagementNAV: PoolNAV<Self::PoolId, Self::Balance>;
type PoolFeesNAV: PoolNAV<Self::PoolId, Self::Balance>;
type TrancheCurrency: Into<Self::CurrencyId>
+ Clone
+ Copy
+ TrancheCurrencyT<Self::PoolId, Self::TrancheId>
+ Parameter
+ MaxEncodedLen
+ TypeInfo;
type Investments: OrderManager<
Error = DispatchError,
InvestmentId = Self::TrancheCurrency,
Orders = TotalOrder<Self::Balance>,
Fulfillment = FulfillmentWithPrice<Self::BalanceRatio>,
>;
type Time: TimeAsSecs;
type PoolFees: PoolFeesMutate<
FeeInfo = PoolFeeInfo<
<Self as frame_system::Config>::AccountId,
Self::Balance,
Self::Rate,
>,
PoolId = Self::PoolId,
> + PoolFeesInspect<PoolId = Self::PoolId>;
type OnEpochTransition: EpochTransitionHook<
Balance = Self::Balance,
PoolId = Self::PoolId,
Time = Seconds,
Error = DispatchError,
>;
#[pallet::constant]
type ChallengeTime: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type DefaultMinEpochTime: Get<Seconds>;
#[pallet::constant]
type DefaultMaxNAVAge: Get<Seconds>;
#[pallet::constant]
type MinEpochTimeLowerBound: Get<Seconds>;
#[pallet::constant]
type MinEpochTimeUpperBound: Get<Seconds>;
#[pallet::constant]
type MaxNAVAgeUpperBound: Get<Seconds>;
#[pallet::constant]
type MinUpdateDelay: Get<Seconds>;
#[pallet::constant]
type StringLimit: Get<u32> + Copy + Member + scale_info::TypeInfo;
#[pallet::constant]
type MaxTranches: Get<u32> + Member + PartialOrd + scale_info::TypeInfo;
#[pallet::constant]
type PoolDeposit: Get<Self::Balance>;
type PoolCreateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type WeightInfo: WeightInfo;
}
const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn pool)]
pub type Pool<T: Config> = StorageMap<_, Blake2_128Concat, T::PoolId, PoolDetailsOf<T>>;
#[pallet::storage]
#[pallet::getter(fn scheduled_update)]
pub type ScheduledUpdate<T: Config> =
StorageMap<_, Blake2_128Concat, T::PoolId, ScheduledUpdateDetailsOf<T>>;
#[pallet::storage]
#[pallet::getter(fn epoch_targets)]
pub type EpochExecution<T: Config> =
StorageMap<_, Blake2_128Concat, T::PoolId, EpochExecutionInfoOf<T>>;
#[pallet::storage]
#[pallet::getter(fn account_deposits)]
pub type AccountDeposit<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, T::Balance, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn pool_deposits)]
pub type PoolDeposit<T: Config> = StorageMap<_, Blake2_128Concat, T::PoolId, PoolDepositOf<T>>;
#[pallet::storage]
pub type NotedChange<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::PoolId,
Blake2_128Concat,
T::Hash,
NotedPoolChange<T::RuntimeChange>,
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Rebalanced { pool_id: T::PoolId },
MaxReserveSet { pool_id: T::PoolId },
EpochClosed {
pool_id: T::PoolId,
epoch_id: T::EpochId,
},
SolutionSubmitted {
pool_id: T::PoolId,
epoch_id: T::EpochId,
solution: EpochSolution<T::Balance, T::MaxTranches>,
},
EpochExecuted {
pool_id: T::PoolId,
epoch_id: T::EpochId,
},
Created {
admin: T::AccountId,
depositor: T::AccountId,
pool_id: T::PoolId,
essence: PoolEssenceOf<T>,
},
Updated {
id: T::PoolId,
old: PoolEssenceOf<T>,
new: PoolEssenceOf<T>,
},
ProposedChange {
pool_id: T::PoolId,
change_id: T::Hash,
change: T::RuntimeChange,
},
ReleasedChange {
pool_id: T::PoolId,
change_id: T::Hash,
change: T::RuntimeChange,
},
NegativeBalanceSheet {
pool_id: T::PoolId,
nav_aum: T::Balance,
nav_fees: T::Balance,
reserve: T::Balance,
},
}
#[pallet::error]
pub enum Error<T> {
PoolInUse,
InvalidJuniorTranche,
InvalidTrancheStructure,
NoSuchPool,
MinEpochTimeHasNotPassed,
ChallengeTimeHasNotPassed,
InSubmissionPeriod,
NAVTooOld,
TrancheId,
WipedOut,
InvalidSolution,
NotInSubmissionPeriod,
InsufficientCurrency,
RiskBufferViolated,
NoNAV,
EpochNotExecutedYet,
CannotAddOrRemoveTranches,
InvalidTrancheSeniority,
InvalidTrancheUpdate,
MetadataForCurrencyNotFound,
TrancheTokenNameTooLong,
TrancheSymbolNameTooLong,
FailedToRegisterTrancheMetadata,
FailedToUpdateTrancheMetadata,
InvalidTrancheId,
TooManyTranches,
NotNewBestSubmission,
NoSolutionAvailable,
PoolParameterBoundViolated,
NoScheduledUpdate,
ScheduledTimeHasNotPassed,
UpdatePrerequesitesNotFulfilled,
InvalidCurrency,
ChangeNotFound,
ChangeNotReady,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::WeightInfo::set_max_reserve(T::PoolFees::get_max_fees_per_bucket()))]
#[pallet::call_index(0)]
pub fn set_max_reserve(
origin: OriginFor<T>,
pool_id: T::PoolId,
max_reserve: T::Balance,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(
T::Permission::has(
PermissionScope::Pool(pool_id),
who,
Role::PoolRole(PoolRole::LiquidityAdmin)
),
BadOrigin
);
Pool::<T>::try_mutate(pool_id, |pool| -> DispatchResult {
let pool = pool.as_mut().ok_or(Error::<T>::NoSuchPool)?;
pool.reserve.max = max_reserve;
Self::deposit_event(Event::MaxReserveSet { pool_id });
Ok(())
})
}
#[pallet::weight(T::WeightInfo::close_epoch_no_orders(T::MaxTranches::get(), T::PoolFees::get_max_fees_per_bucket())
.max(T::WeightInfo::close_epoch_no_execution(T::MaxTranches::get(), T::PoolFees::get_max_fees_per_bucket()))
.max(T::WeightInfo::close_epoch_execute(T::MaxTranches::get(), T::PoolFees::get_max_fees_per_bucket())))]
#[transactional]
#[pallet::call_index(1)]
pub fn close_epoch(origin: OriginFor<T>, pool_id: T::PoolId) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(origin, &pool_id)?;
Pool::<T>::try_mutate(pool_id, |pool| {
let pool = pool.as_mut().ok_or(Error::<T>::NoSuchPool)?;
ensure!(
!EpochExecution::<T>::contains_key(pool_id),
Error::<T>::InSubmissionPeriod
);
let now = T::Time::now();
ensure!(
now.saturating_sub(pool.epoch.last_closed) >= pool.parameters.min_epoch_time,
Error::<T>::MinEpochTimeHasNotPassed
);
let (nav_aum, aum_last_updated) =
T::AssetsUnderManagementNAV::nav(pool_id).ok_or(Error::<T>::NoNAV)?;
ensure!(
now.saturating_sub(aum_last_updated) <= pool.parameters.max_nav_age,
Error::<T>::NAVTooOld
);
T::OnEpochTransition::on_closing_mutate_reserve(
pool_id,
nav_aum,
&mut pool.reserve.total,
)?;
let (nav_fees, fees_last_updated) =
T::PoolFeesNAV::nav(pool_id).ok_or(Error::<T>::NoNAV)?;
ensure!(
now.saturating_sub(fees_last_updated) <= pool.parameters.max_nav_age,
Error::<T>::NAVTooOld
);
let nav = Nav::new(nav_aum, nav_fees);
let nav_total = nav
.total(pool.reserve.total)
.map_err(|_| {
Self::deposit_event(Event::NegativeBalanceSheet {
pool_id,
nav_aum,
nav_fees,
reserve: pool.reserve.total,
});
})
.unwrap_or(T::Balance::default());
let submission_period_epoch = pool.epoch.current;
pool.start_next_epoch(now)?;
let epoch_tranche_prices = pool
.tranches
.calculate_prices::<T::BalanceRatio, T::Tokens, _>(nav_total, now)?;
ensure!(
!epoch_tranche_prices
.iter()
.any(|price| *price == Zero::zero()),
Error::<T>::WipedOut
);
Self::deposit_event(Event::EpochClosed {
pool_id,
epoch_id: submission_period_epoch,
});
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| {
let zero_fulfillment = FulfillmentWithPrice {
of_amount: Perquintill::zero(),
price: *price,
};
T::Investments::invest_fulfillment(tranche.currency, zero_fulfillment)?;
T::Investments::redeem_fulfillment(tranche.currency, zero_fulfillment)
},
)?;
pool.execute_previous_epoch()?;
Self::deposit_event(Event::EpochExecuted {
pool_id,
epoch_id: submission_period_epoch,
});
return Ok(Some(T::WeightInfo::close_epoch_no_orders(
pool.tranches
.num_tranches()
.try_into()
.expect("MaxTranches is u32. qed."),
T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top),
))
.into());
}
let epoch_tranches: Vec<EpochExecutionTrancheOf<T>> =
pool.tranches.combine_with_residual_top(
epoch_tranche_prices
.iter()
.zip(orders.invest_redeem_residual_top()),
|tranche, (price, (invest, redeem))| {
let epoch_tranche = EpochExecutionTranche {
currency: tranche.currency,
supply: tranche.balance()?,
price: *price,
invest,
redeem,
seniority: tranche.seniority,
min_risk_buffer: tranche.min_risk_buffer(),
_phantom: Default::default(),
};
Ok(epoch_tranche)
},
)?;
let mut epoch = EpochExecutionInfo {
nav,
epoch: submission_period_epoch,
tranches: EpochExecutionTranches::new(epoch_tranches),
best_submission: None,
challenge_period_end: None,
};
let full_execution_solution = pool.tranches.combine_residual_top(|_| {
Ok(TrancheSolution {
invest_fulfillment: Perquintill::one(),
redeem_fulfillment: Perquintill::one(),
})
})?;
if Self::inspect_solution(pool, &epoch, &full_execution_solution)
.map(|state| state == PoolState::Healthy)
.unwrap_or(false)
{
Self::do_execute_epoch(pool_id, pool, &epoch, &full_execution_solution)?;
Self::deposit_event(Event::EpochExecuted {
pool_id,
epoch_id: submission_period_epoch,
});
Ok(Some(T::WeightInfo::close_epoch_execute(
pool.tranches
.num_tranches()
.try_into()
.expect("MaxTranches is u32. qed."),
T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top),
))
.into())
} else {
let no_execution_solution = pool.tranches.combine_residual_top(|_| {
Ok(TrancheSolution {
invest_fulfillment: Perquintill::zero(),
redeem_fulfillment: Perquintill::zero(),
})
})?;
let existing_state_solution =
Self::score_solution(pool, &epoch, &no_execution_solution)?;
epoch.best_submission = Some(existing_state_solution);
EpochExecution::<T>::insert(pool_id, epoch);
Ok(Some(T::WeightInfo::close_epoch_no_execution(
pool.tranches
.num_tranches()
.try_into()
.expect("MaxTranches is u32. qed."),
T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top),
))
.into())
}
})
}
#[pallet::weight(T::WeightInfo::submit_solution(
T::MaxTranches::get(),
T::PoolFees::get_max_fees_per_bucket()
))]
#[pallet::call_index(2)]
pub fn submit_solution(
origin: OriginFor<T>,
pool_id: T::PoolId,
solution: Vec<TrancheSolution>,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
EpochExecution::<T>::try_mutate(pool_id, |epoch| {
let epoch = epoch.as_mut().ok_or(Error::<T>::NotInSubmissionPeriod)?;
let pool = Pool::<T>::try_get(pool_id).map_err(|_| Error::<T>::NoSuchPool)?;
let new_solution = Self::score_solution(&pool, epoch, &solution)?;
if let Some(ref previous_solution) = epoch.best_submission {
ensure!(
&new_solution >= previous_solution,
Error::<T>::NotNewBestSubmission
);
}
epoch.best_submission = Some(new_solution.clone());
if epoch.challenge_period_end.is_none() {
epoch.challenge_period_end =
Some(Self::current_block().saturating_add(T::ChallengeTime::get()));
}
Self::deposit_event(Event::SolutionSubmitted {
pool_id,
epoch_id: epoch.epoch,
solution: new_solution,
});
Ok(Some(T::WeightInfo::submit_solution(
epoch
.tranches
.num_tranches()
.try_into()
.expect("MaxTranches is u32. qed."),
T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top),
))
.into())
})
}
#[pallet::weight(T::WeightInfo::execute_epoch(
T::MaxTranches::get(),
T::PoolFees::get_max_fees_per_bucket()
))]
#[pallet::call_index(3)]
pub fn execute_epoch(
origin: OriginFor<T>,
pool_id: T::PoolId,
) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(origin, &pool_id)?;
EpochExecution::<T>::try_mutate(pool_id, |epoch_info| {
let epoch = epoch_info
.as_mut()
.ok_or(Error::<T>::NotInSubmissionPeriod)?;
ensure!(
epoch.best_submission.is_some(),
Error::<T>::NoSolutionAvailable
);
ensure!(
epoch.challenge_period_end.is_some(),
Error::<T>::NoSolutionAvailable
);
ensure!(
epoch
.challenge_period_end
.expect("Challenge period is some. qed.")
<= Self::current_block(),
Error::<T>::ChallengeTimeHasNotPassed
);
Pool::<T>::try_mutate(pool_id, |pool| -> DispatchResult {
let pool = pool
.as_mut()
.expect("EpochExecutionInfo can only exist on existing pools. qed.");
let solution = &epoch
.best_submission
.as_ref()
.expect("Solution exists. qed.")
.solution();
Self::do_execute_epoch(pool_id, pool, epoch, solution)?;
Self::deposit_event(Event::EpochExecuted {
pool_id,
epoch_id: epoch.epoch,
});
Ok(())
})?;
let num_tranches = epoch
.tranches
.num_tranches()
.try_into()
.expect("MaxTranches is u32. qed.");
*epoch_info = None;
Ok(Some(T::WeightInfo::execute_epoch(
num_tranches,
T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top),
))
.into())
})
}
}
impl<T: Config> Pallet<T> {
pub(crate) fn current_block() -> BlockNumberFor<T> {
<frame_system::Pallet<T>>::block_number()
}
fn summarize_orders(
tranches: &TranchesOf<T>,
prices: &[T::BalanceRatio],
) -> Result<SummarizedOrders<T::Balance>, DispatchError> {
let mut acc_invest_orders = T::Balance::zero();
let mut acc_redeem_orders = T::Balance::zero();
let mut invest_orders = Vec::with_capacity(tranches.num_tranches());
let mut redeem_orders = Vec::with_capacity(tranches.num_tranches());
tranches.combine_with_residual_top(prices, |tranche, price| {
let invest_order = T::Investments::process_invest_orders(tranche.currency)?;
acc_invest_orders.ensure_add_assign(invest_order.amount)?;
invest_orders.push(invest_order.amount);
let redeem_order = T::Investments::process_redeem_orders(tranche.currency)?;
let redeem_amount_in_pool_currency = price.ensure_mul_int(redeem_order.amount)?;
acc_redeem_orders.ensure_add_assign(redeem_amount_in_pool_currency)?;
redeem_orders.push(redeem_amount_in_pool_currency);
Ok(())
})?;
Ok(SummarizedOrders {
acc_invest_orders,
acc_redeem_orders,
invest_orders,
redeem_orders,
})
}
pub fn score_solution(
pool_id: &PoolDetailsOf<T>,
epoch: &EpochExecutionInfoOf<T>,
solution: &[TrancheSolution],
) -> Result<EpochSolution<T::Balance, T::MaxTranches>, DispatchError> {
match Self::inspect_solution(pool_id, epoch, solution)? {
PoolState::Healthy => {
EpochSolution::score_solution_healthy(solution, &epoch.tranches)
}
PoolState::Unhealthy(states) => EpochSolution::score_solution_unhealthy(
solution,
&epoch.tranches,
pool_id.reserve.total,
pool_id.reserve.max,
&states,
),
}
.map_err(|_| Error::<T>::InvalidSolution.into())
}
pub(crate) fn inspect_solution(
pool: &PoolDetailsOf<T>,
epoch: &EpochExecutionInfoOf<T>,
solution: &[TrancheSolution],
) -> Result<PoolState, DispatchError> {
ensure!(
solution.len() == epoch.tranches.num_tranches(),
Error::<T>::InvalidSolution
);
let (acc_invest, acc_redeem, risk_buffers) = calculate_solution_parameters::<
_,
_,
T::Rate,
_,
T::TrancheCurrency,
T::MaxTranches,
>(&epoch.tranches, solution)
.map_err(|e| {
if e == DispatchError::Arithmetic(ArithmeticError::Underflow) {
Error::<T>::InsufficientCurrency
} else {
Error::<T>::InvalidSolution
}
})?;
let currency_available: T::Balance = acc_invest
.checked_add(&pool.reserve.total)
.ok_or(Error::<T>::InvalidSolution)?;
let new_reserve = currency_available
.checked_sub(&acc_redeem)
.ok_or(Error::<T>::InsufficientCurrency)?;
Self::validate_pool_constraints(
PoolState::Healthy,
new_reserve,
pool.reserve.max,
&pool.tranches.min_risk_buffers(),
&risk_buffers,
)
}
fn validate_pool_constraints(
mut state: PoolState,
reserve: T::Balance,
max_reserve: T::Balance,
min_risk_buffers: &[Perquintill],
risk_buffers: &[Perquintill],
) -> Result<PoolState, DispatchError> {
if reserve > max_reserve {
state.add_unhealthy(UnhealthyState::MaxReserveViolated);
}
for (risk_buffer, min_risk_buffer) in
risk_buffers.iter().rev().zip(min_risk_buffers.iter().rev())
{
if risk_buffer < min_risk_buffer {
state.add_unhealthy(UnhealthyState::MinRiskBufferViolated);
}
}
Ok(state)
}
pub(crate) fn do_update_pool(
pool_id: &T::PoolId,
changes: &PoolChangesOf<T>,
) -> DispatchResult {
Pool::<T>::try_mutate(pool_id, |pool| -> DispatchResult {
let pool = pool.as_mut().ok_or(Error::<T>::NoSuchPool)?;
let old_pool =
pool.essence_from_registry::<T::AssetRegistry, T::Balance, T::StringLimit>()?;
if let Change::NewValue(min_epoch_time) = changes.min_epoch_time {
pool.parameters.min_epoch_time = min_epoch_time;
}
if let Change::NewValue(max_nav_age) = changes.max_nav_age {
pool.parameters.max_nav_age = max_nav_age;
}
if let Change::NewValue(tranches) = &changes.tranches {
let now = T::Time::now();
pool.tranches.combine_with_mut_residual_top(
tranches.iter(),
|tranche, tranche_update| {
tranche.accrue(now)?;
tranche.tranche_type = tranche_update.tranche_type;
if let Some(new_seniority) = tranche_update.seniority {
tranche.seniority = new_seniority;
}
Ok(())
},
)?;
}
if let Change::NewValue(metadata) = &changes.tranche_metadata {
for (tranche, updated_metadata) in
pool.tranches.tranches.iter().zip(metadata.iter())
{
T::AssetRegistry::update_asset(
tranche.currency.into(),
None,
Some(updated_metadata.clone().token_name),
Some(updated_metadata.clone().token_symbol),
None,
None,
None,
)
.map_err(|_| Error::<T>::FailedToUpdateTrancheMetadata)?;
}
}
Self::deposit_event(Event::Updated {
id: *pool_id,
old: old_pool,
new: pool
.essence_from_registry::<T::AssetRegistry, T::Balance, T::StringLimit>()?,
});
ScheduledUpdate::<T>::remove(pool_id);
Ok(())
})
}
pub fn is_valid_tranche_change(
old_tranches: Option<&TranchesOf<T>>,
new_tranches: &[TrancheUpdate<T::Rate>],
) -> DispatchResult {
ensure!(
new_tranches.len() <= T::MaxTranches::get() as usize,
Error::<T>::TooManyTranches
);
let mut tranche_iter = new_tranches.iter();
let mut prev_tranche = tranche_iter
.next()
.ok_or(Error::<T>::InvalidJuniorTranche)?;
let max_seniority = new_tranches
.len()
.try_into()
.expect("MaxTranches is u32. qed.");
for tranche_input in tranche_iter {
ensure!(
prev_tranche
.tranche_type
.valid_next_tranche(&tranche_input.tranche_type),
Error::<T>::InvalidTrancheStructure
);
ensure!(
prev_tranche.seniority <= tranche_input.seniority
&& tranche_input.seniority <= Some(max_seniority),
Error::<T>::InvalidTrancheSeniority
);
prev_tranche = tranche_input;
}
if let Some(old_tranches) = old_tranches {
ensure!(
new_tranches.len() == old_tranches.num_tranches(),
Error::<T>::CannotAddOrRemoveTranches
);
}
Ok(())
}
fn do_execute_epoch(
pool_id: T::PoolId,
pool: &mut PoolDetailsOf<T>,
epoch: &EpochExecutionInfoOf<T>,
solution: &[TrancheSolution],
) -> DispatchResult {
T::OnEpochTransition::on_execution_pre_fulfillments(pool_id)?;
pool.reserve.deposit_from_epoch(&epoch.tranches, solution)?;
for (tranche, solution) in epoch.tranches.residual_top_slice().iter().zip(solution) {
T::Investments::invest_fulfillment(
tranche.currency,
FulfillmentWithPrice {
of_amount: solution.invest_fulfillment,
price: tranche.price,
},
)?;
T::Investments::redeem_fulfillment(
tranche.currency,
FulfillmentWithPrice {
of_amount: solution.redeem_fulfillment,
price: tranche.price,
},
)?;
}
pool.execute_previous_epoch()?;
let executed_amounts = epoch.tranches.fulfillment_cash_flows(solution)?;
let total_assets = epoch.nav.total(pool.reserve.total)?;
let tranche_ratios = {
let mut sum_non_residual_tranche_ratios = Perquintill::zero();
let num_tranches = pool.tranches.num_tranches();
let mut current_tranche = 1;
let mut ratios = epoch
.tranches
.combine_with_non_residual_top(
executed_amounts.rev(),
|tranche, &(invest, redeem)| {
let ratio = if total_assets.is_zero() {
Perquintill::zero()
} else if current_tranche < num_tranches {
Perquintill::from_rational(
tranche.supply.ensure_add(invest)?.ensure_sub(redeem)?,
total_assets,
)
} else {
Perquintill::one().ensure_sub(sum_non_residual_tranche_ratios)?
};
sum_non_residual_tranche_ratios.ensure_add_assign(ratio)?;
current_tranche.ensure_add_assign(1)?;
Ok(ratio)
},
)?;
ratios.reverse();
ratios
};
pool.tranches.rebalance_tranches(
T::Time::now(),
pool.reserve.total,
epoch.nav.nav_aum,
tranche_ratios.as_slice(),
&executed_amounts,
)?;
Self::deposit_event(Event::Rebalanced { pool_id });
Ok(())
}
pub(crate) fn do_deposit(
who: T::AccountId,
pool_id: T::PoolId,
amount: T::Balance,
) -> DispatchResult {
let pool_account = PoolLocator { pool_id }.into_account_truncating();
Pool::<T>::try_mutate(pool_id, |pool| {
let pool = pool.as_mut().ok_or(Error::<T>::NoSuchPool)?;
let now = T::Time::now();
pool.reserve.total.ensure_add_assign(amount)?;
let mut remaining_amount = amount;
for tranche in pool.tranches.non_residual_top_slice_mut() {
tranche.accrue(now)?;
let tranche_amount = if tranche.tranche_type != TrancheType::Residual {
let max_entitled_amount = tranche.ratio.mul_ceil(amount);
sp_std::cmp::min(max_entitled_amount, tranche.debt)
} else {
remaining_amount
};
tranche.debt = tranche.debt.saturating_sub(tranche_amount);
tranche.reserve.ensure_add_assign(tranche_amount)?;
remaining_amount.ensure_sub_assign(tranche_amount)?;
}
T::Tokens::transfer(
pool.currency,
&who,
&pool_account,
amount,
Preservation::Expendable,
)?;
Self::deposit_event(Event::Rebalanced { pool_id });
Ok(())
})
}
pub(crate) fn do_withdraw(
who: T::AccountId,
pool_id: T::PoolId,
amount: T::Balance,
) -> DispatchResult {
let pool_account = PoolLocator { pool_id }.into_account_truncating();
Pool::<T>::try_mutate(pool_id, |pool| {
let pool = pool.as_mut().ok_or(Error::<T>::NoSuchPool)?;
let now = T::Time::now();
pool.reserve.total = pool
.reserve
.total
.checked_sub(&amount)
.ok_or(TokenError::FundsUnavailable)?;
pool.reserve.available = pool
.reserve
.available
.checked_sub(&amount)
.ok_or(TokenError::FundsUnavailable)?;
let mut remaining_amount = amount;
for tranche in pool.tranches.non_residual_top_slice_mut() {
tranche.accrue(now)?;
let tranche_amount = if tranche.tranche_type != TrancheType::Residual {
tranche.ratio.mul_ceil(amount)
} else {
remaining_amount
};
let tranche_amount = if tranche_amount > tranche.reserve {
tranche.reserve
} else {
tranche_amount
};
tranche.reserve -= tranche_amount;
tranche.debt.ensure_add_assign(tranche_amount)?;
remaining_amount -= tranche_amount;
}
T::Tokens::transfer(
pool.currency,
&pool_account,
&who,
amount,
Preservation::Expendable,
)?;
Self::deposit_event(Event::Rebalanced { pool_id });
Ok(())
})
}
pub(crate) fn take_deposit(depositor: T::AccountId, pool: T::PoolId) -> DispatchResult {
let deposit = T::PoolDeposit::get();
T::Currency::reserve(&depositor, deposit)?;
AccountDeposit::<T>::mutate(&depositor, |total_deposit| {
*total_deposit += deposit;
});
PoolDeposit::<T>::insert(pool, PoolDepositOf::<T> { deposit, depositor });
Ok(())
}
}
}