From 178fa2716e1339d2ca76d27806c9d9495512df5c Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Tue, 4 Jun 2024 12:05:27 +0200 Subject: [PATCH 1/9] fix: fix wrong ratio calculation --- pallets/pool-system/src/lib.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index cf4bba7567..a2ddb84015 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -1239,15 +1239,28 @@ pub mod pallet { let executed_amounts = epoch.tranches.fulfillment_cash_flows(solution)?; let total_assets = epoch.nav.total(pool.reserve.total)?; - let tranche_ratios = epoch.tranches.combine_with_residual_top( - &executed_amounts, - |tranche, &(invest, redeem)| { - Ok(Perquintill::from_rational( - tranche.supply.ensure_add(invest)?.ensure_sub(redeem)?, - total_assets, - )) - }, - )?; + let mut tranche_ratios = epoch + .tranches + .non_residual_tranches() + .map(|tranches| { + tranches + .iter() + .zip(&executed_amounts) + .map(|(tranche, &(invest, redeem))| { + Ok(Perquintill::from_rational( + tranche.supply.ensure_add(invest)?.ensure_sub(redeem)?, + total_assets, + )) + }) + .collect::, ArithmeticError>>() + }) + .unwrap_or(Ok(Vec::new()))?; + + let non_residual_tranche_ratio_sum = tranche_ratios + .iter() + .try_fold(Perquintill::zero(), |acc, ratio| acc.ensure_add(*ratio))?; + + tranche_ratios.push(Perquintill::one().ensure_sub(non_residual_tranche_ratio_sum)?); pool.tranches.rebalance_tranches( T::Time::now(), From 8596d6c1ce0f55f3b2a2889ae0a38122b03cda15 Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Tue, 4 Jun 2024 12:53:53 +0200 Subject: [PATCH 2/9] fix: wrong calculation and add test --- pallets/pool-system/src/lib.rs | 12 +- pallets/pool-system/src/tests/mod.rs | 2 + pallets/pool-system/src/tests/ratios.rs | 163 ++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 pallets/pool-system/src/tests/ratios.rs 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(); + }); + }); +} From 4bdfa0f6146e48bba774b6dc5c9d0fae95e0dbdc Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Tue, 4 Jun 2024 23:14:52 +0200 Subject: [PATCH 3/9] chore: address review comments --- pallets/pool-system/src/mock.rs | 4 +- pallets/pool-system/src/tests/mod.rs | 6 +-- pallets/pool-system/src/tests/ratios.rs | 55 +++++-------------------- 3 files changed, 15 insertions(+), 50 deletions(-) diff --git a/pallets/pool-system/src/mock.rs b/pallets/pool-system/src/mock.rs index 6955679adb..2562175e6c 100644 --- a/pallets/pool-system/src/mock.rs +++ b/pallets/pool-system/src/mock.rs @@ -27,9 +27,7 @@ use cfg_types::{ tokens::{CurrencyId, CustomMetadata, TrancheCurrency}, }; use frame_support::{ - assert_ok, derive_impl, - dispatch::RawOrigin, - parameter_types, + assert_ok, derive_impl, parameter_types, traits::{Contains, EnsureOriginWithArg, Hooks, PalletInfoAccess, SortedMembers}, Blake2_128, PalletId, StorageHasher, }; diff --git a/pallets/pool-system/src/tests/mod.rs b/pallets/pool-system/src/tests/mod.rs index 97889978e9..ea6f1f6777 100644 --- a/pallets/pool-system/src/tests/mod.rs +++ b/pallets/pool-system/src/tests/mod.rs @@ -75,7 +75,7 @@ pub mod util { TrancheInput { tranche_type: TrancheType::NonResidual { interest_rate_per_sec: Rate::default(), - min_risk_buffer: Perquintill::default(), + min_risk_buffer: Perquintill::from_percent(50), }, seniority: None, metadata: TrancheMetadata { @@ -85,7 +85,7 @@ pub mod util { }, ], AUSD_CURRENCY_ID, - 0, + 10_000 * CURRENCY, vec![], ) .unwrap(); @@ -97,7 +97,7 @@ pub mod util { // forcing to call `execute_epoch()` later. Investments::update_invest_order( RuntimeOrigin::signed(0), - TrancheCurrency::generate(0, JuniorTrancheId::get()), + TrancheCurrency::generate(0, SeniorTrancheId::get()), 500 * CURRENCY, ) .unwrap(); diff --git a/pallets/pool-system/src/tests/ratios.rs b/pallets/pool-system/src/tests/ratios.rs index 62a0284d8a..4af7654ddf 100644 --- a/pallets/pool-system/src/tests/ratios.rs +++ b/pallets/pool-system/src/tests/ratios.rs @@ -1,5 +1,3 @@ -use sp_arithmetic::traits::EnsureSub; - // Copyright 2021 Centrifuge Foundation (centrifuge.io). // // This file is part of the Centrifuge chain project. @@ -11,6 +9,8 @@ use sp_arithmetic::traits::EnsureSub; // 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 sp_arithmetic::traits::EnsureSub; + use super::*; #[test] @@ -19,42 +19,9 @@ fn ensure_ratios_are_distributed_correctly() { 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(); + util::default_pool::create(); - 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 + // Assert ratios are all zero crate::Pool::::get(0) .unwrap() .tranches @@ -67,7 +34,7 @@ fn ensure_ratios_are_distributed_correctly() { // 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<(), ()> { + 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(()) @@ -83,7 +50,7 @@ fn ensure_ratios_are_distributed_correctly() { ); // Ensure ratios are 50/50 - crate::Pool::::get(0) + Pool::::get(0) .unwrap() .tranches .residual_top_slice() @@ -92,7 +59,7 @@ fn ensure_ratios_are_distributed_correctly() { assert_eq!(tranche.ratio, Perquintill::from_percent(50)); }); - // Attempt to redeem half + // Attempt to redeem 40% assert_ok!(Investments::update_redeem_order( RuntimeOrigin::signed(0), TrancheCurrency::generate(0, SeniorTrancheId::get()), @@ -108,7 +75,7 @@ fn ensure_ratios_are_distributed_correctly() { let mut next_ratio = new_residual_ratio; // Ensure ratios are 500/800 and 300/800 - crate::Pool::::get(0) + Pool::::get(0) .unwrap() .tranches .residual_top_slice() @@ -134,7 +101,7 @@ fn ensure_ratios_are_distributed_correctly() { let mut next_ratio = new_residual_ratio; // Ensure ratios are 100/0 - crate::Pool::::get(0) + Pool::::get(0) .unwrap() .tranches .residual_top_slice() @@ -144,13 +111,13 @@ fn ensure_ratios_are_distributed_correctly() { next_ratio = Perquintill::one().ensure_sub(next_ratio).unwrap(); }); - /// Ensure ratio goes up again + // 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) + Pool::::get(0) .unwrap() .tranches .residual_top_slice() From 740d032512dd07590601007afa54e9e8e0b9faac Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Tue, 4 Jun 2024 23:25:06 +0200 Subject: [PATCH 4/9] fix: tests failing due to missing import --- pallets/pool-system/src/mock.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pallets/pool-system/src/mock.rs b/pallets/pool-system/src/mock.rs index 2562175e6c..24d2495815 100644 --- a/pallets/pool-system/src/mock.rs +++ b/pallets/pool-system/src/mock.rs @@ -26,6 +26,8 @@ use cfg_types::{ time::TimeProvider, tokens::{CurrencyId, CustomMetadata, TrancheCurrency}, }; +#[cfg(feature = "runtime-benchmarks")] +use frame_support::dispatch::RawOrigin; use frame_support::{ assert_ok, derive_impl, parameter_types, traits::{Contains, EnsureOriginWithArg, Hooks, PalletInfoAccess, SortedMembers}, From 7886f40345db4e438b72ab5182b2e7fa2df1394f Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Wed, 5 Jun 2024 09:16:56 +0200 Subject: [PATCH 5/9] chore: make cleaner? --- pallets/pool-system/src/lib.rs | 58 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index dd3cb94dac..f43893fa8e 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -1240,37 +1240,39 @@ pub mod pallet { let executed_amounts = epoch.tranches.fulfillment_cash_flows(solution)?; let total_assets = epoch.nav.total(pool.reserve.total)?; - let mut tranche_ratios = epoch - .tranches - .non_residual_tranches() - .map(|tranches| { - tranches - .iter() - // 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)?, - total_assets, - )) - }) - .collect::, ArithmeticError>>() - }) - .unwrap_or(Ok(Vec::new()))?; - let non_residual_tranche_ratio_sum = tranche_ratios - .iter() - .try_fold(Perquintill::zero(), |acc, ratio| acc.ensure_add(*ratio))?; + 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 + // NOTE: Reversing amounts, as residual amount is on top. + .combine_with_non_residual_top( + executed_amounts.rev(), + |tranche, &(invest, redeem)| { + let ratio = 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) + }, + )?; - // 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 + ratios.reverse(); - // NOTE: We need to reverse the ratios here, as the residual tranche is on top - // all the time - tranche_ratios.reverse(); + ratios + }; pool.tranches.rebalance_tranches( T::Time::now(), From 417c6720ab15688db2e1929f7d33c9980311d981 Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Wed, 5 Jun 2024 11:12:46 +0200 Subject: [PATCH 6/9] fix: Perquintill default to 1, 1 tranche test --- pallets/pool-system/src/lib.rs | 19 +++-- pallets/pool-system/src/mock.rs | 1 + pallets/pool-system/src/tests/mod.rs | 96 +++++++++++++++++----- pallets/pool-system/src/tests/ratios.rs | 103 +++++++++++++++++++++++- pallets/pool-system/src/tranches.rs | 6 +- 5 files changed, 195 insertions(+), 30 deletions(-) diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index f43893fa8e..e3fae3a253 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -1251,13 +1251,20 @@ pub mod pallet { .combine_with_non_residual_top( executed_amounts.rev(), |tranche, &(invest, redeem)| { - let ratio = if current_tranche < num_tranches { - Perquintill::from_rational( - tranche.supply.ensure_add(invest)?.ensure_sub(redeem)?, - total_assets, - ) + // 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 { - Perquintill::one().ensure_sub(sum_non_residual_tranche_ratios)? + 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)?; diff --git a/pallets/pool-system/src/mock.rs b/pallets/pool-system/src/mock.rs index 24d2495815..53748c57e1 100644 --- a/pallets/pool-system/src/mock.rs +++ b/pallets/pool-system/src/mock.rs @@ -506,6 +506,7 @@ fn create_tranche_id(pool: u64, tranche: u64) -> [u8; 16] { parameter_types! { pub JuniorTrancheId: [u8; 16] = create_tranche_id(0, 0); pub SeniorTrancheId: [u8; 16] = create_tranche_id(0, 1); + pub SecondSeniorTrancheId: [u8; 16] = create_tranche_id(0, 2); } // Build genesis storage according to the mock runtime. diff --git a/pallets/pool-system/src/tests/mod.rs b/pallets/pool-system/src/tests/mod.rs index ea6f1f6777..2cdefa2c23 100644 --- a/pallets/pool-system/src/tests/mod.rs +++ b/pallets/pool-system/src/tests/mod.rs @@ -58,32 +58,86 @@ pub mod util { pub mod default_pool { use super::*; + pub fn one_tranche_input() -> Vec> { + vec![TrancheInput { + tranche_type: TrancheType::Residual, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + }, + }] + } + + pub fn three_tranche_input() -> Vec> { + 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: Rate::default(), + min_risk_buffer: Perquintill::from_percent(50), + }, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + }, + }, + TrancheInput { + tranche_type: TrancheType::NonResidual { + interest_rate_per_sec: Rate::default(), + min_risk_buffer: Perquintill::from_percent(50), + }, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + }, + }, + ] + } + + pub fn two_tranche_input() -> Vec> { + 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: Rate::default(), + min_risk_buffer: Perquintill::from_percent(50), + }, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + }, + }, + ] + } + pub fn create() { + create_with_tranche_input(two_tranche_input()) + } + + pub fn create_with_tranche_input(input: Vec>) { PoolSystem::create( DEFAULT_POOL_OWNER, DEFAULT_POOL_OWNER, DEFAULT_POOL_ID, - 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: Rate::default(), - min_risk_buffer: Perquintill::from_percent(50), - }, - seniority: None, - metadata: TrancheMetadata { - token_name: BoundedVec::default(), - token_symbol: BoundedVec::default(), - }, - }, - ], + input, AUSD_CURRENCY_ID, 10_000 * CURRENCY, vec![], diff --git a/pallets/pool-system/src/tests/ratios.rs b/pallets/pool-system/src/tests/ratios.rs index 4af7654ddf..776a2813dc 100644 --- a/pallets/pool-system/src/tests/ratios.rs +++ b/pallets/pool-system/src/tests/ratios.rs @@ -14,7 +14,7 @@ use sp_arithmetic::traits::EnsureSub; use super::*; #[test] -fn ensure_ratios_are_distributed_correctly() { +fn ensure_ratios_are_distributed_correctly_2_tranches() { new_test_ext().execute_with(|| { let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); @@ -22,7 +22,7 @@ fn ensure_ratios_are_distributed_correctly() { util::default_pool::create(); // Assert ratios are all zero - crate::Pool::::get(0) + Pool::::get(0) .unwrap() .tranches .residual_top_slice() @@ -128,3 +128,102 @@ fn ensure_ratios_are_distributed_correctly() { }); }); } + +#[test] +fn ensure_ratios_are_distributed_correctly_1_tranche() { + new_test_ext().execute_with(|| { + let pool_owner = DEFAULT_POOL_OWNER; + let pool_owner_origin = RuntimeOrigin::signed(pool_owner); + + util::default_pool::create_with_tranche_input(util::default_pool::one_tranche_input()); + + // Assert ratios are all zero + 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. + 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)]); + + // Ensure ratios are 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(100)); + }); + + // Attempt to redeem 40% + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + 200 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + )); + + // Ensure ratio is 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(100)); + }); + + // Attempt to redeem everything + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + 300 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + )); + + // Ensure ratio is 0 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(0)); + }); + + // Ensure ratio goes up again + invest_close_and_collect(0, vec![(0, JuniorTrancheId::get(), 300 * CURRENCY)]); + + // Ensure ratio 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::from_percent(100)); + }); + }); +} diff --git a/pallets/pool-system/src/tranches.rs b/pallets/pool-system/src/tranches.rs index ff285ecc02..ca9b89f6c1 100644 --- a/pallets/pool-system/src/tranches.rs +++ b/pallets/pool-system/src/tranches.rs @@ -1352,7 +1352,11 @@ where .map(|tranche_value| { remaining_subordinate_value = remaining_subordinate_value.saturating_sub(*tranche_value); - Perquintill::from_rational(remaining_subordinate_value, pool_value) + if pool_value.is_zero() { + Perquintill::zero() + } else { + Perquintill::from_rational(remaining_subordinate_value, pool_value) + } }) .collect::>(); From 0c0cb614316fa2a324250bf3c757d3b068887975 Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Wed, 5 Jun 2024 14:30:27 +0200 Subject: [PATCH 7/9] fix: last tests and risk buffer calc --- pallets/pool-system/src/tests/mod.rs | 102 +++++++------- pallets/pool-system/src/tests/ratios.rs | 177 +++++++++++++++++++++++- pallets/pool-system/src/tranches.rs | 9 +- 3 files changed, 232 insertions(+), 56 deletions(-) diff --git a/pallets/pool-system/src/tests/mod.rs b/pallets/pool-system/src/tests/mod.rs index 2cdefa2c23..7054e0a7ae 100644 --- a/pallets/pool-system/src/tests/mod.rs +++ b/pallets/pool-system/src/tests/mod.rs @@ -82,7 +82,7 @@ pub mod util { TrancheInput { tranche_type: TrancheType::NonResidual { interest_rate_per_sec: Rate::default(), - min_risk_buffer: Perquintill::from_percent(50), + min_risk_buffer: Perquintill::from_percent(20), }, seniority: None, metadata: TrancheMetadata { @@ -93,7 +93,7 @@ pub mod util { TrancheInput { tranche_type: TrancheType::NonResidual { interest_rate_per_sec: Rate::default(), - min_risk_buffer: Perquintill::from_percent(50), + min_risk_buffer: Perquintill::from_percent(25), }, seniority: None, metadata: TrancheMetadata { @@ -117,7 +117,7 @@ pub mod util { TrancheInput { tranche_type: TrancheType::NonResidual { interest_rate_per_sec: Rate::default(), - min_risk_buffer: Perquintill::from_percent(50), + min_risk_buffer: Perquintill::from_percent(25), }, seniority: None, metadata: TrancheMetadata { @@ -874,63 +874,72 @@ fn submission_period() { // Not allowed as it breaks the min risk buffer, and the current state isn't // broken let epoch = >::try_get(0).unwrap(); + assert_err!( + PoolSystem::submit_solution( + pool_owner_origin.clone(), + 0, + vec![ + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::one(), + }, + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::one(), + } + ] + ), + Error::::InvalidSolution + ); + let existing_state_score = PoolSystem::score_solution( &crate::Pool::::try_get(0).unwrap(), &epoch, &epoch.clone().best_submission.unwrap().solution(), ) .unwrap(); + + let new_solution = vec![ + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::from_rational(9u64, 10), + }, + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::one(), + }, + ]; + let new_solution_score = PoolSystem::score_solution( &crate::Pool::::try_get(0).unwrap(), &epoch, - &vec![ - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - }, - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - }, - ], + &new_solution, ) .unwrap(); assert_eq!(existing_state_score.healthy(), true); assert_eq!(new_solution_score.healthy(), false); assert_eq!(new_solution_score < existing_state_score, true); + // Is error as would put pool in unhealthy state assert_err!( - PoolSystem::submit_solution( - pool_owner_origin.clone(), - 0, - vec![ - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - }, - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - } - ] - ), + PoolSystem::submit_solution(pool_owner_origin.clone(), 0, new_solution,), Error::::NotNewBestSubmission ); - // Allowed as 1% redemption keeps the risk buffer healthy + let new_solution = vec![ + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::from_rational(7u64, 10), + }, + TrancheSolution { + invest_fulfillment: Perquintill::one(), + redeem_fulfillment: Perquintill::one(), + }, + ]; let partial_fulfilment_solution = PoolSystem::score_solution( &crate::Pool::::try_get(0).unwrap(), &epoch, - &vec![ - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::from_float(0.01), - }, - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - }, - ], + &new_solution, ) .unwrap(); assert_eq!(partial_fulfilment_solution.healthy(), true); @@ -939,16 +948,7 @@ fn submission_period() { assert_ok!(PoolSystem::submit_solution( pool_owner_origin.clone(), 0, - vec![ - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::from_float(0.01), - }, - TrancheSolution { - invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::one(), - } - ] + new_solution )); // Can submit the same solution twice @@ -958,7 +958,7 @@ fn submission_period() { vec![ TrancheSolution { invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::from_float(0.01), + redeem_fulfillment: Perquintill::from_rational(7u64, 10), }, TrancheSolution { invest_fulfillment: Perquintill::one(), @@ -967,14 +967,14 @@ fn submission_period() { ] )); - // Slight risk buffer improvement + // Risk buffer not touched, so increase in redemption is better assert_ok!(PoolSystem::submit_solution( pool_owner_origin.clone(), 0, vec![ TrancheSolution { invest_fulfillment: Perquintill::one(), - redeem_fulfillment: Perquintill::from_float(0.10), + redeem_fulfillment: Perquintill::from_rational(8u64, 10), }, TrancheSolution { invest_fulfillment: Perquintill::one(), diff --git a/pallets/pool-system/src/tests/ratios.rs b/pallets/pool-system/src/tests/ratios.rs index 776a2813dc..392379bbb3 100644 --- a/pallets/pool-system/src/tests/ratios.rs +++ b/pallets/pool-system/src/tests/ratios.rs @@ -9,7 +9,7 @@ // 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 sp_arithmetic::traits::EnsureSub; +use sp_arithmetic::{traits::EnsureSub, PerThing, Rounding}; use super::*; @@ -227,3 +227,178 @@ fn ensure_ratios_are_distributed_correctly_1_tranche() { }); }); } + +#[test] +fn ensure_ratios_are_distributed_correctly_3_tranches() { + new_test_ext().execute_with(|| { + let pool_owner = DEFAULT_POOL_OWNER; + let pool_owner_origin = RuntimeOrigin::signed(pool_owner); + + util::default_pool::create_with_tranche_input(util::default_pool::three_tranche_input()); + + // Assert ratios are all zero + 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. + 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), + (0, SecondSeniorTrancheId::get(), 500 * CURRENCY), + ], + ); + + let check_ratios = [ + Perquintill::from_rational_with_rounding(1u64, 3, Rounding::Up).unwrap(), + Perquintill::from_rational(1u64, 3), + Perquintill::from_rational(1u64, 3), + ]; + + // Ensure ratios are 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .zip(check_ratios.into_iter()) + .for_each(|(tranche, check)| { + assert_eq!(tranche.ratio, check); + }); + + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + 250 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + )); + + let check_ratios = [ + Perquintill::from_rational(1u64, 5), + Perquintill::from_rational(2u64, 5), + ]; + + // Ensure ratios are 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .zip(check_ratios.into_iter()) + .for_each(|(tranche, check)| { + assert_eq!(tranche.ratio, check); + }); + + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SecondSeniorTrancheId::get()), + 250 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SecondSeniorTrancheId::get()), + )); + + let check_ratios = [ + Perquintill::from_rational(1u64, 4), + Perquintill::from_rational(2u64, 4), + Perquintill::from_rational(1u64, 4), + ]; + + // Ensure ratios are 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .zip(check_ratios.into_iter()) + .for_each(|(tranche, check)| { + assert_eq!(tranche.ratio, check); + }); + + // Ensure ratio goes up again + invest_close_and_collect(0, vec![(0, JuniorTrancheId::get(), 250 * CURRENCY)]); + + let check_ratios = [ + Perquintill::from_rational(2u64, 5), + Perquintill::from_rational(2u64, 5), + Perquintill::from_rational(1u64, 5), + ]; + + // Ensure ratios are 100 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .zip(check_ratios.into_iter()) + .for_each(|(tranche, check)| { + assert_eq!(tranche.ratio, check); + }); + + // Redeem everything + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SecondSeniorTrancheId::get()), + 250 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SecondSeniorTrancheId::get()), + )); + + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + 500 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, SeniorTrancheId::get()), + )); + + assert_ok!(Investments::update_redeem_order( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + 500 * CURRENCY + )); + assert_ok!(PoolSystem::close_epoch(pool_owner_origin.clone(), 0)); + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(0), + TrancheCurrency::generate(0, JuniorTrancheId::get()), + )); + + // Ensure ratios are 0 + Pool::::get(0) + .unwrap() + .tranches + .residual_top_slice() + .iter() + .for_each(|tranche| { + assert_eq!(tranche.ratio, Perquintill::zero()); + }); + }); +} diff --git a/pallets/pool-system/src/tranches.rs b/pallets/pool-system/src/tranches.rs index ca9b89f6c1..2cd337bf0b 100644 --- a/pallets/pool-system/src/tranches.rs +++ b/pallets/pool-system/src/tranches.rs @@ -1347,12 +1347,13 @@ where // previous more senior tranche - this tranche value. let mut remaining_subordinate_value = pool_value; let mut risk_buffers: Vec = tranche_values - .iter() + .into_iter() .rev() .map(|tranche_value| { - remaining_subordinate_value = - remaining_subordinate_value.saturating_sub(*tranche_value); - if pool_value.is_zero() { + remaining_subordinate_value = remaining_subordinate_value.saturating_sub(tranche_value); + if tranche_value.is_zero() { + Perquintill::one() + } else if pool_value.is_zero() { Perquintill::zero() } else { Perquintill::from_rational(remaining_subordinate_value, pool_value) From 4e7c3fd966df21494570799f1dffe43d51701315 Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Wed, 5 Jun 2024 17:38:30 +0200 Subject: [PATCH 8/9] fix: test missing rate check --- pallets/pool-system/src/tests/ratios.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pallets/pool-system/src/tests/ratios.rs b/pallets/pool-system/src/tests/ratios.rs index 392379bbb3..0e90611b19 100644 --- a/pallets/pool-system/src/tests/ratios.rs +++ b/pallets/pool-system/src/tests/ratios.rs @@ -296,6 +296,7 @@ fn ensure_ratios_are_distributed_correctly_3_tranches() { let check_ratios = [ Perquintill::from_rational(1u64, 5), Perquintill::from_rational(2u64, 5), + Perquintill::from_rational(2u64, 5), ]; // Ensure ratios are 100 From d17501033e60ee14eda2715bbb3b2b84a328b0d9 Mon Sep 17 00:00:00 2001 From: Frederik Gartenmeister Date: Thu, 6 Jun 2024 09:45:23 +0200 Subject: [PATCH 9/9] fix: clippy v1 --- pallets/pool-system/src/lib.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index e3fae3a253..097c3ac4d5 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -1255,16 +1255,13 @@ pub mod pallet { // 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)?, + total_assets, + ) } 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)? - } + Perquintill::one().ensure_sub(sum_non_residual_tranche_ratios)? }; sum_non_residual_tranche_ratios.ensure_add_assign(ratio)?;