diff --git a/libs/mocks/src/pools.rs b/libs/mocks/src/pools.rs index d1849b345b..bf604c29c5 100644 --- a/libs/mocks/src/pools.rs +++ b/libs/mocks/src/pools.rs @@ -155,6 +155,14 @@ pub mod pallet { execute_call!((a, b)) } + fn debit(a: Self::InvestmentId, b: &T::AccountId, c: Self::Amount) -> DispatchResult { + execute_call!((a, b, c)) + } + + fn credit(a: Self::InvestmentId, b: &T::AccountId, c: Self::Amount) -> DispatchResult { + execute_call!((a, b, c)) + } + fn transfer( a: Self::InvestmentId, b: &T::AccountId, diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index 2e9690b8ed..2e0e4cbfbd 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -193,6 +193,13 @@ pub trait InvestmentAccountant { amount: Self::Amount, ) -> Result<(), Self::Error>; + /// Debit a given amount from the account to the given accountant account + fn debit(id: Self::InvestmentId, from: &AccountId, amount: Self::Amount) -> DispatchResult; + + /// Credit a given amount from the accountant account to the given to + /// account + fn credit(id: Self::InvestmentId, to: &AccountId, amount: Self::Amount) -> DispatchResult; + /// Increases the existence of fn deposit( buyer: &AccountId, diff --git a/libs/traits/src/lib.rs b/libs/traits/src/lib.rs index 5860bbfb98..fd8ce1c4b9 100644 --- a/libs/traits/src/lib.rs +++ b/libs/traits/src/lib.rs @@ -322,6 +322,14 @@ pub trait StatusNotificationHook { fn notify_status_change(id: Self::Id, status: Self::Status) -> Result<(), Self::Error>; } +pub trait Reserve { + fn deposit(&mut self, amount: Balance) -> DispatchResult; + + fn withdraw(&mut self, amount: Balance) -> DispatchResult; + + fn total(&self) -> Balance; +} + /// Trait to signal an epoch transition. pub trait EpochTransitionHook { type Balance; @@ -333,7 +341,7 @@ pub trait EpochTransitionHook { fn on_closing_mutate_reserve( pool_id: Self::PoolId, assets_under_management: Self::Balance, - reserve: &mut Self::Balance, + reserve: &mut impl Reserve, ) -> Result<(), Self::Error>; /// Hook into the execution of an epoch before any investment and diff --git a/pallets/investments/src/lib.rs b/pallets/investments/src/lib.rs index f12a427d66..2ce18f852c 100644 --- a/pallets/investments/src/lib.rs +++ b/pallets/investments/src/lib.rs @@ -1173,6 +1173,13 @@ impl OrderManager for Pallet { }, )?; + // NOTE: The accountant already receives the current amount of investments + T::Accountant::debit( + investment_id, + &InvestmentAccount { investment_id }.into_account_truncating(), + total_orders.amount, + )?; + let order_id = InvestOrderId::::try_mutate( investment_id, |order_id| -> Result { @@ -1264,13 +1271,8 @@ impl OrderManager for Pallet { InvestmentAccount { investment_id }.into_account_truncating(); let info = T::Accountant::info(investment_id)?; - T::Tokens::transfer( - info.payment_currency, - &investment_account, - &info.owner, - invest_amount, - Preservation::Expendable, - )?; + // NOTE: The accountant gives back what he did not used for the investment + T::Accountant::credit(investment_id, &investment_account, remaining_invest_amount)?; // The amount of investments the accountant needs to // note newly in his books is the invest amount divide through @@ -1356,14 +1358,7 @@ impl OrderManager for Pallet { InvestmentAccount { investment_id }.into_account_truncating(); let info = T::Accountant::info(investment_id)?; - T::Tokens::transfer( - info.payment_currency, - &info.owner, - &investment_account, - redeem_amount_payment, - Preservation::Expendable, - )?; - + T::Accountant::credit(investment_id, &investment_account, redeem_amount_payment)?; T::Accountant::withdraw(&investment_account, info.id, redeem_amount)?; // The previous OrderId is always 1 away diff --git a/pallets/pool-fees/src/lib.rs b/pallets/pool-fees/src/lib.rs index 4cf182a03f..420fb1776c 100644 --- a/pallets/pool-fees/src/lib.rs +++ b/pallets/pool-fees/src/lib.rs @@ -35,7 +35,8 @@ pub mod pallet { use cfg_traits::{ changes::ChangeGuard, fee::{FeeAmountProration, PoolFeeBucket, PoolFeesInspect, PoolFeesMutate}, - EpochTransitionHook, PoolInspect, PoolNAV, PoolReserve, PreConditions, Seconds, TimeAsSecs, + EpochTransitionHook, PoolInspect, PoolNAV, PoolReserve, PreConditions, Reserve, Seconds, + TimeAsSecs, }; use cfg_types::{ pools::{ @@ -615,10 +616,10 @@ pub mod pallet { pub(crate) fn update_active_fees( pool_id: T::PoolId, bucket: PoolFeeBucket, - reserve: &mut T::Balance, + reserve: &mut impl Reserve, assets_under_management: T::Balance, epoch_duration: Seconds, - ) -> Result { + ) -> Result, DispatchError> { ActiveFees::::mutate(pool_id, bucket, |fees| { for fee in fees.iter_mut() { let limit = fee.amounts.limit(); @@ -648,8 +649,8 @@ pub mod pallet { .map_err(|e: DispatchError| e)?; // Disbursement amount is limited by reserve - let disbursement = fee_amount.min(*reserve); - reserve.ensure_sub_assign(disbursement)?; + let disbursement = fee_amount.min(reserve.total()); + reserve.withdraw(disbursement)?; // Update fee amounts fee.amounts.pending.ensure_sub_assign(disbursement)?; @@ -672,7 +673,7 @@ pub mod pallet { Ok::<(), DispatchError>(()) })?; - Ok(*reserve) + Ok(reserve) } /// Entirely remove a stored fee from the given pair of pool id and fee @@ -738,7 +739,7 @@ pub mod pallet { /// ``` pub fn update_portfolio_valuation_for_pool( pool_id: T::PoolId, - reserve: &mut T::Balance, + reserve: &mut impl Reserve, ) -> Result<(T::Balance, u32), DispatchError> { let fee_nav = PortfolioValuation::::get(pool_id); let aum = AssetsUnderManagement::::get(pool_id); @@ -859,17 +860,17 @@ pub mod pallet { fn on_closing_mutate_reserve( pool_id: Self::PoolId, assets_under_management: Self::Balance, - reserve: &mut Self::Balance, + reserve: &mut impl Reserve, ) -> Result<(), Self::Error> { // Determine pending fees and NAV based on last epoch's AUM - let res_pre_fees = *reserve; + let res_pre_fees = reserve.total(); Self::update_portfolio_valuation_for_pool(pool_id, reserve)?; // Set current AUM for next epoch's closing AssetsUnderManagement::::insert(pool_id, assets_under_management); // Transfer disbursement amount from pool account to pallet sovereign account - let total_fee_amount = res_pre_fees.saturating_sub(*reserve); + let total_fee_amount = res_pre_fees.saturating_sub(reserve.total()); if !total_fee_amount.is_zero() { let pool_currency = T::PoolReserve::currency_for(pool_id).ok_or(Error::::PoolNotFound)?; diff --git a/pallets/pool-system/src/impls.rs b/pallets/pool-system/src/impls.rs index cf80f26ec8..c15d92c262 100644 --- a/pallets/pool-system/src/impls.rs +++ b/pallets/pool-system/src/impls.rs @@ -360,6 +360,22 @@ impl InvestmentAccountant for Pallet { T::Tokens::transfer(id.into(), source, dest, amount, Preservation::Expendable).map(|_| ()) } + fn debit( + id: Self::InvestmentId, + from: &T::AccountId, + amount: Self::Amount, + ) -> sp_runtime::DispatchResult { + Self::do_debit(id.of_pool(), from, amount) + } + + fn credit( + id: Self::InvestmentId, + to: &T::AccountId, + amount: Self::Amount, + ) -> sp_runtime::DispatchResult { + Self::do_credit(id.of_pool(), to, amount) + } + fn deposit( buyer: &T::AccountId, id: Self::InvestmentId, diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index 097c3ac4d5..6262afe997 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -50,7 +50,7 @@ use sp_runtime::{ EnsureAddAssign, EnsureFixedPointNumber, EnsureSub, EnsureSubAssign, Get, One, Saturating, Zero, }, - DispatchError, FixedPointNumber, FixedPointOperand, Perquintill, TokenError, + DispatchError, FixedPointNumber, FixedPointOperand, Perquintill, }; use sp_std::{cmp::Ordering, vec::Vec}; use tranches::{ @@ -112,26 +112,13 @@ pub type TrancheOf = Tranche< >; /// Type alias to ease function signatures -pub type PoolDetailsOf = PoolDetails< - ::CurrencyId, - ::TrancheCurrency, - ::EpochId, - ::Balance, - ::Rate, - ::TrancheWeight, - ::TrancheId, - ::PoolId, - ::MaxTranches, ->; +pub type PoolDetailsOf = PoolDetails; /// Type alias for `struct EpochExecutionInfo` type EpochExecutionInfoOf = EpochExecutionInfo< ::Balance, - ::BalanceRatio, ::EpochId, - ::TrancheWeight, BlockNumberFor, - ::TrancheCurrency, ::MaxTranches, >; @@ -174,7 +161,7 @@ pub mod pallet { use cfg_traits::{ fee::{PoolFeeBucket, PoolFeesInspect, PoolFeesMutate}, investments::{OrderManager, TrancheCurrency as TrancheCurrencyT}, - EpochTransitionHook, PoolUpdateGuard, + EpochTransitionHook, PoolUpdateGuard, Reserve, }; use cfg_types::{ orders::{FulfillmentWithPrice, TotalOrder}, @@ -631,188 +618,86 @@ pub mod pallet { Error::::MinEpochTimeHasNotPassed ); - // Get positive NAV from AUM - let (nav_aum, aum_last_updated) = - T::AssetsUnderManagementNAV::nav(pool_id).ok_or(Error::::NoNAV)?; - ensure!( - now.saturating_sub(aum_last_updated) <= pool.parameters.max_nav_age, - Error::::NAVTooOld - ); + let nav = Self::get_nav(pool_id, |nav_aum| { + T::OnEpochTransition::on_closing_mutate_reserve( + pool_id, + nav_aum, + &mut pool.reserve, + ) + })?; - // Calculate fees to get negative NAV - T::OnEpochTransition::on_closing_mutate_reserve( - pool_id, - nav_aum, - &mut pool.reserve.total, + let prices = Self::calculate_prices( + &mut pool.tranches, + nav.total(pool.reserve.total()) + // NOTE: From an accounting perspective, erroring out would be correct. + // However, since investments of this epoch are included in the + // reserve only in the next epoch, every new pool with a configured + // fee is likely to be blocked if we threw an error here. Thus, we + // dispatch an event as a defensive workaround. + .map_err(|_| { + Self::deposit_event(Event::NegativeBalanceSheet { + pool_id, + nav_aum, + nav_fees, + reserve: pool.reserve.total(), + }); + }) + .unwrap_or(T::Balance::default()), )?; - let (nav_fees, fees_last_updated) = - T::PoolFeesNAV::nav(pool_id).ok_or(Error::::NoNAV)?; - ensure!( - now.saturating_sub(fees_last_updated) <= pool.parameters.max_nav_age, - Error::::NAVTooOld - ); - let nav = Nav::new(nav_aum, nav_fees); - let nav_total = nav - .total(pool.reserve.total) - // NOTE: From an accounting perspective, erroring out would be correct. However, - // since investments of this epoch are included in the reserve only in the next - // epoch, every new pool with a configured fee is likely to be blocked if we - // threw an error here. Thus, we dispatch an event as a defensive workaround. - .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; + let submission_period_epoch = pool.epoch.current; pool.start_next_epoch(now)?; - let epoch_tranche_prices = pool - .tranches - .calculate_prices::(nav_total, now)?; - - // If closing the epoch would wipe out a tranche, the close is invalid. - // TODO: This should instead put the pool into an error state - ensure!( - !epoch_tranche_prices - .iter() - .any(|price| *price == Zero::zero()), - Error::::WipedOut - ); - Self::deposit_event(Event::EpochClosed { pool_id, epoch_id: submission_period_epoch, }); - // 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| { - 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> = - 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) - }, - )?; + // Get the orders. + // + // NOTE: This will move the total amounts of investments into the reserve, + // making them available for originations. IF the pools does not + // fulfill the order 100%, the reserve is expected to be able + // to re-fund the investment side when executing the epoch. + let orders = Self::summarize_orders(&pool.tranches, &prices)?; let mut epoch = EpochExecutionInfo { - nav, + orders, epoch: submission_period_epoch, - tranches: EpochExecutionTranches::new(epoch_tranches), best_submission: None, challenge_period_end: None, + dirty: false, }; - let full_execution_solution = pool.tranches.combine_residual_top(|_| { + let no_execution_solution = pool.tranches.combine_residual_top(|_| { Ok(TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), + invest_fulfillment: Perquintill::zero(), + redeem_fulfillment: Perquintill::zero(), }) })?; - 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 { - // Any new submission needs to improve on the existing state (which is defined - // as a total fulfilment of 0%) - 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::::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()) - } + let existing_state_solution = + Self::score_solution(pool, &epoch, &no_execution_solution)?; + epoch.best_submission = Some(existing_state_solution); + EpochExecution::::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()) }) } - /// Submit a partial execution solution for a closed epoch + /// Submit a execution solution for a closed epoch /// /// If the submitted solution is "better" than the /// previous best solution, it will replace it. Solutions /// are ordered such that solutions which do not violate /// constraints are better than those that do. - /// - /// Once a valid solution has been submitted, the - /// challenge time begins. The pool can be executed once - /// the challenge time has expired. #[pallet::weight(T::WeightInfo::submit_solution( T::MaxTranches::get(), T::PoolFees::get_max_fees_per_bucket() @@ -823,7 +708,7 @@ pub mod pallet { pool_id: T::PoolId, solution: Vec, ) -> DispatchResultWithPostInfo { - ensure_signed(origin)?; + T::AdminOrigin::ensure_origin(origin, &pool_id)?; EpochExecution::::try_mutate(pool_id, |epoch| { let epoch = epoch.as_mut().ok_or(Error::::NotInSubmissionPeriod)?; @@ -838,12 +723,16 @@ pub mod pallet { } epoch.best_submission = Some(new_solution.clone()); + epoch.dirty = false; + // TODO: We might wanna re-enable that at some point WHEN we enable epoch + // closing and execution from all but the admin again + // // Challenge period starts when the first new solution has been submitted - if epoch.challenge_period_end.is_none() { - epoch.challenge_period_end = - Some(Self::current_block().saturating_add(T::ChallengeTime::get())); - } + // 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, @@ -886,39 +775,25 @@ pub mod pallet { .as_mut() .ok_or(Error::::NotInSubmissionPeriod)?; - ensure!( - epoch.best_submission.is_some(), - Error::::NoSolutionAvailable - ); + let solution = if let Some(best_submission) = &epoch.best_submission { + best_submission.solution() + } else { + return Err(Error::::NoSolutionAvailable); + }; - // The challenge period is some if we have submitted at least one valid - // solution since going into submission period. Hence, if it is none - // no solution beside the injected zero-solution is available. - ensure!( - epoch.challenge_period_end.is_some(), - Error::::NoSolutionAvailable - ); + if let Some(challenge_period_end) = epoch.challenge_period_end { + ensure!( + challenge_period_end <= Self::current_block(), + Error::::ChallengeTimeHasNotPassed + ); + } - ensure!( - epoch - .challenge_period_end - .expect("Challenge period is some. qed.") - <= Self::current_block(), - Error::::ChallengeTimeHasNotPassed - ); + ensure!(!epoch.dirty, Error::::InvalidSolution); - // TODO: Write a test for the `expect` in case we allow the removal of pools at - // some point Pool::::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(); + .map_err("EpochExecutionInfo can only exist on existing pools. qed.")?; Self::do_execute_epoch(pool_id, pool, epoch, solution)?; Self::deposit_event(Event::EpochExecuted { @@ -928,17 +803,14 @@ pub mod pallet { Ok(()) })?; - let num_tranches = epoch - .tranches - .num_tranches() - .try_into() - .expect("MaxTranches is u32. qed."); - - // This kills the epoch info in storage. - // See: https://github.com/paritytech/substrate/blob/bea8f32e7807233ab53045fe8214427e0f136230/frame/support/src/storage/generator/map.rs#L269-L284 *epoch_info = None; + Ok(Some(T::WeightInfo::execute_epoch( - num_tranches, + epoch + .tranches + .num_tranches() + .try_into() + .map_err("TryInto u32 failed. Never should happen. qed.")?, T::PoolFees::get_pool_fee_bucket_count(pool_id, PoolFeeBucket::Top), )) .into()) @@ -1014,7 +886,7 @@ pub mod pallet { solution: &[TrancheSolution], ) -> Result { ensure!( - solution.len() == epoch.tranches.num_tranches(), + solution.len() == pool.tranches.num_tranches(), Error::::InvalidSolution ); @@ -1038,7 +910,7 @@ pub mod pallet { })?; let currency_available: T::Balance = acc_invest - .checked_add(&pool.reserve.total) + .checked_add(&pool.reserve.total()) .ok_or(Error::::InvalidSolution)?; let new_reserve = currency_available @@ -1208,6 +1080,50 @@ pub mod pallet { Ok(()) } + fn get_nav( + pool_id: T::PoolId, + between: impl FnOnce(T::Balance) -> DispatchResult, + ) -> Result, DispatchError> { + let now = T::Time::now(); + + // Get positive NAV from AUM + let (nav_aum, aum_last_updated) = + T::AssetsUnderManagementNAV::nav(pool_id).ok_or(Error::::NoNAV)?; + ensure!( + now.saturating_sub(aum_last_updated) <= pool.parameters.max_nav_age, + Error::::NAVTooOld + ); + + between(nav_aum)?; + + let (nav_fees, fees_last_updated) = + T::PoolFeesNAV::nav(pool_id).ok_or(Error::::NoNAV)?; + + ensure!( + now.saturating_sub(fees_last_updated) <= pool.parameters.max_nav_age, + Error::::NAVTooOld + ); + + Ok(Nav::new(nav_aum, nav_fees)) + } + + fn calculate_prices( + tranches: &mut TranchesOf, + nav_total: T::Balance, + ) -> Result, DispatchError> { + let prices = tranches + .calculate_prices::(nav_total, T::Time::now())?; + + // If closing the epoch would wipe out a tranche, the close is invalid. + // TODO: This should instead put the pool into an error state + ensure!( + !prices.iter().any(|price| *price == Zero::zero()), + Error::::WipedOut + ); + + Ok(prices) + } + fn do_execute_epoch( pool_id: T::PoolId, pool: &mut PoolDetailsOf, @@ -1216,14 +1132,40 @@ pub mod pallet { ) -> DispatchResult { T::OnEpochTransition::on_execution_pre_fulfillments(pool_id)?; - pool.reserve.deposit_from_epoch(&epoch.tranches, solution)?; + let nav = Self::get_nav(pool_id, |_| Ok(()))?; + let prices = Self::calculate_prices( + &mut pool.tranches, + nav.total(pool.reserve.total()) + // NOTE: From an accounting perspective, erroring out would be correct. + // However, since investments of this epoch are included in the + // reserve only in the next epoch, every new pool with a configured + // fee is likely to be blocked if we threw an error here. Thus, we + // dispatch an event as a defensive workaround. + .map_err(|_| { + Self::deposit_event(Event::NegativeBalanceSheet { + pool_id, + nav_aum, + nav_fees, + reserve: pool.reserve.total(), + }); + }) + .unwrap_or(T::Balance::default()), + )?; - for (tranche, solution) in epoch.tranches.residual_top_slice().iter().zip(solution) { + // NOTE: Calling invest_fulfillment() will take care of re-transferring the + // unfulfilled funds from the reserve to the investment pallet. + for ((tranche, solution), price) in pool + .tranches + .residual_top_slice() + .iter() + .zip(solution) + .zip(prices) + { T::Investments::invest_fulfillment( tranche.currency, FulfillmentWithPrice { of_amount: solution.invest_fulfillment, - price: tranche.price, + price, }, )?; @@ -1238,26 +1180,38 @@ pub mod pallet { pool.execute_previous_epoch()?; - let executed_amounts = epoch.tranches.fulfillment_cash_flows(solution)?; - let total_assets = epoch.nav.total(pool.reserve.total)?; + let executed_amounts = + pool.tranches + .combine_with_residual_top(solution, |tranche, solution| { + Ok(( + solution.invest_fulfillment.mul_floor(tranche.invest), + solution.redeem_fulfillment.mul_floor(tranche.redeem), + )) + }); + let total_assets = nav.total(pool.reserve.total())?; + + let now = T::Time::now(); 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 + let mut ratios = pool .tranches // NOTE: Reversing amounts, as residual amount is on top. - .combine_with_non_residual_top( + .combine_with_mut_non_residual_top( executed_amounts.rev(), |tranche, &(invest, redeem)| { + // Update tranche debt + tranche.accrue(now)?; + // NOTE: Need to have this clause as the current Perquintill // implementation defaults to 100% if the denominator is zero 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)?, + tranche.balance().ensure_add(invest)?.ensure_sub(redeem)?, total_assets, ) } else { @@ -1279,9 +1233,9 @@ pub mod pallet { }; pool.tranches.rebalance_tranches( - T::Time::now(), - pool.reserve.total, - epoch.nav.nav_aum, + now, + pool.reserve.total(), + nav.aum(), tranche_ratios.as_slice(), &executed_amounts, )?; @@ -1301,7 +1255,7 @@ pub mod pallet { let pool = pool.as_mut().ok_or(Error::::NoSuchPool)?; let now = T::Time::now(); - pool.reserve.total.ensure_add_assign(amount)?; + Self::do_debit(pool_id, &who, amount)?; let mut remaining_amount = amount; for tranche in pool.tranches.non_residual_top_slice_mut() { @@ -1328,16 +1282,11 @@ pub mod pallet { // TODO: Add a debug log here and/or a debut_assert maybe even an error if // remaining_amount != 0 at this point! - T::Tokens::transfer( - pool.currency, - &who, - &pool_account, - amount, - Preservation::Expendable, - )?; Self::deposit_event(Event::Rebalanced { pool_id }); Ok(()) - }) + })?; + + Self::mark_dirty(pool_id) } pub(crate) fn do_withdraw( @@ -1350,16 +1299,7 @@ pub mod pallet { let pool = pool.as_mut().ok_or(Error::::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)?; + Self::do_credit(pool_id, &who, amount)?; let mut remaining_amount = amount; for tranche in pool.tranches.non_residual_top_slice_mut() { @@ -1383,14 +1323,66 @@ pub mod pallet { remaining_amount -= tranche_amount; } + Self::deposit_event(Event::Rebalanced { pool_id }); + + Ok(()) + })?; + + Self::mark_dirty(pool_id) + } + + pub(crate) fn do_credit( + pool_id: T::PoolId, + to: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + Pool::::try_mutate(pool_id, |details| { + let details = details.as_mut().ok_or(Error::::NoSuchPool)?; + T::Tokens::transfer( - pool.currency, - &pool_account, - &who, + details.currency, + &PoolLocator { pool_id }.into_account_truncating(), + to, amount, Preservation::Expendable, - )?; - Self::deposit_event(Event::Rebalanced { pool_id }); + ) + .map(|_| ())?; + + details.reserve.withdraw(amount) + })?; + + Self::mark_dirty(pool_id) + } + + pub(crate) fn do_debit( + pool_id: T::PoolId, + from: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + Pool::::try_mutate(pool_id, |details| { + let details = details.as_mut().ok_or(Error::::NoSuchPool)?; + + T::Tokens::transfer( + details.currency, + from, + &PoolLocator { pool_id }.into_account_truncating(), + amount, + Preservation::Expendable, + ) + .map(|_| ())?; + + details.reserve.deposit(amount) + })?; + + Self::mark_dirty(pool_id) + } + + fn mark_dirty(pool_id: T::PoolId) -> DispatchResult { + // Need to mark the solution as dirty, as the state has changed + EpochExecution::::try_mutate(pool_id, |epoch_info| { + if let Some(epoch) = epoch_info.as_mut() { + epoch.dirty = true; + } Ok(()) }) } diff --git a/pallets/pool-system/src/pool_types.rs b/pallets/pool-system/src/pool_types.rs index 0594d15ab2..ec2bcf10a4 100644 --- a/pallets/pool-system/src/pool_types.rs +++ b/pallets/pool-system/src/pool_types.rs @@ -10,7 +10,7 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use cfg_traits::Seconds; +use cfg_traits::{Reserve, Seconds}; use cfg_types::{epoch::EpochState, pools::TrancheMetadata}; pub use changes::PoolChangeProposal; use frame_support::{ @@ -29,8 +29,12 @@ use sp_runtime::{ }; use sp_std::{cmp::PartialEq, vec::Vec}; -use crate::tranches::{ - EpochExecutionTranches, TrancheEssence, TrancheInput, TrancheSolution, TrancheUpdate, Tranches, +use crate::{ + tranches::{ + EpochExecutionTranches, TrancheEssence, TrancheInput, TrancheSolution, TrancheUpdate, + Tranches, + }, + Config, Error, }; // The TypeId impl we derive pool-accounts from @@ -39,53 +43,37 @@ impl TypeId for PoolLocator { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct ReserveDetails { +pub struct ReserveDetails { /// Investments will be allowed up to this amount. - pub max: Balance, + pub max: T::Balance, /// Current total amount of currency in the pool reserve. - pub total: Balance, + total: T::Balance, /// Current reserve that is available for originations. - pub available: Balance, + available: T::Balance, } -impl ReserveDetails -where - Balance: AtLeast32BitUnsigned + Copy + From, -{ - /// Update the total balance of the reserve based on the provided solution - /// for in- and outflows of this epoc. - pub fn deposit_from_epoch( - &mut self, - epoch_tranches: &EpochExecutionTranches< - Balance, - BalanceRatio, - Weight, - TrancheCurrency, - MaxExecutionTranches, - >, - solution: &[TrancheSolution], - ) -> DispatchResult - where - Weight: Copy + From, - BalanceRatio: Copy, - MaxExecutionTranches: Get, - { - let executed_amounts = epoch_tranches.fulfillment_cash_flows(solution)?; - - // Update the total/available reserve for the new total value of the pool - let mut acc_investments = Balance::zero(); - let mut acc_redemptions = Balance::zero(); - for &(invest, redeem) in executed_amounts.iter() { - acc_investments.ensure_add_assign(invest)?; - acc_redemptions.ensure_add_assign(redeem)?; - } +impl Reserve for ReserveDetails { + fn deposit(&mut self, amount: T::Balance) -> DispatchResult { + self.total = self.total.ensure_add(amount)?; + self.available = self.available.ensure_add(amount)?; + Ok(()) + } + + fn withdraw(&mut self, amount: T::Balance) -> DispatchResult { self.total = self .total - .ensure_add(acc_investments)? - .ensure_sub(acc_redemptions)?; - + .ensure_sub(amount) + .map_err(Error::InsufficientCurrency)?; + self.available = self + .available + .ensure_sub(amount) + .map_err(Error::InsufficientCurrency)?; Ok(()) } + + fn total(&self) -> T::Balance { + self.total + } } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -106,34 +94,27 @@ pub struct PoolLocator { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct PoolDetails< - CurrencyId, - TrancheCurrency, - EpochId, - Balance, - Rate, - Weight, - TrancheId, - PoolId, - MaxTranches, -> where - Rate: FixedPointNumber, - Balance: FixedPointOperand + sp_arithmetic::MultiplyRational, - MaxTranches: Get, - TrancheCurrency: Into, -{ +pub struct PoolDetails { /// Currency that the pool is denominated in (immutable). - pub currency: CurrencyId, + pub currency: T::CurrencyId, /// List of tranches, ordered junior to senior. - pub tranches: Tranches, + pub tranches: Tranches< + T::Balance, + T::Rate, + T::TrancheWeight, + T::TrancheCurrency, + T::TrancheId, + T::PoolId, + T::MaxTranches, + >, /// Details about the parameters of the pool. pub parameters: PoolParameters, /// The status the pool is currently in. pub status: PoolStatus, /// Details about the epochs of the pool. - pub epoch: EpochState, + pub epoch: EpochState, /// Details about the reserve (unused capital) in the pool. - pub reserve: ReserveDetails, + pub reserve: ReserveDetails, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -226,16 +207,11 @@ where pub fn start_next_epoch(&mut self, now: Seconds) -> DispatchResult { self.epoch.current.ensure_add_assign(One::one())?; self.epoch.last_closed = now; - // TODO: Remove and set state rather to EpochClosing or similar - // Set available reserve to 0 to disable originations while the epoch is closed - // but not executed - self.reserve.available = Zero::zero(); Ok(()) } pub fn execute_previous_epoch(&mut self) -> DispatchResult { - self.reserve.available = self.reserve.total; self.epoch .last_executed .ensure_add_assign(One::one()) diff --git a/pallets/pool-system/src/solution.rs b/pallets/pool-system/src/solution.rs index a22a73656f..352847482d 100644 --- a/pallets/pool-system/src/solution.rs +++ b/pallets/pool-system/src/solution.rs @@ -112,8 +112,8 @@ where #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Nav { - pub nav_aum: Balance, - pub nav_fees: Balance, + nav_aum: Balance, + nav_fees: Balance, } impl Nav { @@ -127,27 +127,27 @@ impl Nav { .ensure_sub(self.nav_fees) .map_err(Into::into) } + + pub fn aum(&self) -> Balance { + self.nav_aum + } + + pub fn fees(&self) -> Balance { + self.nav_fees + } } /// The information for a currently executing epoch #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct EpochExecutionInfo< - Balance, - BalanceRatio, - EpochId, - Weight, - BlockNumber, - TrancheCurrency, - MaxTranches, -> where +pub struct EpochExecutionInfo +where MaxTranches: Get, { pub epoch: EpochId, - pub nav: Nav, - pub tranches: - EpochExecutionTranches, + pub orders: SummarizedOrders, pub best_submission: Option>, pub challenge_period_end: Option, + pub dirty: bool, } impl EpochSolution diff --git a/pallets/pool-system/src/tranches.rs b/pallets/pool-system/src/tranches.rs index 946977e587..79b4040aa1 100644 --- a/pallets/pool-system/src/tranches.rs +++ b/pallets/pool-system/src/tranches.rs @@ -978,8 +978,6 @@ where #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct EpochExecutionTranche { pub currency: TrancheCurrency, - pub supply: Balance, - pub price: BalanceRatio, pub invest: Balance, pub redeem: Balance, pub min_risk_buffer: Perquintill, diff --git a/runtime/integration-tests/submodules/liquidity-pools b/runtime/integration-tests/submodules/liquidity-pools index 6e8f1a29df..987cd7d0d5 160000 --- a/runtime/integration-tests/submodules/liquidity-pools +++ b/runtime/integration-tests/submodules/liquidity-pools @@ -1 +1 @@ -Subproject commit 6e8f1a29dff0d7cf5ff74285cfffadae8a8b303f +Subproject commit 987cd7d0d586e21b881dd47b0caabbbde591acb8