diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index a2ddb84015..dd3cb94dac 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -187,6 +187,7 @@ pub mod pallet { traits::{tokens::Preservation, Contains, EnsureOriginWithArg}, PalletId, }; + use rev_slice::SliceExt; use sp_runtime::{traits::BadOrigin, ArithmeticError}; use super::*; @@ -1245,7 +1246,11 @@ pub mod pallet { .map(|tranches| { tranches .iter() - .zip(&executed_amounts) + // NOTE: Reversing amounts, as residual amount is on top. + // NOTE: Iterator of executed amounts is one time larger than the + // non_residual_tranche-iterator, but we anyways push all reamaining + // ratio to the residual tranche. + .zip(executed_amounts.rev()) .map(|(tranche, &(invest, redeem))| { Ok(Perquintill::from_rational( tranche.supply.ensure_add(invest)?.ensure_sub(redeem)?, @@ -1260,8 +1265,13 @@ pub mod pallet { .iter() .try_fold(Perquintill::zero(), |acc, ratio| acc.ensure_add(*ratio))?; + // Pushing all remaining ratio to the residual tranche tranche_ratios.push(Perquintill::one().ensure_sub(non_residual_tranche_ratio_sum)?); + // NOTE: We need to reverse the ratios here, as the residual tranche is on top + // all the time + tranche_ratios.reverse(); + pool.tranches.rebalance_tranches( T::Time::now(), pool.reserve.total, diff --git a/pallets/pool-system/src/tests/mod.rs b/pallets/pool-system/src/tests/mod.rs index 077dd0e501..97889978e9 100644 --- a/pallets/pool-system/src/tests/mod.rs +++ b/pallets/pool-system/src/tests/mod.rs @@ -42,6 +42,8 @@ use crate::{ UnhealthyState, }; +mod ratios; + const AUSD_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1); pub mod util { diff --git a/pallets/pool-system/src/tests/ratios.rs b/pallets/pool-system/src/tests/ratios.rs new file mode 100644 index 0000000000..62a0284d8a --- /dev/null +++ b/pallets/pool-system/src/tests/ratios.rs @@ -0,0 +1,163 @@ +use sp_arithmetic::traits::EnsureSub; + +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// 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 super::*; + +#[test] +fn ensure_ratios_are_distributed_correctly() { + new_test_ext().execute_with(|| { + let pool_owner = DEFAULT_POOL_OWNER; + let pool_owner_origin = RuntimeOrigin::signed(pool_owner); + + // Initialize pool with initial investments + let senior_interest_rate = Rate::saturating_from_rational(10, 100) + / Rate::saturating_from_integer(SECONDS_PER_YEAR) + + One::one(); + + assert_ok!(PoolSystem::create( + pool_owner.clone(), + pool_owner.clone(), + 0, + vec![ + TrancheInput { + tranche_type: TrancheType::Residual, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + } + }, + TrancheInput { + tranche_type: TrancheType::NonResidual { + interest_rate_per_sec: senior_interest_rate, + min_risk_buffer: Perquintill::from_percent(10), + }, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + } + }, + ], + AUSD_CURRENCY_ID, + 10_000 * CURRENCY, + vec![], + )); + + /// Assert ratios are all zero + crate::Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(0)); + }); + + // Force min_epoch_time to 0 without using update + // as this breaks the runtime-defined pool + // parameter bounds and update will not allow this. + crate::Pool::::try_mutate(0, |maybe_pool| -> Result<(), ()> { + maybe_pool.as_mut().unwrap().parameters.min_epoch_time = 0; + maybe_pool.as_mut().unwrap().parameters.max_nav_age = u64::MAX; + Ok(()) + }) + .unwrap(); + + invest_close_and_collect( + 0, + vec![ + (0, JuniorTrancheId::get(), 500 * CURRENCY), + (0, SeniorTrancheId::get(), 500 * CURRENCY), + ], + ); + + // Ensure ratios are 50/50 + crate::Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(50)); + }); + + // Attempt to redeem half + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + 200 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + )); + + let new_residual_ratio = Perquintill::from_rational(5u64, 8u64); + let mut next_ratio = new_residual_ratio; + + // Ensure ratios are 500/800 and 300/800 + crate::Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, next_ratio); + next_ratio = Perquintill::one().ensure_sub(next_ratio).unwrap(); + }); + + // Attempt to redeem everything + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + 300 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + )); + + let new_residual_ratio = Perquintill::one(); + let mut next_ratio = new_residual_ratio; + + // Ensure ratios are 100/0 + crate::Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, next_ratio); + next_ratio = Perquintill::one().ensure_sub(next_ratio).unwrap(); + }); + + /// Ensure ratio goes up again + invest_close_and_collect(0, vec![(0, SeniorTrancheId::get(), 300 * CURRENCY)]); + let new_residual_ratio = Perquintill::from_rational(5u64, 8u64); + let mut next_ratio = new_residual_ratio; + + // Ensure ratios are 500/800 and 300/800 + crate::Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, next_ratio); + next_ratio = Perquintill::one().ensure_sub(next_ratio).unwrap(); + }); + }); +}