diff --git a/Cargo.lock b/Cargo.lock index 89fa577626..3b99ac1256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11386,6 +11386,7 @@ dependencies = [ "log", "orml-asset-registry", "orml-oracle", + "orml-tokens", "orml-traits", "pallet-anchors", "pallet-authorship", @@ -11407,6 +11408,7 @@ dependencies = [ "pallet-liquidity-pools-gateway", "pallet-loans", "pallet-pool-system", + "pallet-restricted-tokens", "pallet-treasury", "parachain-info", "parity-scale-codec 3.6.5", diff --git a/libs/primitives/src/lib.rs b/libs/primitives/src/lib.rs index b7e5be88ba..2c4d027c83 100644 --- a/libs/primitives/src/lib.rs +++ b/libs/primitives/src/lib.rs @@ -222,7 +222,7 @@ pub mod constants { /// We allow for 0.5 seconds of compute with a 6 second average block time. pub const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0) .saturating_div(2) - .set_proof_size(MAX_POV_SIZE as u64); + .set_proof_size(MAX_POV_SIZE); pub const MICRO_CFG: Balance = 1_000_000_000_000; // 10−6 0.000001 pub const MILLI_CFG: Balance = 1_000 * MICRO_CFG; // 10−3 0.001 diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index 905966de61..5bab8c4a57 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -219,26 +219,6 @@ pub trait InvestmentAccountant { ) -> Result<(), Self::Error>; } -/// Trait to handle Investment Portfolios for accounts -pub trait InvestmentsPortfolio { - type InvestmentId; - type CurrencyId; - type Balance; - type Error; - type AccountInvestmentPortfolio; - - /// Get the payment currency for an investment. - fn get_investment_currency_id( - investment_id: Self::InvestmentId, - ) -> Result; - - /// Get the investments and associated payment currencies and balances for - /// an account. - fn get_account_investments_currency( - who: &Account, - ) -> Result; -} - /// Trait to handle investments in (presumably) foreign currencies, i.e., other /// currencies than the pool currency. /// diff --git a/libs/types/src/fixed_point.rs b/libs/types/src/fixed_point.rs index 377232128a..96eab4e55c 100644 --- a/libs/types/src/fixed_point.rs +++ b/libs/types/src/fixed_point.rs @@ -550,7 +550,7 @@ impl FixedPointNumberExtension for FixedU128
{ multiply_by_rational_with_rounding( lhs.value, - Self::DIV as u128, + Self::DIV, rhs.value, Rounding::from_signed(r, negative), ) diff --git a/libs/types/src/investments.rs b/libs/types/src/investments.rs index 57b127d782..85a3b1b42e 100644 --- a/libs/types/src/investments.rs +++ b/libs/types/src/investments.rs @@ -239,3 +239,69 @@ pub struct ExecutedForeignCollect { /// pool currency) pub amount_remaining: Balance, } + +/// A representation of an investment portfolio consisting of free, pending and +/// claimable pool currency as well as tranche tokens. +#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + +pub struct InvestmentPortfolio { + /// The identifier of the pool currency + pub pool_currency_id: CurrencyId, + /// The unprocessed invest order amount in pool currency + pub pending_invest_currency: Balance, + /// The amount of tranche tokens which can be collected for an invest order + pub claimable_tranche_tokens: Balance, + /// The amount of tranche tokens which can be transferred + pub free_tranche_tokens: Balance, + /// The amount of tranche tokens which can not be used at all and could get + /// slashed + pub reserved_tranche_tokens: Balance, + /// The unprocessed redeem order amount in tranche tokens + pub pending_redeem_tranche_tokens: Balance, + /// The amount of pool currency which can be collected for a redeem order + pub claimable_currency: Balance, +} + +impl InvestmentPortfolio { + pub fn new(pool_currency_id: CurrencyId) -> Self { + Self { + pool_currency_id, + pending_invest_currency: Balance::default(), + claimable_tranche_tokens: Balance::default(), + free_tranche_tokens: Balance::default(), + reserved_tranche_tokens: Balance::default(), + pending_redeem_tranche_tokens: Balance::default(), + claimable_currency: Balance::default(), + } + } + + pub fn with_pending_invest_currency(mut self, amount: Balance) -> Self { + self.pending_invest_currency = amount; + self + } + + pub fn with_free_tranche_tokens(mut self, amount: Balance) -> Self { + self.free_tranche_tokens = amount; + self + } + + pub fn with_reserved_tranche_tokens(mut self, amount: Balance) -> Self { + self.reserved_tranche_tokens = amount; + self + } + + pub fn with_claimable_tranche_tokens(mut self, amount: Balance) -> Self { + self.claimable_tranche_tokens = amount; + self + } + + pub fn with_pending_redeem_tranche_tokens(mut self, amount: Balance) -> Self { + self.pending_redeem_tranche_tokens = amount; + self + } + + pub fn with_claimable_currency(mut self, amount: Balance) -> Self { + self.claimable_currency = amount; + self + } +} diff --git a/pallets/investments/src/lib.rs b/pallets/investments/src/lib.rs index 99bbd87fed..5f606f8a1b 100644 --- a/pallets/investments/src/lib.rs +++ b/pallets/investments/src/lib.rs @@ -15,9 +15,7 @@ use cfg_primitives::OrderId; use cfg_traits::{ - investments::{ - Investment, InvestmentAccountant, InvestmentCollector, InvestmentsPortfolio, OrderManager, - }, + investments::{Investment, InvestmentAccountant, InvestmentCollector, OrderManager}, PreConditions, StatusNotificationHook, }; use cfg_types::{ @@ -47,6 +45,7 @@ use sp_std::{ convert::TryInto, vec::Vec, }; + pub mod weights; pub use weights::WeightInfo; @@ -60,12 +59,6 @@ mod tests; type CurrencyOf = <::Tokens as Inspect<::AccountId>>::AssetId; -type AccountInvestmentPortfolioOf = Vec<( - ::InvestmentId, - CurrencyOf, - ::Amount, -)>; - /// The enum we parse to `PreConditions` so the runtime /// can make an educated decision about this investment #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] @@ -1100,37 +1093,6 @@ impl Pallet { } } -impl InvestmentsPortfolio for Pallet { - type AccountInvestmentPortfolio = AccountInvestmentPortfolioOf; - type Balance = T::Amount; - type CurrencyId = CurrencyOf; - type Error = DispatchError; - type InvestmentId = T::InvestmentId; - - /// Get the payment currency for an investment. - fn get_investment_currency_id( - investment_id: T::InvestmentId, - ) -> Result, DispatchError> { - let info = T::Accountant::info(investment_id).map_err(|_| Error::::UnknownInvestment)?; - Ok(info.payment_currency) - } - - /// Get the investments and associated payment currencies and balances for - /// an account. - fn get_account_investments_currency( - who: &T::AccountId, - ) -> Result { - let mut investments_currency: Vec<(Self::InvestmentId, Self::CurrencyId, Self::Balance)> = - Vec::new(); - >::iter_key_prefix(who).try_for_each(|i| { - let currency = Self::get_investment_currency_id(i)?; - let balance = T::Accountant::balance(i, who); - investments_currency.push((i, currency, balance)); - Ok::<(), DispatchError>(()) - })?; - Ok(investments_currency) - } -} impl Investment for Pallet { type Amount = T::Amount; type CurrencyId = CurrencyOf; diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 2e1ab06714..d8e0b62f98 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -31,6 +31,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, oracles::OracleKey, permissions::{PermissionRoles, PermissionScope, PermissionedCurrencyRole, PoolRole, Role}, time::TimeProvider, @@ -2164,9 +2165,9 @@ impl_runtime_apis! { // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/centrifuge/Cargo.toml b/runtime/centrifuge/Cargo.toml index ff9040e52a..1d63f1f64a 100644 --- a/runtime/centrifuge/Cargo.toml +++ b/runtime/centrifuge/Cargo.toml @@ -420,3 +420,4 @@ on-chain-release-build = [ # Set timing constants (e.g. session period) to faster versions to speed up testing. fast-runtime = [] + diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index de69ab4264..37787deb79 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -30,6 +30,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, oracles::OracleKey, permissions::{ PermissionRoles, PermissionScope, PermissionedCurrencyRole, PoolRole, Role, UNION, @@ -2201,9 +2202,9 @@ impl_runtime_apis! { } // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 37f58a69ed..7c2542b97c 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -35,6 +35,7 @@ xcm-executor = { git = "https://github.com/paritytech/polkadot", default-feature # ORML dependencies orml-asset-registry = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } orml-oracle = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } +orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } # Frontier dependencies @@ -70,6 +71,7 @@ pallet-liquidity-pools = { path = "../../pallets/liquidity-pools", default-featu pallet-liquidity-pools-gateway = { path = "../../pallets/liquidity-pools-gateway", default-features = false } pallet-loans = { path = "../../pallets/loans", default-features = false } pallet-pool-system = { path = "../../pallets/pool-system", default-features = false } +pallet-restricted-tokens = { path = "../../pallets/restricted-tokens", default-features = false } # Used for migrations log = "0.4" @@ -97,6 +99,7 @@ std = [ "log/std", "orml-asset-registry/std", "orml-oracle/std", + "orml-tokens/std", "orml-traits/std", "pallet-anchors/std", "pallet-authorship/std", @@ -117,6 +120,7 @@ std = [ "pallet-liquidity-pools/std", "pallet-loans/std", "pallet-pool-system/std", + "pallet-restricted-tokens/std", "pallet-treasury/std", "parachain-info/std", "polkadot-parachain/std", @@ -143,6 +147,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "orml-asset-registry/runtime-benchmarks", + "orml-tokens/runtime-benchmarks", "pallet-anchors/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-data-collector/runtime-benchmarks", @@ -153,6 +158,7 @@ runtime-benchmarks = [ "pallet-liquidity-pools/runtime-benchmarks", "pallet-loans/runtime-benchmarks", "pallet-pool-system/runtime-benchmarks", + "pallet-restricted-tokens/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "polkadot-parachain/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -176,6 +182,7 @@ try-runtime = [ "frame-system/try-runtime", "orml-asset-registry/try-runtime", "orml-oracle/try-runtime", + "orml-tokens/try-runtime", "pallet-anchors/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", @@ -189,6 +196,7 @@ try-runtime = [ "pallet-liquidity-pools/try-runtime", "pallet-loans/try-runtime", "pallet-pool-system/try-runtime", + "pallet-restricted-tokens/try-runtime", "pallet-treasury/try-runtime", "parachain-info/try-runtime", "sp-runtime/try-runtime", diff --git a/runtime/common/src/apis/investments.rs b/runtime/common/src/apis/investments.rs index d07b45f00a..16257e429a 100644 --- a/runtime/common/src/apis/investments.rs +++ b/runtime/common/src/apis/investments.rs @@ -16,14 +16,12 @@ use sp_std::vec::Vec; decl_runtime_apis! { /// Runtime API for investments - pub trait InvestmentsApi + pub trait InvestmentsApi where AccountId: Codec, InvestmentId: Codec, - PoolId: Codec, - CurrencyId: Codec, - Balance: Codec, + InvestmentPortfolio: Codec, { - fn investment_portfolio(account_id: AccountId) -> Option>; + fn investment_portfolio(account_id: AccountId) -> Vec<(InvestmentId, InvestmentPortfolio)>; } } diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 8e47762d35..097fff8002 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -334,7 +334,6 @@ pub mod changes { use pallet_pool_system::pool_types::changes::Requirement; use super::*; - const SECONDS_PER_WEEK: u32 = 60; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -373,50 +372,174 @@ pub mod changes { /// Module for investment portfolio common to all runtimes pub mod investment_portfolios { - - use cfg_traits::investments::{InvestmentsPortfolio, TrancheCurrency}; - use sp_std::vec::Vec; + use cfg_primitives::{Balance, PoolId, TrancheId}; + use cfg_traits::{ + investments::{InvestmentCollector, TrancheCurrency}, + PoolInspect, Seconds, + }; + use cfg_types::{investments::InvestmentPortfolio, tokens::CurrencyId}; + use frame_support::traits::{ + fungibles, + tokens::{Fortitude, Preservation}, + }; + use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; /// Get the PoolId, CurrencyId, InvestmentId, and Balance for all /// investments for an account. - pub fn get_portfolios< - Runtime, - AccountId, - TrancheId, - Investments, - InvestmentId, - CurrencyId, - PoolId, - Balance, - >( - account_id: AccountId, - ) -> Option> + /// + /// NOTE: Moving inner scope to any pallet would introduce tight(er) + /// coupling due to requirement of iterating over storage maps which in turn + /// require the pallet's Config trait. + pub fn get_account_portfolio( + investor: ::AccountId, + ) -> Vec<( + ::InvestmentId, + InvestmentPortfolio, + )> where - Investments: InvestmentsPortfolio< - AccountId, - AccountInvestmentPortfolio = Vec<(InvestmentId, CurrencyId, Balance)>, - InvestmentId = InvestmentId, - CurrencyId = CurrencyId, - Balance = Balance, + T: frame_system::Config + + pallet_investments::Config + + orml_tokens::Config + + pallet_restricted_tokens::Config, + ::InvestmentId: TrancheCurrency + + Into<::CurrencyId> + + Ord + + Into<::CurrencyId>, + CurrencyId: From<::CurrencyId> + + From<::CurrencyId>, + ::CurrencyId: + From<::CurrencyId>, + Balance: From<::Amount> + + From<::Balance>, + PoolInspector: PoolInspect< + ::AccountId, + ::CurrencyId, + PoolId = PoolId, + TrancheId = TrancheId, + Moment = Seconds, >, - AccountId: Into<::AccountId>, - InvestmentId: TrancheCurrency, - Runtime: frame_system::Config, { - let account_investments: Vec<(InvestmentId, CurrencyId, Balance)> = - Investments::get_account_investments_currency(&account_id).ok()?; - // Pool getting defined in runtime - // as opposed to pallet helper method - // as getting pool id in investments pallet - // would force tighter coupling of investments - // and pool pallets. - let portfolio: Vec<(PoolId, CurrencyId, InvestmentId, Balance)> = account_investments - .into_iter() - .map(|(investment_id, currency_id, balance)| { - (investment_id.of_pool(), currency_id, investment_id, balance) - }) - .collect(); - Some(portfolio) + let mut portfolio = BTreeMap::< + ::InvestmentId, + InvestmentPortfolio, + >::new(); + + // Denote current tranche token balances before dry running collecting + orml_tokens::Accounts::::iter_key_prefix(&investor).for_each(|currency| { + if let CurrencyId::Tranche(pool_id, tranche_id) = CurrencyId::from(currency) { + let pool_currency = PoolInspector::currency_for(pool_id) + .expect("Pool must exist; qed") + .into(); + let free_balance = as fungibles::Inspect< + T::AccountId, + >>::reducible_balance( + currency.into(), + &investor, + Preservation::Preserve, + Fortitude::Polite, + ); + let reserved_balance = as fungibles::InspectHold< + T::AccountId, + >>::balance_on_hold(currency.into(), &(), &investor); + + portfolio + .entry(TrancheCurrency::generate(pool_id, tranche_id)) + .and_modify(|p| { + p.free_tranche_tokens = free_balance.into(); + p.reserved_tranche_tokens = reserved_balance.into(); + }) + .or_insert( + InvestmentPortfolio::::new(pool_currency) + .with_free_tranche_tokens(free_balance.into()) + .with_reserved_tranche_tokens(reserved_balance.into()), + ); + } + }); + + // Set pending invest currency and claimable tranche tokens + pallet_investments::InvestOrders::::iter_key_prefix(&investor).for_each(|invest_id| { + let pool_currency = + PoolInspector::currency_for(invest_id.of_pool()).expect("Pool must exist; qed"); + + // Collect such that we can determine claimable tranche tokens + // NOTE: Does not modify storage since RtAPI is readonly + let _ = + pallet_investments::Pallet::::collect_investment(investor.clone(), invest_id); + let amount = pallet_investments::InvestOrders::::get(&investor, invest_id) + .map(|order| order.amount()) + .unwrap_or_default(); + let free_tranche_tokens_new: Balance = as fungibles::Inspect< + T::AccountId, + >>::reducible_balance( + invest_id.into(), + &investor, + Preservation::Preserve, + Fortitude::Polite, + ).into(); + + portfolio + .entry(invest_id) + .and_modify(|p| { + p.pending_invest_currency = amount.into(); + if p.free_tranche_tokens < free_tranche_tokens_new { + p.claimable_tranche_tokens = + free_tranche_tokens_new.saturating_sub(p.free_tranche_tokens); + } + }) + .or_insert( + InvestmentPortfolio::::new(pool_currency.into()) + .with_pending_invest_currency(amount.into()) + .with_claimable_tranche_tokens(free_tranche_tokens_new), + ); + }); + + // Set pending tranche tokens and claimable invest currency + pallet_investments::RedeemOrders::::iter_key_prefix(&investor).for_each(|invest_id| { + let pool_currency = + PoolInspector::currency_for(invest_id.of_pool()).expect("Pool must exist; qed"); + let balance_before: Balance = + as fungibles::Inspect< + T::AccountId, + >>::reducible_balance( + pool_currency, + &investor, + Preservation::Preserve, + Fortitude::Polite, + ).into(); + + // Collect such that we can determine claimable invest currency + // NOTE: Does not modify storage since RtAPI is readonly + let _ = + pallet_investments::Pallet::::collect_redemption(investor.clone(), invest_id); + let amount = pallet_investments::RedeemOrders::::get(&investor, invest_id) + .map(|order| order.amount()) + .unwrap_or_default(); + let balance_after: Balance = + as fungibles::Inspect< + T::AccountId, + >>::reducible_balance( + pool_currency, + &investor, + Preservation::Preserve, + Fortitude::Polite, + ).into(); + + portfolio + .entry(invest_id) + .and_modify(|p| { + p.pending_redeem_tranche_tokens = amount.into(); + if balance_before < balance_after { + p.claimable_currency = balance_after.saturating_sub(balance_before); + } + }) + .or_insert( + InvestmentPortfolio::::new(pool_currency.into()) + .with_pending_redeem_tranche_tokens(amount.into()) + .with_claimable_currency(balance_after), + ); + }); + + portfolio.into_iter().collect() } } diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index a7126c20b3..19e314dd37 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -33,6 +33,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, locations::Location, oracles::OracleKey, permissions::{ @@ -2294,9 +2295,9 @@ impl_runtime_apis! { } // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/integration-tests/src/generic/cases/investments.rs b/runtime/integration-tests/src/generic/cases/investments.rs new file mode 100644 index 0000000000..dd559a9517 --- /dev/null +++ b/runtime/integration-tests/src/generic/cases/investments.rs @@ -0,0 +1,192 @@ +use cfg_primitives::{AccountId, Balance, PoolId}; +use cfg_traits::{investments::TrancheCurrency as _, Seconds}; +use cfg_types::{ + investments::InvestmentPortfolio, + permissions::PoolRole, + tokens::{CurrencyId, TrancheCurrency}, +}; +use frame_support::traits::fungibles::MutateHold; +use runtime_common::apis::{ + runtime_decl_for_investments_api::InvestmentsApiV1, runtime_decl_for_pools_api::PoolsApiV1, +}; +use sp_core::Get; + +use crate::{ + generic::{ + config::Runtime, + env::{Blocks, Env}, + envs::runtime_env::RuntimeEnv, + utils::{ + self, + currency::{cfg, usd6, CurrencyInfo, Usd6}, + genesis::{self, Genesis}, + POOL_MIN_EPOCH_TIME, + }, + }, + utils::accounts::Keyring, +}; + +const POOL_ADMIN: Keyring = Keyring::Admin; +const INVESTOR: Keyring = Keyring::Alice; +const POOL_A: PoolId = 23; +const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000); +const REDEEM_AMOUNT: Balance = EXPECTED_POOL_BALANCE / 2; +const HOLD_AMOUNT: Balance = EXPECTED_POOL_BALANCE / 10; +const FOR_FEES: Balance = cfg(1); + +mod common { + use super::*; + + pub fn initialize_state_for_investments, T: Runtime>() -> E { + let mut env = E::from_storage( + Genesis::::default() + .add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES)) + .add(genesis::assets(vec![Usd6::ID])) + .add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)])) + .storage(), + Genesis::::default().storage(), + ); + + env.parachain_state_mut(|| { + // Create a pool + utils::give_balance::(POOL_ADMIN.id(), T::PoolDeposit::get()); + utils::create_empty_pool::(POOL_ADMIN.id(), POOL_A, Usd6::ID); + + // Grant permissions + let tranche_id = T::Api::tranche_id(POOL_A, 0).unwrap(); + let tranche_investor = PoolRole::TrancheInvestor(tranche_id, Seconds::MAX); + utils::give_pool_role::(INVESTOR.id(), POOL_A, tranche_investor); + }); + + env + } +} + +fn investment_portfolio_single_tranche() { + let mut env = common::initialize_state_for_investments::, T>(); + + let tranche_id = env.parachain_state(|| T::Api::tranche_id(POOL_A, 0).unwrap()); + let invest_id = TrancheCurrency::generate(POOL_A, tranche_id); + + let mut investment_portfolio = + env.parachain_state(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!(investment_portfolio, vec![]); + + // Invest to have pending pool currency + env.parachain_state_mut(|| { + utils::give_tokens::(INVESTOR.id(), Usd6::ID, EXPECTED_POOL_BALANCE); + utils::invest::(INVESTOR.id(), POOL_A, tranche_id, EXPECTED_POOL_BALANCE); + assert_eq!( + pallet_investments::InvestOrders::::get(INVESTOR.id(), invest_id) + .unwrap() + .amount(), + EXPECTED_POOL_BALANCE + ); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_pending_invest_currency(EXPECTED_POOL_BALANCE) + )] + ); + + // Execute epoch to move pending to claimable pool currency + env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME)); + env.parachain_state_mut(|| { + utils::close_pool_epoch::(POOL_ADMIN.id(), POOL_A); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_claimable_tranche_tokens(EXPECTED_POOL_BALANCE) + )] + ); + + // Collect to move claimable pool currency to free tranche tokens + env.parachain_state_mut(|| { + utils::collect_investments::(INVESTOR.id(), POOL_A, tranche_id); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_free_tranche_tokens(EXPECTED_POOL_BALANCE) + )] + ); + + // Redeem to move free tranche tokens to partially pending + env.parachain_state_mut(|| { + utils::redeem::(INVESTOR.id(), POOL_A, tranche_id, REDEEM_AMOUNT); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT) + .with_pending_redeem_tranche_tokens(REDEEM_AMOUNT) + )] + ); + + // Execute epoch to move pending tranche tokens to claimable pool currency + env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME)); + env.parachain_state_mut(|| { + utils::close_pool_epoch::(POOL_ADMIN.id(), POOL_A); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT) + .with_claimable_currency(REDEEM_AMOUNT) + )] + ); + + // Collect redemption to clear claimable pool currency + env.parachain_state_mut(|| { + utils::collect_redemptions::(INVESTOR.id(), POOL_A, tranche_id); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT) + )] + ); + + // Simulate holding + env.parachain_state_mut(|| { + as MutateHold>::hold( + invest_id.into(), + &(), + &INVESTOR.id(), + HOLD_AMOUNT, + ) + .unwrap(); + }); + investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id())); + assert_eq!( + investment_portfolio, + vec![( + invest_id, + InvestmentPortfolio::::new(Usd6::ID) + .with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT - HOLD_AMOUNT) + .with_reserved_tranche_tokens(HOLD_AMOUNT) + )] + ); +} + +crate::test_for_runtimes!(all, investment_portfolio_single_tranche); diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index f52a6cbe0e..635b477931 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -7,6 +7,7 @@ use cfg_primitives::{ use cfg_traits::Millis; use cfg_types::{ fixed_point::{Quantity, Rate}, + investments::InvestmentPortfolio, oracles::OracleKey, permissions::{PermissionScope, Role}, tokens::{CurrencyId, CustomMetadata, TrancheCurrency}, @@ -95,6 +96,7 @@ pub trait Runtime: + orml_oracle::Config + orml_xtokens::Config + pallet_xcm::Config + + pallet_restricted_tokens::Config { /// Just the RuntimeCall type, but redefined with extra bounds. /// You can add `From` bounds in order to convert pallet calls to @@ -128,6 +130,7 @@ pub trait Runtime: + TryInto> + From> + From> + + From> + From> + From> + From> @@ -170,6 +173,11 @@ pub trait Runtime: CurrencyId, Quantity, Self::MaxTranchesExt, + > + apis::runtime_decl_for_investments_api::InvestmentsApiV1< + Self::Block, + AccountId, + TrancheCurrency, + InvestmentPortfolio, >; type MaxTranchesExt: Codec + Get + Member + PartialOrd + TypeInfo; diff --git a/runtime/integration-tests/src/generic/mod.rs b/runtime/integration-tests/src/generic/mod.rs index 4685a67ab9..4c3b0efb8b 100644 --- a/runtime/integration-tests/src/generic/mod.rs +++ b/runtime/integration-tests/src/generic/mod.rs @@ -15,6 +15,7 @@ pub mod utils; // Test cases mod cases { mod example; + mod investments; mod liquidity_pools; mod loans; } diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index ca74b05de8..eb7878f67b 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -147,6 +147,44 @@ pub fn invest( .unwrap(); } +pub fn redeem( + investor: AccountId, + pool_id: PoolId, + tranche_id: TrancheId, + amount: Balance, +) { + pallet_investments::Pallet::::update_redeem_order( + RawOrigin::Signed(investor).into(), + TrancheCurrency::generate(pool_id, tranche_id), + amount, + ) + .unwrap(); +} + +pub fn collect_investments( + investor: AccountId, + pool_id: PoolId, + tranche_id: TrancheId, +) { + pallet_investments::Pallet::::collect_investments( + RawOrigin::Signed(investor).into(), + TrancheCurrency::generate(pool_id, tranche_id), + ) + .unwrap(); +} + +pub fn collect_redemptions( + investor: AccountId, + pool_id: PoolId, + tranche_id: TrancheId, +) { + pallet_investments::Pallet::::collect_redemptions( + RawOrigin::Signed(investor).into(), + TrancheCurrency::generate(pool_id, tranche_id), + ) + .unwrap(); +} + pub fn feed_oracle(values: Vec<(OracleKey, Quantity)>) { orml_oracle::Pallet::::feed_values(RawOrigin::Root.into(), values.try_into().unwrap()) .unwrap();