diff --git a/Cargo.lock b/Cargo.lock index 66667fb5ea..0aeffe6808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7713,15 +7713,12 @@ name = "pallet-foreign-investments" version = "1.0.0" dependencies = [ "cfg-mocks", - "cfg-primitives", "cfg-traits", "cfg-types", "frame-benchmarking", "frame-support", "frame-system", - "log", "parity-scale-codec 3.6.5", - "rand 0.8.5", "scale-info", "sp-core", "sp-io", diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index 5bab8c4a57..787a99818e 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -242,7 +242,6 @@ pub trait ForeignInvestment { investment_id: Self::InvestmentId, amount: Self::Amount, foreign_payment_currency: Self::CurrencyId, - pool_currency: Self::CurrencyId, ) -> Result<(), Self::Error>; /// Initiates the decrement of a foreign investment amount in @@ -258,7 +257,6 @@ pub trait ForeignInvestment { investment_id: Self::InvestmentId, amount: Self::Amount, foreign_payment_currency: Self::CurrencyId, - pool_currency: Self::CurrencyId, ) -> Result<(), Self::Error>; /// Initiates the increment of a foreign redemption amount for the given @@ -286,7 +284,7 @@ pub trait ForeignInvestment { investment_id: Self::InvestmentId, amount: Self::Amount, foreign_payout_currency: Self::CurrencyId, - ) -> Result<(Self::Amount, Self::Amount), Self::Error>; + ) -> Result<(), Self::Error>; /// Collect the results of a user's foreign invest orders for the given /// investment. If any amounts are not fulfilled they are directly @@ -308,7 +306,6 @@ pub trait ForeignInvestment { who: &AccountId, investment_id: Self::InvestmentId, foreign_payout_currency: Self::CurrencyId, - pool_currency: Self::CurrencyId, ) -> Result<(), Self::Error>; /// Returns, if possible, the currently unprocessed investment amount (in diff --git a/libs/types/src/investments.rs b/libs/types/src/investments.rs index 3ddef114ae..b203cdf46a 100644 --- a/libs/types/src/investments.rs +++ b/libs/types/src/investments.rs @@ -15,7 +15,10 @@ use frame_support::{dispatch::fmt::Debug, RuntimeDebug}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_arithmetic::traits::{EnsureAdd, EnsureSub}; -use sp_runtime::{traits::Zero, DispatchError, DispatchResult}; +use sp_runtime::{ + traits::{EnsureAddAssign, Zero}, + ArithmeticError, DispatchError, DispatchResult, +}; use sp_std::cmp::PartialEq; use crate::orders::Order; @@ -127,6 +130,14 @@ pub struct CollectedAmount { pub amount_payment: Balance, } +impl CollectedAmount { + pub fn increase(&mut self, other: &Self) -> Result<(), ArithmeticError> { + self.amount_collected + .ensure_add_assign(other.amount_collected)?; + self.amount_payment.ensure_add_assign(other.amount_payment) + } +} + /// A representation of an investment identifier and the corresponding owner. /// /// NOTE: Trimmed version of `InvestmentInfo` required for foreign investments. @@ -140,18 +151,7 @@ pub struct ForeignInvestmentInfo { /// A simple representation of a currency swap. #[derive( - Clone, - Default, - Copy, - PartialOrd, - Ord, - PartialEq, - Eq, - Debug, - Encode, - Decode, - TypeInfo, - MaxEncodedLen, + Clone, Default, PartialOrd, Ord, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, )] pub struct Swap< Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, @@ -162,7 +162,7 @@ pub struct Swap< /// The outgoing currency, i.e. the one which should be replaced. pub currency_out: Currency, /// The amount of incoming currency which shall be bought. - pub amount: Balance, + pub amount_in: Balance, } impl @@ -196,6 +196,16 @@ impl Result { + if self.currency_in == other.currency_in && self.currency_out == other.currency_out { + Ok(true) + } else if self.currency_in == other.currency_out && self.currency_out == other.currency_in { + Ok(false) + } else { + Err(DispatchError::Other("Swap contains different currencies")) + } + } } /// A representation of an executed investment decrement. diff --git a/pallets/foreign-investments/Cargo.toml b/pallets/foreign-investments/Cargo.toml index 3444d59a5d..58c5d6b567 100644 --- a/pallets/foreign-investments/Cargo.toml +++ b/pallets/foreign-investments/Cargo.toml @@ -11,11 +11,9 @@ version = "1.0.0" targets = ["x86_64-unknown-linux-gnu"] [dependencies] -log = { version = "0.4.17", default-features = false } parity-scale-codec = { version = "3.0.0", features = ["derive"], default-features = false } scale-info = { version = "2.3.0", default-features = false, features = ["derive"] } -cfg-primitives = { path = "../../libs/primitives", default-features = false } cfg-traits = { path = "../../libs/traits", default-features = false } cfg-types = { path = "../../libs/types", default-features = false } @@ -28,7 +26,6 @@ sp-std = { git = "https://github.com/paritytech/substrate", default-features = f frame-benchmarking = { git = "https://github.com/paritytech/substrate", default-features = false, optional = true, branch = "polkadot-v0.9.43" } [dev-dependencies] -rand = "0.8" sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } sp-io = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } @@ -37,14 +34,12 @@ cfg-mocks = { path = "../../libs/mocks" } [features] default = ["std"] std = [ - "cfg-primitives/std", "cfg-traits/std", "cfg-types/std", "parity-scale-codec/std", "frame-benchmarking?/std", "frame-support/std", "frame-system/std", - "log/std", "scale-info/std", "sp-runtime/std", "sp-std/std", @@ -52,7 +47,6 @@ std = [ runtime-benchmarks = [ "cfg-traits/runtime-benchmarks", "cfg-types/runtime-benchmarks", - "cfg-primitives/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", @@ -60,7 +54,6 @@ runtime-benchmarks = [ "cfg-mocks/runtime-benchmarks", ] try-runtime = [ - "cfg-primitives/try-runtime", "cfg-traits/try-runtime", "cfg-types/try-runtime", "frame-support/try-runtime", diff --git a/pallets/foreign-investments/src/entities.rs b/pallets/foreign-investments/src/entities.rs new file mode 100644 index 0000000000..0aa81dc10a --- /dev/null +++ b/pallets/foreign-investments/src/entities.rs @@ -0,0 +1,345 @@ +//! Types with Config access. This module does not mutate FI storage + +use cfg_traits::{investments::Investment, IdentityCurrencyConversion}; +use cfg_types::investments::{ + CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, Swap, +}; +use frame_support::{dispatch::DispatchResult, ensure}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{EnsureAdd, EnsureAddAssign, EnsureSub, Saturating, Zero}, + DispatchError, +}; + +use crate::{ + pallet::{Config, Error}, + pool_currency_of, + swaps::Swaps, + Action, SwapOf, +}; + +/// Hold the base information of a foreign investment/redemption +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct BaseInfo { + pub foreign_currency: T::CurrencyId, + pub collected: CollectedAmount, +} + +impl BaseInfo { + pub fn new(foreign_currency: T::CurrencyId) -> Result { + Ok(Self { + foreign_currency, + collected: CollectedAmount::default(), + }) + } + + pub fn ensure_same_foreign(&self, foreign_currency: T::CurrencyId) -> DispatchResult { + ensure!( + self.foreign_currency == foreign_currency, + Error::::MismatchedForeignCurrency + ); + + Ok(()) + } +} + +/// Hold the information of a foreign investment +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct InvestmentInfo { + /// General info + pub base: BaseInfo, + + /// Total swapped amount pending to execute for decreasing the investment. + /// Measured in foreign currency + pub decrease_swapped_amount: T::Balance, +} + +impl InvestmentInfo { + pub fn new(foreign_currency: T::CurrencyId) -> Result { + Ok(Self { + base: BaseInfo::new(foreign_currency)?, + decrease_swapped_amount: T::Balance::default(), + }) + } + + /// This method is performed before applying the swap. + pub fn pre_increase_swap( + &mut self, + investment_id: T::InvestmentId, + foreign_amount: T::Balance, + ) -> Result, DispatchError> { + let pool_currency = pool_currency_of::(investment_id)?; + + // NOTE: This line will be removed with market ratios + let pool_amount = T::CurrencyConverter::stable_to_stable( + pool_currency, + self.base.foreign_currency, + foreign_amount, + )?; + + Ok(Swap { + currency_in: pool_currency, + currency_out: self.base.foreign_currency, + amount_in: pool_amount, + }) + } + + /// Decrease an investment taking into account that a previous increment + /// could be pending. + /// This method is performed before applying the swap. + pub fn pre_decrease_swap( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + foreign_amount: T::Balance, + ) -> Result, DispatchError> { + let pool_currency = pool_currency_of::(investment_id)?; + + // NOTE: This line will be removed with market ratios + let pool_amount = T::CurrencyConverter::stable_to_stable( + pool_currency, + self.base.foreign_currency, + foreign_amount, + )?; + + let pending_pool_amount_increment = + Swaps::::pending_amount_for(who, investment_id, Action::Investment, pool_currency); + + let investment_decrement = pool_amount.saturating_sub(pending_pool_amount_increment); + if !investment_decrement.is_zero() { + T::Investment::update_investment( + who, + investment_id, + T::Investment::investment(who, investment_id)? + .ensure_sub(investment_decrement) + .map_err(|_| Error::::TooMuchDecrease)?, + )?; + } + + Ok(Swap { + currency_in: self.base.foreign_currency, + currency_out: pool_currency, + amount_in: foreign_amount, + }) + } + + /// Increase an investment taking into account that a previous decrement + /// could be pending. + /// This method is performed after resolve the swap. + pub fn post_increase_swap( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + pool_amount: T::Balance, + ) -> DispatchResult { + self.decrease_swapped_amount = T::Balance::default(); + + if !pool_amount.is_zero() { + T::Investment::update_investment( + who, + investment_id, + T::Investment::investment(who, investment_id)?.ensure_add(pool_amount)?, + )?; + } + + Ok(()) + } + + /// This method is performed after resolve the swap. + #[allow(clippy::type_complexity)] + pub fn post_decrease_swap( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + swapped_amount: T::Balance, + pending_amount: T::Balance, + ) -> Result>, DispatchError> { + self.decrease_swapped_amount + .ensure_add_assign(swapped_amount)?; + + if pending_amount.is_zero() { + let amount_decreased = sp_std::mem::take(&mut self.decrease_swapped_amount); + + let msg = ExecutedForeignDecreaseInvest { + amount_decreased, + foreign_currency: self.base.foreign_currency, + amount_remaining: self.remaining_foreign_amount(who, investment_id)?, + }; + + return Ok(Some(msg)); + } + + Ok(None) + } + + /// This method is performed after a collect + pub fn post_collect( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + collected: CollectedAmount, + ) -> Result, DispatchError> { + self.base.collected.increase(&collected)?; + + // NOTE: How make this works with market ratios? + let collected_foreign_amount = T::CurrencyConverter::stable_to_stable( + self.base.foreign_currency, + pool_currency_of::(investment_id)?, + collected.amount_payment, + )?; + + let msg = ExecutedForeignCollect { + currency: self.base.foreign_currency, + amount_currency_payout: collected_foreign_amount, + amount_tranche_tokens_payout: collected.amount_collected, + amount_remaining: self.remaining_foreign_amount(who, investment_id)?, + }; + + Ok(msg) + } + + pub fn remaining_foreign_amount( + &self, + who: &T::AccountId, + investment_id: T::InvestmentId, + ) -> Result { + let pending_swap = Swaps::::any_pending_amount_demominated_in( + who, + investment_id, + Action::Investment, + self.base.foreign_currency, + )?; + + // NOTE: How make this works with market ratios? + let pending_invested = T::CurrencyConverter::stable_to_stable( + self.base.foreign_currency, + pool_currency_of::(investment_id)?, + T::Investment::investment(who, investment_id)?, + )?; + + Ok(pending_swap + .ensure_add(self.decrease_swapped_amount)? + .ensure_add(pending_invested)?) + } + + pub fn is_completed( + &self, + who: &T::AccountId, + investment_id: T::InvestmentId, + ) -> Result { + Ok(self.remaining_foreign_amount(who, investment_id)?.is_zero()) + } +} + +/// Hold the information of an foreign redemption +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct RedemptionInfo { + /// General info + pub base: BaseInfo, + + /// Total swapped amount pending to execute. + pub swapped_amount: T::Balance, +} + +impl RedemptionInfo { + pub fn new(foreign_currency: T::CurrencyId) -> Result { + Ok(Self { + base: BaseInfo::new(foreign_currency)?, + swapped_amount: T::Balance::default(), + }) + } + + pub fn increase( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + tranche_tokens_amount: T::Balance, + ) -> DispatchResult { + T::Investment::update_redemption( + who, + investment_id, + T::Investment::redemption(who, investment_id)?.ensure_add(tranche_tokens_amount)?, + ) + } + + pub fn decrease( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + tranche_tokens_amount: T::Balance, + ) -> DispatchResult { + T::Investment::update_redemption( + who, + investment_id, + T::Investment::redemption(who, investment_id)?.ensure_sub(tranche_tokens_amount)?, + ) + } + + /// This method is performed after a collect and before applying the swap + pub fn post_collect_and_pre_swap( + &mut self, + investment_id: T::InvestmentId, + collected: CollectedAmount, + ) -> Result, DispatchError> { + self.base.collected.increase(&collected)?; + + let pool_currency = pool_currency_of::(investment_id)?; + + // NOTE: This line will be removed with market ratios + let foreign_amount_collected = T::CurrencyConverter::stable_to_stable( + self.base.foreign_currency, + pool_currency, + collected.amount_collected, + )?; + + Ok(Swap { + currency_in: self.base.foreign_currency, + currency_out: pool_currency, + amount_in: foreign_amount_collected, + }) + } + + /// This method is performed after resolve the swap. + #[allow(clippy::type_complexity)] + pub fn post_swap( + &mut self, + who: &T::AccountId, + investment_id: T::InvestmentId, + swapped_amount: T::Balance, + pending_amount: T::Balance, + ) -> Result>, DispatchError> { + self.swapped_amount.ensure_add_assign(swapped_amount)?; + if pending_amount.is_zero() { + let msg = ExecutedForeignCollect { + currency: self.base.foreign_currency, + amount_currency_payout: self.swapped_amount, + amount_tranche_tokens_payout: self.collected_tranche_tokens(), + amount_remaining: T::Investment::redemption(who, investment_id)?, + }; + + self.base.collected = CollectedAmount::default(); + self.swapped_amount = T::Balance::default(); + + return Ok(Some(msg)); + } + + Ok(None) + } + + pub fn collected_tranche_tokens(&self) -> T::Balance { + self.base.collected.amount_payment + } + + pub fn is_completed( + &self, + who: &T::AccountId, + investment_id: T::InvestmentId, + ) -> Result { + Ok(T::Investment::redemption(who, investment_id)?.is_zero() + && self.collected_tranche_tokens().is_zero()) + } +} diff --git a/pallets/foreign-investments/src/errors.rs b/pallets/foreign-investments/src/errors.rs deleted file mode 100644 index d530f091f8..0000000000 --- a/pallets/foreign-investments/src/errors.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 frame_support::PalletError; -use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; - -use crate::pallet::{Config, Error}; - -#[derive(Encode, Decode, TypeInfo, PalletError)] -pub enum InvestError { - /// Failed to increase the investment. - IncreaseTransition, - /// The desired decreasing amount exceeds the max amount. - DecreaseAmountOverflow, - /// Failed to transition the state as a result of a decrease. - DecreaseTransition, - /// Failed to transition after fulfilled swap order. - FulfillSwapOrderTransition, - /// Failed to transition a (partially) processed investment after - /// collecting. - CollectTransition, - /// The investment needs to be collected before it can be updated further. - CollectRequired, - /// The provided currency does not match the one stored when the first - /// investment increase was triggered. - /// - /// NOTE: As long as the `InvestmentState` has not been cleared, the - /// payment currency cannot change from the initially provided one. - InvalidPaymentCurrency, -} - -#[derive(Encode, Decode, TypeInfo, PalletError)] - -pub enum RedeemError { - /// Failed to increase the redemption. - IncreaseTransition, - /// Failed to collect the redemption. - CollectTransition, - /// The desired decreasing amount exceeds the max amount. - DecreaseAmountOverflow, - /// Failed to transition the state as a result of a decrease. - DecreaseTransition, - /// Failed to transition after fulfilled swap order. - FulfillSwapOrderTransition, - /// The redemption needs to be collected before it can be updated further. - CollectRequired, - /// The provided currency does not match the one stored when the first - /// redemption increase was triggered. - /// - /// NOTE: As long as the `RedemptionState` has not been cleared, the - /// payout currency cannot change from the initially provided one. - InvalidPayoutCurrency, -} - -impl From for Error { - fn from(error: InvestError) -> Self { - Error::::InvestError(error) - } -} - -impl From for Error { - fn from(error: RedeemError) -> Self { - Error::::RedeemError(error) - } -} diff --git a/pallets/foreign-investments/src/hooks.rs b/pallets/foreign-investments/src/hooks.rs deleted file mode 100644 index 17daddd943..0000000000 --- a/pallets/foreign-investments/src/hooks.rs +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 cfg_traits::{ - investments::{Investment, InvestmentCollector}, - StatusNotificationHook, -}; -use cfg_types::investments::{CollectedAmount, ForeignInvestmentInfo}; -use frame_support::{ensure, transactional}; -use sp_runtime::{ - traits::{EnsureAdd, EnsureSub, Zero}, - DispatchError, DispatchResult, -}; -use sp_std::marker::PhantomData; - -use crate::{ - errors::{InvestError, RedeemError}, - types::{InvestState, InvestTransition, RedeemState, RedeemTransition, TokenSwapReason}, - Config, Error, ForeignInvestmentInfo as ForeignInvestmentInfoStorage, InvestmentState, Of, - Pallet, RedemptionState, SwapOf, -}; - -/// The hook struct which acts upon a fulfilled swap order. Depending on the -/// last swap reason, advances either the [`InvestmentState`] or -/// [`RedemptionState`]. -/// -/// Assumes `TokenSwaps` as caller of of the the `notify_status_change` message. -pub struct FulfilledSwapOrderHook(PhantomData); - -// Hook execution for (partially) fulfilled token swaps which should be consumed -// by `TokenSwaps`. -impl StatusNotificationHook for FulfilledSwapOrderHook { - type Error = DispatchError; - type Id = T::TokenSwapOrderId; - type Status = SwapOf; - - #[transactional] - fn notify_status_change( - id: T::TokenSwapOrderId, - status: SwapOf, - ) -> Result<(), DispatchError> { - let maybe_info = ForeignInvestmentInfoStorage::::get(id); - if maybe_info.is_none() { - return Ok(()); - } - let info = maybe_info.expect("Cannot be None; qed"); - - match info.last_swap_reason { - // Swapping into pool or foreign - Some(TokenSwapReason::Investment) => { - Self::fulfill_invest_swap_order(&info.owner, info.id, status, true) - } - // Swapping into foreign - Some(TokenSwapReason::Redemption) => { - Self::fulfill_redeem_swap_order(&info.owner, info.id, status) - } - // Both states are swapping into foreign - Some(TokenSwapReason::InvestmentAndRedemption) => { - let active_invest_swap_amount = InvestmentState::::get(&info.owner, info.id) - .get_active_swap_amount_foreign_denominated()?; - let active_redeem_swap_amount = InvestmentState::::get(&info.owner, info.id) - .get_active_swap() - .map(|swap| swap.amount) - .unwrap_or(T::Balance::zero()); - - ensure!( - status.amount - <= active_invest_swap_amount.ensure_add(active_redeem_swap_amount)?, - Error::::FulfilledTokenSwapAmountOverflow - ); - - // Order was fulfilled at least for invest swap amount - if status.amount > active_invest_swap_amount { - let invest_swap = SwapOf:: { - amount: active_invest_swap_amount, - ..status - }; - let redeem_swap = SwapOf:: { - amount: status.amount.ensure_sub(active_invest_swap_amount)?, - ..status - }; - - // NOTE: Fulfillment of invest swap before redeem one for no particular reason. - // If we wanted to fulfill the min swap amount, we would have to add support for - // oppression of for swap updates to `fulfill_redeem_swap_order` as well in case - // redeem_swap.amount < status.amount < invest_swap.amount - Self::fulfill_invest_swap_order(&info.owner, info.id, invest_swap, false)?; - Self::fulfill_redeem_swap_order(&info.owner, info.id, redeem_swap) - } - // Order was fulfilled below invest swap amount - else { - Self::fulfill_invest_swap_order(&info.owner, info.id, status, true) - } - } - _ => { - log::debug!("Fulfilled token swap order id {:?} without advancing foreign investment because swap reason does not exist", id); - Ok(()) - } - } - } -} - -impl FulfilledSwapOrderHook { - /// Transitions the `InvestState` after fulfilling a swap order. - /// - /// NOTE: If the transition should be followed by a `RedeemState` - /// transition, the `update_swap_order` should be set to false in order to - /// oppress updating the swap order here. - #[transactional] - fn fulfill_invest_swap_order( - who: &T::AccountId, - investment_id: T::InvestmentId, - swap: SwapOf, - update_swap_order: bool, - ) -> DispatchResult { - // If the investment requires to be collected, the transition of the - // `InvestState` would fail. By implicitly collecting here, we defend against - // that and ensure that the swap order fulfillment won't be reverted (since this - // function is `transactional`). - // - // NOTE: We only collect the tranche tokens, but do not transfer them back. This - // updates the unprocessed investment amount such that transitioning the - // `InvestState` is not blocked. The user still has to do that manually by - // sending `CollectInvest`. - if T::Investment::investment_requires_collect(who, investment_id) { - T::Investment::collect_investment(who.clone(), investment_id)?; - } - - let pre_state = InvestmentState::::get(who, investment_id); - let post_state = pre_state - .transition(InvestTransition::FulfillSwapOrder(swap)) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("ForeignInvestment state transition error: {:?}", e); - Error::::from(InvestError::FulfillSwapOrderTransition) - })?; - Pallet::::apply_invest_state_transition( - who, - investment_id, - post_state, - update_swap_order, - ) - } - - /// Transitions the `RedeemState` after fulfilling a swap order. - #[transactional] - fn fulfill_redeem_swap_order( - who: &T::AccountId, - investment_id: T::InvestmentId, - swap: SwapOf, - ) -> DispatchResult { - // If the investment requires to be collected, the transition of the - // `RedeemState` would fail. By implicitly collecting here, we defend against - // that and ensure that the swap order fulfillment won't be reverted (since this - // function is `transactional`). - // - // NOTE: We only collect the pool currency, but do neither transfer them to the - // investor nor initiate the swap back into foreign currency. This updates the - // unprocessed investment amount such that transitioning the `RedeemState` is - // not blocked. The user still has to do that manually by - // sending `CollectInvest`. - if T::Investment::redemption_requires_collect(who, investment_id) { - T::Investment::collect_redemption(who.clone(), investment_id)?; - } - - // Check if redeem state is swapping and thus needs to be fulfilled - let pre_state = RedemptionState::::get(who, investment_id); - let post_state = pre_state - .transition(RedeemTransition::FulfillSwapOrder(swap)) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("ForeignInvestment state transition error: {:?}", e); - Error::::from(RedeemError::FulfillSwapOrderTransition) - })?; - Pallet::::apply_redeem_state_transition(who, investment_id, post_state) - } -} - -/// The hook struct which acts upon the collection of a foreign investment. -/// -/// NOTE: Only increments the collected amount and transitions the `InvestState` -/// to update the unprocessed invest amount but does not transfer back the -/// collected amounts. We expect the user do that via -/// `collect_foreign_investment`. -pub struct CollectedInvestmentHook(PhantomData); -impl StatusNotificationHook for CollectedInvestmentHook { - type Error = DispatchError; - type Id = ForeignInvestmentInfo; - type Status = CollectedAmount; - - #[transactional] - fn notify_status_change( - id: ForeignInvestmentInfo, - status: CollectedAmount, - ) -> DispatchResult { - let ForeignInvestmentInfo { - id: investment_id, - owner: investor, - .. - } = id; - let pre_state = InvestmentState::::get(&investor, investment_id); - - // Exit early if there is no foreign investment - if pre_state == InvestState::>::NoState { - return Ok(()); - } - - Pallet::::denote_collected_investment(&investor, investment_id, status)?; - - Ok(()) - } -} - -/// The hook struct which acts upon a finalized redemption collection. -/// -/// NOTE: Only increments the collected amount and transitions the `RedeemState` -/// to update the unprocessed redeem amount but does not transfer back the -/// collected amounts. We expect the user do via -/// `collect_foreign_redemption`. - -pub struct CollectedRedemptionHook(PhantomData); -impl StatusNotificationHook for CollectedRedemptionHook { - type Error = DispatchError; - type Id = ForeignInvestmentInfo; - type Status = CollectedAmount; - - #[transactional] - fn notify_status_change( - id: ForeignInvestmentInfo, - status: CollectedAmount, - ) -> DispatchResult { - let ForeignInvestmentInfo { - id: investment_id, - owner: investor, - .. - } = id; - let pre_state = RedemptionState::::get(&investor, investment_id); - - // Exit early if there is no foreign redemption - if pre_state == RedeemState::NoState { - return Ok(()); - } - - Pallet::::denote_collected_redemption(&investor, investment_id, status)?; - - Ok(()) - } -} diff --git a/pallets/foreign-investments/src/impls.rs b/pallets/foreign-investments/src/impls.rs new file mode 100644 index 0000000000..184e2f7deb --- /dev/null +++ b/pallets/foreign-investments/src/impls.rs @@ -0,0 +1,355 @@ +//! Trait implementations. Higher level file. + +use cfg_traits::{ + investments::{ForeignInvestment, Investment, InvestmentCollector, TrancheCurrency}, + PoolInspect, StatusNotificationHook, TokenSwaps, +}; +use cfg_types::investments::CollectedAmount; +use frame_support::pallet_prelude::*; +use sp_runtime::traits::Zero; +use sp_std::marker::PhantomData; + +use crate::{ + entities::{InvestmentInfo, RedemptionInfo}, + pallet::{Config, Error, ForeignInvestmentInfo, ForeignRedemptionInfo, Pallet}, + pool_currency_of, + swaps::Swaps, + Action, SwapOf, +}; + +impl ForeignInvestment for Pallet { + type Amount = T::Balance; + type CurrencyId = T::CurrencyId; + type Error = DispatchError; + type InvestmentId = T::InvestmentId; + + fn increase_foreign_investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + foreign_amount: T::Balance, + foreign_currency: T::CurrencyId, + ) -> DispatchResult { + let swap = ForeignInvestmentInfo::::mutate(who, investment_id, |info| { + let info = info.get_or_insert(InvestmentInfo::new(foreign_currency)?); + info.base.ensure_same_foreign(foreign_currency)?; + info.pre_increase_swap(investment_id, foreign_amount) + })?; + + let status = Swaps::::apply(who, investment_id, Action::Investment, swap)?; + + if !status.swapped.is_zero() { + SwapDone::::for_increase_investment(who, investment_id, status.swapped)?; + } + + Ok(()) + } + + fn decrease_foreign_investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + foreign_amount: T::Balance, + foreign_currency: T::CurrencyId, + ) -> DispatchResult { + let swap = ForeignInvestmentInfo::::mutate(who, investment_id, |info| { + let info = info.as_mut().ok_or(Error::::InfoNotFound)?; + info.base.ensure_same_foreign(foreign_currency)?; + info.pre_decrease_swap(who, investment_id, foreign_amount) + })?; + + let status = Swaps::::apply(who, investment_id, Action::Investment, swap)?; + + if !status.swapped.is_zero() { + SwapDone::::for_decrease_investment( + who, + investment_id, + status.swapped, + status.pending, + )?; + } + + Ok(()) + } + + fn increase_foreign_redemption( + who: &T::AccountId, + investment_id: T::InvestmentId, + tranche_tokens_amount: T::Balance, + payout_foreign_currency: T::CurrencyId, + ) -> DispatchResult { + ForeignRedemptionInfo::::mutate(who, investment_id, |info| -> DispatchResult { + let info = info.get_or_insert(RedemptionInfo::new(payout_foreign_currency)?); + info.base.ensure_same_foreign(payout_foreign_currency)?; + info.increase(who, investment_id, tranche_tokens_amount) + }) + } + + fn decrease_foreign_redemption( + who: &T::AccountId, + investment_id: T::InvestmentId, + tranche_tokens_amount: T::Balance, + payout_foreign_currency: T::CurrencyId, + ) -> DispatchResult { + ForeignRedemptionInfo::::mutate_exists(who, investment_id, |entry| { + let info = entry.as_mut().ok_or(Error::::InfoNotFound)?; + info.base.ensure_same_foreign(payout_foreign_currency)?; + info.decrease(who, investment_id, tranche_tokens_amount)?; + + if info.is_completed(who, investment_id)? { + *entry = None; + } + + Ok(()) + }) + } + + fn collect_foreign_investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + payment_foreign_currency: T::CurrencyId, + ) -> DispatchResult { + ForeignInvestmentInfo::::mutate(who, investment_id, |info| { + let info = info.as_mut().ok_or(Error::::InfoNotFound)?; + info.base.ensure_same_foreign(payment_foreign_currency) + })?; + + T::Investment::collect_investment(who.clone(), investment_id) + } + + fn collect_foreign_redemption( + who: &T::AccountId, + investment_id: T::InvestmentId, + payout_foreign_currency: T::CurrencyId, + ) -> DispatchResult { + ForeignRedemptionInfo::::mutate(who, investment_id, |info| { + let info = info.as_mut().ok_or(Error::::InfoNotFound)?; + info.base.ensure_same_foreign(payout_foreign_currency) + })?; + + T::Investment::collect_redemption(who.clone(), investment_id) + } + + fn investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + ) -> Result { + T::Investment::investment(who, investment_id) + } + + fn redemption( + who: &T::AccountId, + investment_id: T::InvestmentId, + ) -> Result { + T::Investment::redemption(who, investment_id) + } + + fn accepted_payment_currency(investment_id: T::InvestmentId, currency: T::CurrencyId) -> bool { + if T::Investment::accepted_payment_currency(investment_id, currency) { + true + } else { + T::PoolInspect::currency_for(investment_id.of_pool()) + .map(|pool_currency| T::TokenSwaps::valid_pair(pool_currency, currency)) + .unwrap_or(false) + } + } + + fn accepted_payout_currency(investment_id: T::InvestmentId, currency: T::CurrencyId) -> bool { + if T::Investment::accepted_payout_currency(investment_id, currency) { + true + } else { + T::PoolInspect::currency_for(investment_id.of_pool()) + .map(|pool_currency| T::TokenSwaps::valid_pair(currency, pool_currency)) + .unwrap_or(false) + } + } +} + +pub struct FulfilledSwapOrderHook(PhantomData); +impl StatusNotificationHook for FulfilledSwapOrderHook { + type Error = DispatchError; + type Id = T::SwapId; + type Status = SwapOf; + + fn notify_status_change(swap_id: T::SwapId, last_swap: SwapOf) -> DispatchResult { + match Swaps::::foreign_id_from(swap_id) { + Ok((who, investment_id, action)) => { + let pool_currency = pool_currency_of::(investment_id)?; + let swapped_amount = last_swap.amount_in; + let pending_amount = match T::TokenSwaps::get_order_details(swap_id) { + Some(swap) => swap.amount_in, + None => { + Swaps::::update_id(&who, investment_id, action, None)?; + T::Balance::default() + } + }; + + match action { + Action::Investment => match pool_currency == last_swap.currency_in { + true => SwapDone::::for_increase_investment( + &who, + investment_id, + swapped_amount, + ), + false => SwapDone::::for_decrease_investment( + &who, + investment_id, + swapped_amount, + pending_amount, + ), + }, + Action::Redemption => SwapDone::::for_redemption( + &who, + investment_id, + swapped_amount, + pending_amount, + ), + } + } + Err(_) => Ok(()), // The event is not for foreign investments + } + } +} + +pub struct CollectedInvestmentHook(PhantomData); +impl StatusNotificationHook for CollectedInvestmentHook { + type Error = DispatchError; + type Id = (T::AccountId, T::InvestmentId); + type Status = CollectedAmount; + + fn notify_status_change( + (who, investment_id): (T::AccountId, T::InvestmentId), + collected: CollectedAmount, + ) -> DispatchResult { + let msg = ForeignInvestmentInfo::::mutate_exists(&who, investment_id, |entry| { + match entry.as_mut() { + Some(info) => { + let msg = info.post_collect(&who, investment_id, collected)?; + + if info.is_completed(&who, investment_id)? { + *entry = None; + } + + Ok::<_, DispatchError>(Some(msg)) + } + None => Ok(None), // Then notification is not for foreign investments + } + })?; + + // We send the event out of the Info mutation closure + if let Some(msg) = msg { + T::CollectedForeignInvestmentHook::notify_status_change( + (who.clone(), investment_id), + msg, + )?; + } + + Ok(()) + } +} + +pub struct CollectedRedemptionHook(PhantomData); +impl StatusNotificationHook for CollectedRedemptionHook { + type Error = DispatchError; + type Id = (T::AccountId, T::InvestmentId); + type Status = CollectedAmount; + + fn notify_status_change( + (who, investment_id): (T::AccountId, T::InvestmentId), + collected: CollectedAmount, + ) -> DispatchResult { + let swap = ForeignRedemptionInfo::::mutate(&who, investment_id, |entry| { + match entry.as_mut() { + Some(info) => info + .post_collect_and_pre_swap(investment_id, collected) + .map(Some), + None => Ok(None), // Then notification is not for foreign investments + } + })?; + + if let Some(swap) = swap { + let status = Swaps::::apply(&who, investment_id, Action::Redemption, swap)?; + + if !status.swapped.is_zero() { + SwapDone::::for_redemption(&who, investment_id, status.swapped, status.pending)?; + } + } + + Ok(()) + } +} + +/// Internal methods used to execute swaps already done +struct SwapDone(PhantomData); +impl SwapDone { + /// Notifies that a partial increse swap has been done and applies the + /// result to an `InvestmentInfo` + fn for_increase_investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + swapped: T::Balance, + ) -> DispatchResult { + ForeignInvestmentInfo::::mutate_exists(who, investment_id, |entry| { + let info = entry.as_mut().ok_or(Error::::InfoNotFound)?; + info.post_increase_swap(who, investment_id, swapped) + }) + } + + /// Notifies that a partial decrease swap has been done and applies the + /// result to an `InvestmentInfo` + fn for_decrease_investment( + who: &T::AccountId, + investment_id: T::InvestmentId, + swapped: T::Balance, + pending: T::Balance, + ) -> DispatchResult { + let msg = ForeignInvestmentInfo::::mutate_exists(who, investment_id, |entry| { + let info = entry.as_mut().ok_or(Error::::InfoNotFound)?; + let msg = info.post_decrease_swap(who, investment_id, swapped, pending)?; + + if info.is_completed(who, investment_id)? { + *entry = None; + } + + Ok::<_, DispatchError>(msg) + })?; + + // We send the event out of the Info mutation closure + if let Some(msg) = msg { + T::DecreasedForeignInvestOrderHook::notify_status_change( + (who.clone(), investment_id), + msg, + )?; + } + + Ok(()) + } + + /// Notifies that a partial swap has been done and applies the result to + /// an `RedemptionInfo` + fn for_redemption( + who: &T::AccountId, + investment_id: T::InvestmentId, + swapped_amount: T::Balance, + pending_amount: T::Balance, + ) -> DispatchResult { + let msg = ForeignRedemptionInfo::::mutate_exists(who, investment_id, |entry| { + let info = entry.as_mut().ok_or(Error::::InfoNotFound)?; + let msg = info.post_swap(who, investment_id, swapped_amount, pending_amount)?; + + if info.is_completed(who, investment_id)? { + *entry = None; + } + + Ok::<_, DispatchError>(msg) + })?; + + // We send the event out of the Info mutation closure + if let Some(msg) = msg { + T::CollectedForeignRedemptionHook::notify_status_change( + (who.clone(), investment_id), + msg, + )?; + } + + Ok(()) + } +} diff --git a/pallets/foreign-investments/src/impls/benchmark_utils.rs b/pallets/foreign-investments/src/impls/benchmark_utils.rs deleted file mode 100644 index 0ad367a805..0000000000 --- a/pallets/foreign-investments/src/impls/benchmark_utils.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 cfg_primitives::CFG; -use cfg_traits::{ - benchmarking::{ - BenchForeignInvestmentSetupInfo, ForeignInvestmentBenchmarkHelper, - FundedPoolBenchmarkHelper, InvestmentIdBenchmarkHelper, OrderBookBenchmarkHelper, - }, - investments::{ForeignInvestment, OrderManager}, -}; -use cfg_types::{ - fixed_point::Ratio, - orders::{FulfillmentWithPrice, TotalOrder}, - tokens::CurrencyId, -}; -use frame_benchmarking::Zero; -use frame_support::assert_ok; -use sp_runtime::{DispatchError, FixedPointNumber, Perquintill}; - -use crate::{Config, Pallet}; - -pub const CURRENCY_POOL: CurrencyId = CurrencyId::ForeignAsset(1); -pub const CURRENCY_FOREIGN: CurrencyId = CurrencyId::ForeignAsset(2); -pub const DECIMALS_POOL: u32 = 12; -pub const DECIMALS_FOREIGN: u32 = 6; -pub const INVEST_AMOUNT_POOL_DENOMINATED: u128 = 100_000_000 * CFG; -pub const INVEST_AMOUNT_FOREIGN_DENOMINATED: u128 = INVEST_AMOUNT_POOL_DENOMINATED / 1_000_000; - -impl ForeignInvestmentBenchmarkHelper for Pallet -where - T::Balance: From, - T::CurrencyId: From, - T::PoolInspect: FundedPoolBenchmarkHelper< - PoolId = T::PoolId, - AccountId = T::AccountId, - Balance = T::Balance, - > + InvestmentIdBenchmarkHelper, - T::TokenSwaps: OrderBookBenchmarkHelper< - AccountId = T::AccountId, - Balance = T::Balance, - CurrencyId = T::CurrencyId, - OrderIdNonce = T::TokenSwapOrderId, - >, - T::Investment: OrderManager< - Error = DispatchError, - InvestmentId = T::InvestmentId, - Orders = TotalOrder, - Fulfillment = FulfillmentWithPrice, - >, - T::BalanceRatio: From, -{ - type AccountId = T::AccountId; - type Balance = T::Balance; - type CurrencyId = T::CurrencyId; - type InvestmentId = T::InvestmentId; - - fn bench_prepare_foreign_investments_setup( - ) -> BenchForeignInvestmentSetupInfo { - let pool_id = Default::default(); - let pool_admin: T::AccountId = frame_benchmarking::account("pool_admin", 0, 0); - ::bench_create_funded_pool( - pool_id, - &pool_admin, - ); - - // Add bidirectional trading pair and fund both accounts - let (investor, funded_trader) = - ::bench_setup_trading_pair( - CURRENCY_POOL.into(), - CURRENCY_FOREIGN.into(), - INVEST_AMOUNT_POOL_DENOMINATED.into(), - INVEST_AMOUNT_FOREIGN_DENOMINATED.into(), - DECIMALS_POOL.into(), - DECIMALS_FOREIGN.into(), - ); - ::bench_setup_trading_pair( - CURRENCY_FOREIGN.into(), - CURRENCY_POOL.into(), - INVEST_AMOUNT_FOREIGN_DENOMINATED.into(), - INVEST_AMOUNT_POOL_DENOMINATED.into(), - DECIMALS_FOREIGN.into(), - DECIMALS_POOL.into(), - ); - - // Grant investor permissions - ::bench_investor_setup( - pool_id, - investor.clone(), - T::Balance::zero(), - ); - let investment_id = - ::bench_default_investment_id(pool_id); - - BenchForeignInvestmentSetupInfo { - investor, - investment_id, - pool_currency: CURRENCY_POOL.into(), - foreign_currency: CURRENCY_FOREIGN.into(), - funded_trader, - } - } - - fn bench_prep_foreign_investments_worst_case( - investor: Self::AccountId, - investment_id: Self::InvestmentId, - pool_currency: Self::CurrencyId, - foreign_currency: Self::CurrencyId, - ) { - log::debug!( - "Preparing worst case foreign investment benchmark setup with pool currency {:?} and foreign currency: {:?}", - pool_currency, - foreign_currency - ); - - // Create `InvestState::ActiveSwapIntoPoolCurrency` and prepare redemption for - // collection by redeeming - assert_ok!(Pallet::::increase_foreign_investment( - &investor, - investment_id, - INVEST_AMOUNT_FOREIGN_DENOMINATED.into(), - foreign_currency, - pool_currency, - )); - assert_eq!( - crate::InvestmentPaymentCurrency::::get(&investor, investment_id).unwrap(), - foreign_currency - ); - - log::debug!("Increasing foreign redemption"); - assert_ok!(Pallet::::increase_foreign_redemption( - &investor, - investment_id, - INVEST_AMOUNT_FOREIGN_DENOMINATED.into(), - foreign_currency, - )); - assert_eq!( - crate::RedemptionPayoutCurrency::::get(&investor, investment_id).unwrap(), - foreign_currency - ); - - // Process redemption such that collecting will trigger worst case - let fulfillment: FulfillmentWithPrice = FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap().into(), - }; - assert_ok!(::process_redeem_orders( - investment_id - )); - assert_ok!(::redeem_fulfillment( - investment_id, - fulfillment - )); - log::debug!("Worst case benchmark foreign investment setup done!"); - } -} diff --git a/pallets/foreign-investments/src/impls/invest.rs b/pallets/foreign-investments/src/impls/invest.rs deleted file mode 100644 index 1df8c4f15f..0000000000 --- a/pallets/foreign-investments/src/impls/invest.rs +++ /dev/null @@ -1,1465 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 core::cmp::Ordering; - -use cfg_traits::IdentityCurrencyConversion; -use cfg_types::investments::Swap; -use sp_runtime::{ - traits::{EnsureAdd, EnsureSub, Zero}, - ArithmeticError, DispatchError, -}; - -use crate::types::{InvestState, InvestStateConfig, InvestTransition}; - -impl InvestState -where - T: InvestStateConfig, -{ - /// Solely apply state machine to transition one `InvestState` into another - /// based on the transition, see . - /// - /// NOTE: MUST call `apply_invest_state_transition` on the post state to - /// actually mutate storage. - pub fn transition( - &self, - transition: InvestTransition, - ) -> Result { - match transition { - InvestTransition::IncreaseInvestOrder(swap) => Self::handle_increase(self, swap), - InvestTransition::DecreaseInvestOrder(swap) => Self::handle_decrease(self, swap), - InvestTransition::FulfillSwapOrder(swap) => { - Self::handle_fulfilled_swap_order(self, swap) - } - InvestTransition::CollectInvestment(amount_unprocessed) => { - Self::handle_collect(self, amount_unprocessed) - } - } - } - - /// Returns the active swap if it exists, i.e. if the state includes - /// `ActiveSwapInto{Foreign, Pool}Currency`. - pub(crate) fn get_active_swap(&self) -> Option> { - match *self { - Self::NoState => None, - Self::InvestmentOngoing { .. } => None, - Self::ActiveSwapIntoPoolCurrency { swap } => Some(swap), - Self::ActiveSwapIntoForeignCurrency { swap } => Some(swap), - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, .. } => Some(swap), - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { swap, .. } => Some(swap), - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { swap, .. } => Some(swap), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } => Some(swap), - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Some(swap), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Some(swap), - Self::SwapIntoForeignDone { .. } => None, - Self::SwapIntoForeignDoneAndInvestmentOngoing { .. } => None, - } - } - - /// Returns the potentially existing active swap amount denominated in pool - /// currency: - /// * If the state includes `ActiveSwapIntoPoolCurrency`, it returns - /// `Some(swap.amount)`. - /// * If the state includes `ActiveSwapIntoForeignCurrency`, it returns - /// `Some(swap.amount)` converted into pool currency denomination. - /// * Else, it returns `None`. - pub(crate) fn get_active_swap_amount_pool_denominated( - &self, - ) -> Result { - match *self { - Self::NoState => Ok(T::Balance::zero()), - Self::InvestmentOngoing { .. } => Ok(T::Balance::zero()), - Self::ActiveSwapIntoPoolCurrency { swap } => Ok(swap.amount), - Self::ActiveSwapIntoForeignCurrency { swap } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, .. } => Ok(swap.amount), - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { swap, .. } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { swap, .. } => Ok(swap.amount), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Ok(swap.amount), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?), - Self::SwapIntoForeignDone { .. } => Ok(T::Balance::zero()), - Self::SwapIntoForeignDoneAndInvestmentOngoing { .. } => Ok(T::Balance::zero()), - } - } - - /// Returns the potentially existing active swap amount denominated in - /// foreign currency: - /// * If the state includes `ActiveSwapIntoPoolCurrency`, it returns - /// `Some(swap.amount)` converted into foreign currency denomination. - /// * If the state includes `ActiveSwapIntoForeignCurrency`, it returns - /// `Some(swap.amount)`. - /// * Else, it returns `None`. - pub(crate) fn get_active_swap_amount_foreign_denominated( - &self, - ) -> Result { - match *self { - Self::NoState => Ok(T::Balance::zero()), - Self::InvestmentOngoing { .. } => Ok(T::Balance::zero()), - Self::ActiveSwapIntoPoolCurrency { swap } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoForeignCurrency { swap } => Ok(swap.amount), - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, .. } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { swap, .. } => Ok(swap.amount), - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { swap, .. } => { - Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?) - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } => { - Ok(swap.amount) - } - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Ok(T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - .. - } => Ok(swap.amount), - Self::SwapIntoForeignDone { .. } => Ok(T::Balance::zero()), - Self::SwapIntoForeignDoneAndInvestmentOngoing { .. } => Ok(T::Balance::zero()), - } - } - - /// Returns the `invest_amount` if existent, else zero. - pub(crate) fn get_investing_amount(&self) -> T::Balance { - match *self { - Self::InvestmentOngoing { invest_amount } - | Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { invest_amount, .. } - | Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { invest_amount, .. } - | Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - invest_amount, - .. - } - | Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - invest_amount, - .. - } - | Self::SwapIntoForeignDoneAndInvestmentOngoing { invest_amount, .. } => invest_amount, - _ => T::Balance::zero(), - } - } -} - -// Actual impl of transition -impl InvestState -where - T: InvestStateConfig, -{ - /// Handle `increase` transitions depicted by `msg::increase` edges in the - /// invest state diagram: - /// * If there is no swap into foreign currency, the pool currency swap - /// amount is increased. - /// * Else, resolves opposite swap directions by immediately fulfilling the - /// side with lower amounts; or both if the swap amounts are equal. - /// - /// When we increase an investment, we normally have to swap it into pool - /// currency (`ActiveSwapIntoPoolCurrency`) before it can be invested - /// (`ActiveInvestmentOngoing`). However, if the current state includes - /// swapping back into pool currency (`ActiveSwapIntoForeignCurrency`) as - /// the result of a previous decrement, then we can minimize the amount - /// which needs to be swapped such that we always have **at most a single - /// active swap** which is the maximum of `pool_swap.amount` and - /// `foreign_swap.amount`. When we do this, we always need to bump the - /// investment amount as well as the `SwapIntoForeignDone` amount as a - /// result of immediately fulfilling the pool swap order up to the possible - /// amount. - /// - /// Example: - /// * Say before my pre invest state has `foreign_done = 1000` and - /// `foreign_swap.amount = 500`. Now we look at three scenarios in which we - /// increase below, exactly at and above the `foreign_swap.amount`: - /// * a) If we increase by 500, we can reduce the `foreign_swap.amount` - /// fully, which we denote by adding the 500 to the `foreign_done` amount. - /// Moreover, we can immediately invest the 500. The resulting state is - /// `(done_amount = 1500, investing = 500)`. - /// * b) If we increase by 400, we can reduce the `foreign_swap.amount` only - /// by 400 and increase both the `investing` as well as `foreign_done` - /// amount by that. The resulting state is - /// `(done_amount = 1400, foreign_swap.amount = 100, investing = 400)`. - /// * c) If we increase by 600, we can reduce the `foreign_swap.amount` - /// fully and need to add a swap into pool currency for 100. Moreover both - /// the `investing` as well as `foreign_done` amount can only be increased - /// by 500. The resulting state is - /// `(done_amount = 1500, pool_swap.amount = 100, investing = 500)`. - /// - /// NOTES: - /// * We can never directly compare `swap.amount` and `invest_amount` with - /// `foreign_swap.amount` and `done_amount` if the currencies mismatch as - /// the former pair is denominated in pool currency and the latter one in - /// foreign currency. - /// * We can ignore handling all states which include `*SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency*` as we consume the done amount - /// and transition in the post transition phase. To be safe and to not - /// make any unhandled assumptions, we throw `DispatchError::Other` for - /// these states though we need to make sure this can never occur! - fn handle_increase( - &self, - swap: Swap, - ) -> Result { - if swap.currency_in == swap.currency_out { - return Self::handle_increase_non_foreign(self, swap); - } - - match &self { - Self::NoState => Ok(Self::ActiveSwapIntoPoolCurrency { swap }), - // Add pool swap - Self::InvestmentOngoing { invest_amount } => { - Ok(Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap, - invest_amount: *invest_amount, - }) - } - // Bump pool swap - Self::ActiveSwapIntoPoolCurrency { swap: pool_swap } => { - swap.ensure_currencies_match(pool_swap, true)?; - Ok(Self::ActiveSwapIntoPoolCurrency { - swap: Swap { - amount: swap.amount.ensure_add(pool_swap.amount)?, - ..swap - }, - }) - } - // Reduce foreign swap amount by the increasing amount and increase investing amount as - // well adding foreign_done amount by the minimum of active swap amounts - Self::ActiveSwapIntoForeignCurrency { swap: foreign_swap } => { - swap.ensure_currencies_match(foreign_swap, false)?; - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_in, - swap.currency_out, - foreign_swap.amount, - )?; - let pool_amount_foreign_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - - match swap.amount.cmp(&foreign_amount_pool_denominated) { - // Pool swap amount is immediately fulfilled, i.e. invested and marked as done into foreign - Ordering::Less => { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap - .amount - .ensure_sub(pool_amount_foreign_denominated)?, - ..*foreign_swap - }, - done_amount: pool_amount_foreign_denominated, - invest_amount: swap.amount, - }) - } - // Both opposite swaps are immediately fulfilled, i.e. invested and marked as done - Ordering::Equal => Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: *foreign_swap, - invest_amount: swap.amount, - }), - // Foreign swap amount is immediately fulfilled, i.e. invested and marked as done - Ordering::Greater => { - Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - // safe since amount_in_foreign > foreign_swap.amount - amount: swap.amount.ensure_sub(foreign_amount_pool_denominated)?, - ..swap - }, - done_amount: foreign_swap.amount, - invest_amount: foreign_amount_pool_denominated, - }, - ) - } - } - } - // Bump pool swap - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - invest_amount, - } => { - swap.ensure_currencies_match(pool_swap, true)?; - - Ok(Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: swap.amount.ensure_add(pool_swap.amount)?, - ..swap - }, - invest_amount: *invest_amount, - }) - } - // Reduce foreign swap amount by the increasing amount and increase investing amount as - // well adding foreign_done amount by the minimum of active swap amounts - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: foreign_swap, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, false)?; - - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_in, - swap.currency_out, - foreign_swap.amount, - )?; - let pool_amount_foreign_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - let invest_amount = - invest_amount.ensure_add(swap.amount.min(foreign_amount_pool_denominated))?; - let done_amount = pool_amount_foreign_denominated.min(foreign_swap.amount); - - match swap.amount.cmp(&foreign_amount_pool_denominated) { - // Pool swap amount is immediately fulfilled, i.e. invested and marked as done - // into foreign - Ordering::Less => { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(pool_amount_foreign_denominated)?, - ..*foreign_swap - }, - done_amount, - invest_amount, - }, - ) - } - // Both opposite swaps are immediately fulfilled, i.e. invested and marked as done - Ordering::Equal => Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: *foreign_swap, - invest_amount, - }), - // Foreign swap amount is immediately fulfilled, i.e. invested and marked as done - Ordering::Greater => { - Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: swap.amount.ensure_sub(foreign_amount_pool_denominated)?, - ..swap - }, - done_amount, - invest_amount, - }, - ) - } - } - } - // Reduce amount of foreign by the increasing amount and increase investing as well as - // foreign_done amount by the minimum of active swap amounts - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: foreign_swap, - done_amount, - } => { - swap.ensure_currencies_match(foreign_swap, false)?; - - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_in, - swap.currency_out, - foreign_swap.amount, - )?; - let pool_amount_foreign_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - let invest_amount = swap.amount.min(foreign_amount_pool_denominated); - let done_amount = pool_amount_foreign_denominated - .min(foreign_swap.amount) - .ensure_add(*done_amount)?; - - match swap.amount.cmp(&foreign_amount_pool_denominated) { - // Pool swap amount is immediately fulfilled, i.e. invested and marked as done - // into foreign - Ordering::Less => { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(pool_amount_foreign_denominated)?, - ..*foreign_swap - }, - done_amount, - invest_amount, - }, - ) - } - // Both opposite swaps are immediately fulfilled, i.e. invested and marked as - // done - Ordering::Equal => Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: Swap { - amount: done_amount, - ..*foreign_swap - }, - invest_amount, - }), - // Foreign swap amount is immediately fulfilled, i.e. invested and marked as - // done - Ordering::Greater => { - Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: swap.amount.ensure_sub(foreign_amount_pool_denominated)?, - ..swap - }, - done_amount, - invest_amount, - }, - ) - } - } - } - // Reduce amount of foreign swap by increasing amount and increase investing as well as - // foreign_done amount by minimum of swap amounts - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: foreign_swap, - done_amount, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, false)?; - - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_in, - swap.currency_out, - foreign_swap.amount, - )?; - let pool_amount_foreign_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - let invest_amount = - invest_amount.ensure_add(swap.amount.min(foreign_amount_pool_denominated))?; - let done_amount = pool_amount_foreign_denominated - .min(foreign_swap.amount) - .ensure_add(*done_amount)?; - - match swap.amount.cmp(&foreign_amount_pool_denominated) { - // Pool swap amount is immediately fulfilled, i.e. invested and marked as done into foreign - Ordering::Less => Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(pool_amount_foreign_denominated)?, - ..*foreign_swap - }, - done_amount, - invest_amount, - }, - ), - // Both opposite swaps are immediately fulfilled, i.e. invested and marked as done - Ordering::Equal => Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: Swap { - amount: done_amount, - ..*foreign_swap - }, - invest_amount, - }), - // Foreign swap amount is immediately fulfilled, i.e. invested and marked as done - Ordering::Greater => Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: swap.amount.ensure_sub(foreign_amount_pool_denominated)?, - ..swap - }, - done_amount, - invest_amount, - }, - ), - } - } - _ => Err(DispatchError::Other( - "Invalid invest state, should automatically be transitioned into \ - ActiveSwapIntoPoolCurrencyAndInvestmentOngoing", - )), - } - } - - /// Handle `decrease` transitions depicted by `msg::decrease` edges in the - /// state diagram: - /// * If there is no swap into pool currency, the foreign currency swap - /// amount is increased up to the ongoing investment amount which is not - /// yet processed. - /// * Else, resolves opposite swap directions by immediately fulfilling the - /// side with lower amounts; or both if the swap amounts are equal. - /// - /// Throws if the decreasing amount exceeds the amount which is - /// currently swapping into pool currency and/or investing as we cannot - /// decrease more than was invested. We must ensure, this can never happen - /// at this stage! - /// - /// NOTE: We can ignore handling all states which include - /// `SwapIntoForeignDone` without `ActiveSwapIntoForeignCurrency` as we - /// consume the done amount and transition in the post transition phase. - /// Moreover, we can ignore handling all states which do not include - /// `ActiveSwapIntoPoolCurrency` or `InvestmentOngoing` as we cannot reduce - /// further then. - /// To be safe and to not make any unhandled assumptions, we throw - /// `DispatchError::Other` for these states though we need to make sure - /// this can never occur! - fn handle_decrease( - &self, - swap: Swap, - ) -> Result { - if swap.currency_in == swap.currency_out { - return Self::handle_decrease_non_foreign(self, swap); - } - - match &self { - // Cannot reduce if there is neither an ongoing investment nor an active swap into pool - // currency - Self::NoState - | Self::ActiveSwapIntoForeignCurrency { .. } - | Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { .. } => Err(DispatchError::Other( - "Invalid invest state when transitioning a decrease", - )), - // Increment foreign swap amount up to ongoing investment - Self::InvestmentOngoing { invest_amount } => { - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - - match foreign_amount_pool_denominated.cmp(invest_amount) { - Ordering::Less => Ok(Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap, - invest_amount: invest_amount.ensure_sub(foreign_amount_pool_denominated)?, - }), - Ordering::Equal => Ok(Self::ActiveSwapIntoForeignCurrency { swap }), - // should never occur but let's be safe here - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Underflow)), - } - } - // Increment return done amount up to amount of the active pool swap - Self::ActiveSwapIntoPoolCurrency { swap: pool_swap } => { - swap.ensure_currencies_match(pool_swap, false)?; - - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - - match foreign_amount_pool_denominated.cmp(&pool_swap.amount) { - Ordering::Less => Ok(Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: pool_swap - .amount - .ensure_sub(foreign_amount_pool_denominated)?, - ..*pool_swap - }, - done_amount: swap.amount, - }), - Ordering::Equal => Ok(Self::SwapIntoForeignDone { done_swap: swap }), - // should never occur but let's be safe here - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Underflow)), - } - } - // Increment `foreign_done` up to pool swap amount and increment foreign swap amount up - // to ongoing investment - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - invest_amount, - } => { - swap.ensure_currencies_match(pool_swap, false)?; - - let foreign_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - let pool_amount_foreign_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_in, - swap.currency_out, - pool_swap.amount, - )?; - let max_decrease_amount_pool_denominated = - pool_swap.amount.ensure_add(*invest_amount)?; - - // Decrease swap into pool - if foreign_amount_pool_denominated < pool_swap.amount { - Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: pool_swap.amount.ensure_sub(foreign_amount_pool_denominated)?, - ..*pool_swap - }, - done_amount: swap.amount, - invest_amount: *invest_amount, - }, - ) - } - // Active swaps cancel out each other - else if foreign_amount_pool_denominated == pool_swap.amount { - Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: swap, - invest_amount: *invest_amount, - }) - } - // Decrement exceeds swap into pool and partially ongoing investment - else if foreign_amount_pool_denominated < max_decrease_amount_pool_denominated { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: swap.amount.ensure_sub(pool_amount_foreign_denominated)?, - ..swap - }, - done_amount: pool_amount_foreign_denominated, - // Foreign swap amount is larger than pool swap amount - invest_amount: max_decrease_amount_pool_denominated.ensure_sub(foreign_amount_pool_denominated)?, - }, - ) - } - // Decrement cancels entire swap into pool and ongoing investment - else if swap.amount == max_decrease_amount_pool_denominated { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount.ensure_sub(pool_amount_foreign_denominated)?, - ..swap - }, - done_amount: pool_amount_foreign_denominated, - }) - } - // should never occur but let's be safe here - else { - Err(DispatchError::Arithmetic(ArithmeticError::Underflow)) - } - } - // Increment foreign swap up to ongoing investment - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: foreign_swap, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, true)?; - - let amount = foreign_swap.amount.ensure_add(swap.amount)?; - let swap_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - - match swap_amount_pool_denominated.cmp(invest_amount) { - Ordering::Less => Ok(Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: Swap { amount, ..swap }, - invest_amount: invest_amount.ensure_sub(swap_amount_pool_denominated)?, - }), - Ordering::Equal => Ok(Self::ActiveSwapIntoForeignCurrency { - swap: Swap { amount, ..swap }, - }), - // should never occur but let's be safe here - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Underflow)), - } - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: foreign_swap, - done_amount, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, true)?; - - let amount = foreign_swap.amount.ensure_add(swap.amount)?; - let swap_amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - - match swap_amount_pool_denominated.cmp(invest_amount) { - Ordering::Less => { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { amount, ..swap }, - done_amount: *done_amount, - invest_amount: invest_amount.ensure_sub(swap_amount_pool_denominated)?, - }, - ) - }, - Ordering::Equal => { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { amount, ..swap }, - done_amount: *done_amount, - }) - } - // should never occur but let's be safe here - Ordering::Greater => { - Err(DispatchError::Arithmetic(ArithmeticError::Underflow)) - } - } - } - _ => Err(DispatchError::Other( - "Invalid invest state, should automatically be transitioned into \ - ActiveSwapIntoPoolCurrencyAndInvestmentOngoing", - )), - } - } - - /// Handle partial/full token swap order transitions depicted by - /// `order_partial` and `order_full` edges in the state diagram. - /// - /// Please note, that we ensure that there can always be at most one swap, - /// either into pool currency (`ActiveSwapIntoPoolCurrency`) or into foreign - /// currency (`ActiveSwapIntoForeignCurrency`). Thus, if the previous state - /// (`&self`) is into pool, we know the incoming transition is made from - /// return into pool currency and vice versa if the previous state is - /// swapping into foreign currency. - /// - /// This transition should always increase the active ongoing - /// investment. - /// - /// NOTES: - /// * The fulfilled swap will always match the current state (i.e. IntoPool - /// or IntoForeign) and we do not need to denominate amounts into the - /// opposite currency. - /// * We can ignore handling all states which include `SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency` as we consume the done amount - /// and transition in the post transition phase. Moreover, we can ignore - /// handling all states which do not include `ActiveSwapInto{Pool, - /// Return}Currency` as else there cannot be an active token swap for - /// investments. To be safe and to not make any unhandled assumptions, we - /// throw `DispatchError::Other` for these states though we need to make - /// sure this can never occur! - fn handle_fulfilled_swap_order( - &self, - swap: Swap, - ) -> Result { - match &self { - Self::NoState | Self::InvestmentOngoing { .. } => Err(DispatchError::Other( - "Invalid invest state when transitioning a fulfilled order", - )), - // Increment ongoing investment by swapped amount - Self::ActiveSwapIntoPoolCurrency { swap: pool_swap } => { - swap.ensure_currencies_match(pool_swap, true)?; - match swap.amount.cmp(&pool_swap.amount) { - Ordering::Equal => Ok(Self::InvestmentOngoing { - invest_amount: swap.amount, - }), - Ordering::Less => Ok(Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: pool_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - invest_amount: swap.amount, - }), - // should never occur but let's be safe here - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - } - } - // Increment ongoing investment by swapped amount - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - invest_amount, - } => { - swap.ensure_currencies_match(pool_swap, true)?; - let invest_amount = invest_amount.ensure_add(swap.amount)?; - match swap.amount.cmp(&pool_swap.amount) { - Ordering::Equal => Ok(Self::InvestmentOngoing { invest_amount }), - Ordering::Less => Ok(Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: pool_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - invest_amount, - }), - // should never occur but let's be safe here - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - } - }, - // Increment done_foreign by swapped amount - Self::ActiveSwapIntoForeignCurrency { swap: foreign_swap } => { - swap.ensure_currencies_match(foreign_swap, true)?; - - match swap.amount.cmp(&foreign_swap.amount) { - Ordering::Equal => { - Ok(Self::SwapIntoForeignDone { done_swap: swap }) - } - Ordering::Less => { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - done_amount: swap.amount, - }) - }, - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), } - }, - // Increment done_foreign by swapped amount, leave invest amount untouched - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: foreign_swap, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, true)?; - - match swap.amount.cmp(&foreign_swap.amount) { - Ordering::Equal => { - Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: swap, - invest_amount: *invest_amount, - }) - } - Ordering::Less => { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - done_amount: swap.amount, - invest_amount: *invest_amount, - }, - ) - }, - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - } - }, - // Increment done_foreign by swapped amount - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: foreign_swap, - done_amount, - } => { - swap.ensure_currencies_match(foreign_swap, true)?; - let done_amount = done_amount.ensure_add(swap.amount)?; - - match swap.amount.cmp(&foreign_swap.amount) { - Ordering::Equal => { - Ok(Self::SwapIntoForeignDone { - done_swap: Swap { - amount: done_amount, - ..swap - }, - }) - } - Ordering::Less => { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - done_amount, - }) - }, - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - } - }, - // Increment done_foreign by swapped amount, leave invest amount untouched - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: foreign_swap, - done_amount, - invest_amount, - } => { - swap.ensure_currencies_match(foreign_swap, true)?; - let done_amount = done_amount.ensure_add(swap.amount)?; - - match swap.amount.cmp(&foreign_swap.amount) { - Ordering::Equal => { - Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: Swap { - amount: done_amount, - ..swap - }, - invest_amount: *invest_amount, - }) - } - Ordering::Less => { - Ok( - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount.ensure_sub(swap.amount)?, - ..swap - }, - done_amount, - invest_amount: *invest_amount, - }, - ) - }, - Ordering::Greater => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - } - }, - _ => Err(DispatchError::Other( - "Invalid invest state, should automatically be transitioned into state without AndSwapIntoForeignDone", - )), - } - } - - /// Handle increase transitions for the same incoming and outgoing - /// currencies. - /// - /// NOTE: We can ignore handling all states which include - /// `SwapIntoForeignDone` without `ActiveSwapIntoForeignCurrency` as we - /// consume the done amount and transition in the post transition phase. - /// Moreover, we can ignore any state which involves an active swap, i.e. - /// `ActiveSwapInto{Pool, Return}Currency`, as these must not exist if the - /// in and out currency is the same. - /// To be safe and to not make any unhandled assumptions, we throw - /// `DispatchError::Other` for these states though we need to make sure - /// this can never occur! - fn handle_increase_non_foreign( - &self, - swap: Swap, - ) -> Result { - match &self { - Self::NoState => Ok(Self::InvestmentOngoing { - invest_amount: swap.amount, - }), - Self::InvestmentOngoing { invest_amount } => Ok(Self::InvestmentOngoing { - invest_amount: invest_amount.ensure_add(swap.amount)?, - }), - Self::ActiveSwapIntoPoolCurrency { .. } - | Self::ActiveSwapIntoForeignCurrency { .. } - | Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { .. } - | Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { .. } - | Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { .. } - | Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - .. - } => Err(DispatchError::Other( - "Invalid invest state when transitioning an increased swap order with the same in- \ - and outgoing currency", - )), - _ => Err(DispatchError::Other( - "Invalid invest state, should automatically be transitioned into state without \ - AndSwapIntoForeignDone", - )), - } - } - - /// Handle decrease transitions for the same incoming and outgoing - /// currencies. - /// - /// NOTES: - /// * We can never directly compare `swap.amount` or `done_amount` with - /// `pool_swap.amount` and `invest_amount` if the currencies mismatch as - /// the former pair is denominated in foreign currency and the latter pair - /// in pool currency. - /// * We can ignore handling all states which include `SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency` as we consume the done amount - /// and transition in the post transition phase. Moreover, we can ignore - /// any state which involves an active swap, i.e. `ActiveSwapInto{Pool, - /// Return}Currency`, as these must not exist if the in and out currency - /// is the same. To be safe and to not make any unhandled assumptions, we - /// throw `DispatchError::Other` for these states though we need to make - /// sure this can never occur! - fn handle_decrease_non_foreign( - &self, - swap: Swap, - ) -> Result { - if let Self::InvestmentOngoing { invest_amount } = &self { - if swap.amount < *invest_amount { - Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: swap, - invest_amount: invest_amount.ensure_sub(swap.amount)?, - }) - } else { - Ok(Self::SwapIntoForeignDone { done_swap: swap }) - } - } - // should never occur but let's be safe here - else { - Err(DispatchError::Other( - "Invalid invest state when transitioning a decreased swap order with the same in- \ - and outgoing currency", - )) - } - } - - /// Update or kill the state's unprocessed investing amount. - /// * If the state includes `InvestmentOngoing`, either update or remove the - /// invested amount. - /// * Else the unprocessed amount should be zero. If it is not, state is - /// corrupted as this reflects the investment was increased improperly. - fn handle_collect(&self, unprocessed_amount: T::Balance) -> Result { - match self { - Self::InvestmentOngoing { .. } => { - if unprocessed_amount.is_zero() { - Ok(Self::NoState) - } else { - Ok(Self::InvestmentOngoing { - invest_amount: unprocessed_amount, - }) - } - } - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, .. } => { - if unprocessed_amount.is_zero() { - Ok(Self::ActiveSwapIntoPoolCurrency { swap: *swap }) - } else { - Ok(Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: *swap, - invest_amount: unprocessed_amount, - }) - } - } - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { swap, .. } => { - if unprocessed_amount.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrency { swap: *swap }) - } else { - Ok(Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: *swap, - invest_amount: unprocessed_amount, - }) - } - } - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - done_amount, - .. - } => { - if unprocessed_amount.is_zero() { - Ok(Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { - swap: *swap, - done_amount: *done_amount, - }) - } else { - Ok( - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: *swap, - done_amount: *done_amount, - invest_amount: unprocessed_amount, - }, - ) - } - } - Self::SwapIntoForeignDoneAndInvestmentOngoing { done_swap, .. } => { - if unprocessed_amount.is_zero() { - Ok(Self::SwapIntoForeignDone { - done_swap: *done_swap, - }) - } else { - Ok(Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: *done_swap, - invest_amount: unprocessed_amount, - }) - } - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - done_amount, - .. - } => { - if unprocessed_amount.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: *swap, - done_amount: *done_amount, - }) - } else { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: *swap, - done_amount: *done_amount, - invest_amount: unprocessed_amount, - }) - } - } - state => { - if unprocessed_amount.is_zero() { - Ok(state.clone()) - } else { - Err(DispatchError::Other( - "Invalid invest state when transitioning epoch execution", - )) - } - } - } - } -} - -#[cfg(test)] -mod tests { - use cfg_traits::IdentityCurrencyConversion; - use frame_support::{assert_err, assert_ok}; - use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; - - use super::*; - - #[derive(Clone, Copy, PartialEq, Debug)] - enum CurrencyId { - Foreign, - Pool, - } - - const CONVERSION_RATE: u128 = 3; // 300% - - fn to_pool(foreign_amount: u128) -> u128 { - foreign_amount * CONVERSION_RATE - } - - fn to_foreign(pool_amount: u128) -> u128 { - pool_amount / CONVERSION_RATE - } - - struct TestCurrencyConverter; - - impl IdentityCurrencyConversion for TestCurrencyConverter { - type Balance = u128; - type Currency = CurrencyId; - type Error = DispatchError; - - fn stable_to_stable( - currency_in: Self::Currency, - currency_out: Self::Currency, - amount: Self::Balance, - ) -> Result { - match (currency_out, currency_in) { - (CurrencyId::Foreign, CurrencyId::Pool) => Ok(to_pool(amount)), - (CurrencyId::Pool, CurrencyId::Foreign) => Ok(to_foreign(amount)), - _ => panic!("Same currency"), - } - } - } - - #[derive(PartialEq)] - struct TestConfig; - - impl InvestStateConfig for TestConfig { - type Balance = u128; - type CurrencyConverter = TestCurrencyConverter; - type CurrencyId = CurrencyId; - } - - type InvestState = super::InvestState; - type InvestTransition = super::InvestTransition; - - #[test] - fn increase_with_pool_swap() { - let done_amount = 60; - let invest_amount = 600; - let pool_swap = Swap { - currency_in: CurrencyId::Pool, - currency_out: CurrencyId::Foreign, - amount: 120, - }; - let foreign_swap = Swap { - currency_in: CurrencyId::Foreign, - currency_out: CurrencyId::Pool, - amount: 240, - }; - let increase = InvestTransition::IncreaseInvestOrder(pool_swap); - - assert_ok!( - InvestState::NoState.transition(increase.clone()), - InvestState::ActiveSwapIntoPoolCurrency { swap: pool_swap } - ); - - assert_ok!( - InvestState::InvestmentOngoing { invest_amount }.transition(increase.clone()), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - invest_amount - } - ); - - assert_ok!( - InvestState::ActiveSwapIntoPoolCurrency { swap: pool_swap } - .transition(increase.clone()), - InvestState::ActiveSwapIntoPoolCurrency { - swap: Swap { - amount: pool_swap.amount + pool_swap.amount, - ..pool_swap - } - } - ); - - assert_ok!( - InvestState::ActiveSwapIntoForeignCurrency { swap: foreign_swap } - .transition(increase.clone()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount - to_foreign(pool_swap.amount), - ..foreign_swap - }, - done_amount: to_foreign(pool_swap.amount), - invest_amount: pool_swap.amount, - } - ); - - assert_ok!( - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - invest_amount - } - .transition(increase.clone()), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: pool_swap.amount + pool_swap.amount, - ..pool_swap - }, - invest_amount - } - ); - - assert_ok!( - InvestState::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: foreign_swap, - invest_amount - } - .transition(increase.clone()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount - to_foreign(pool_swap.amount), - ..foreign_swap - }, - done_amount: to_foreign(pool_swap.amount), - invest_amount: invest_amount + pool_swap.amount, - } - ); - - assert_err!( - InvestState::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { - swap: pool_swap, - done_amount, - } - .transition(increase.clone()), - DispatchError::Other( - "Invalid invest state, should automatically be transitioned into \ - ActiveSwapIntoPoolCurrencyAndInvestmentOngoing", - ) - ); - - assert_ok!( - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: foreign_swap, - done_amount, - } - .transition(increase.clone()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount - to_foreign(pool_swap.amount), - ..foreign_swap - }, - done_amount: done_amount + to_foreign(pool_swap.amount), - invest_amount: pool_swap.amount - } - ); - - assert_ok!( - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: foreign_swap, - done_amount, - invest_amount, - } - .transition(increase.clone()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap { - amount: foreign_swap.amount - to_foreign(pool_swap.amount), - ..foreign_swap - }, - done_amount: done_amount + to_foreign(pool_swap.amount), - invest_amount: invest_amount + pool_swap.amount - } - ); - } - - impl InvestState { - fn get_done_amount(&self) -> u128 { - match *self { - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { done_amount, .. } => { - to_pool(done_amount) - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - done_amount, .. - } => to_pool(done_amount), - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - done_amount, - .. - } => to_pool(done_amount), - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - done_amount, - .. - } => to_pool(done_amount), - Self::SwapIntoForeignDone { done_swap } => to_pool(done_swap.amount), - Self::SwapIntoForeignDoneAndInvestmentOngoing { done_swap, .. } => { - to_pool(done_swap.amount) - } - _ => 0, - } - } - - fn get_swap_pool_amount(&self) -> u128 { - self.get_active_swap_amount_pool_denominated().unwrap() - } - - fn has_active_pool_swap(&self) -> bool { - self.get_active_swap() - .map(|swap| swap.currency_in == CurrencyId::Pool) - .unwrap_or(false) - } - - fn total(&self) -> u128 { - self.get_investing_amount() + self.get_done_amount() + self.get_swap_pool_amount() - } - } - - struct Checker { - old_state: InvestState, - } - - impl Checker { - fn new(initial_state: InvestState, use_case: &[InvestTransition]) -> Self { - println!("Testing use case: {:#?}", use_case); - - Self { - old_state: initial_state, - } - } - - /// Invariants from: https://centrifuge.hackmd.io/IPtRlOrOSrOF9MHjEY48BA?view#Without-storage - fn check_delta_invariant(&self, transition: &InvestTransition, new_state: &InvestState) { - println!("Transition: {:#?}", transition); - println!("New state: {:#?}, total: {}", new_state, new_state.total()); - - match *transition { - InvestTransition::IncreaseInvestOrder(swap) => { - let diff = new_state.total() - self.old_state.total(); - assert_eq!(diff, swap.amount); - } - InvestTransition::DecreaseInvestOrder(_) => { - let diff = new_state.total() - self.old_state.total(); - assert_eq!(diff, 0); - } - InvestTransition::FulfillSwapOrder(swap) => { - let diff = new_state.total() - self.old_state.total(); - assert_eq!(diff, 0); - - if self.old_state.has_active_pool_swap() { - let invest_diff = new_state.get_investing_amount() - - self.old_state.get_investing_amount(); - assert_eq!(invest_diff, swap.amount) - } else { - let done_diff = - new_state.get_done_amount() - self.old_state.get_done_amount(); - assert_eq!(done_diff, to_pool(swap.amount)) - } - } - InvestTransition::CollectInvestment(value) => { - if self.old_state.get_investing_amount() == 0 { - assert_eq!(new_state.get_investing_amount(), 0) - } else { - assert_eq!(new_state.get_investing_amount(), value); - - assert_eq!( - new_state.get_done_amount(), - self.old_state.get_done_amount() - ); - assert_eq!( - new_state.get_swap_pool_amount(), - self.old_state.get_swap_pool_amount(), - ); - } - } - } - } - } - - #[test] - fn fuzzer() { - let pool_swap_big = Swap { - currency_in: CurrencyId::Pool, - currency_out: CurrencyId::Foreign, - amount: 120, - }; - let pool_swap_small = Swap { - currency_in: CurrencyId::Pool, - currency_out: CurrencyId::Foreign, - amount: 60, - }; - let foreign_swap_big = Swap { - currency_in: CurrencyId::Foreign, - currency_out: CurrencyId::Pool, - amount: to_foreign(pool_swap_big.amount), - }; - let foreign_swap_small = Swap { - currency_in: CurrencyId::Foreign, - currency_out: CurrencyId::Pool, - amount: to_foreign(pool_swap_small.amount), - }; - - let transitions = [ - InvestTransition::IncreaseInvestOrder(pool_swap_big), - InvestTransition::IncreaseInvestOrder(pool_swap_small), - InvestTransition::DecreaseInvestOrder(foreign_swap_big), - InvestTransition::DecreaseInvestOrder(foreign_swap_small), - InvestTransition::FulfillSwapOrder(pool_swap_big), - InvestTransition::FulfillSwapOrder(pool_swap_small), - InvestTransition::FulfillSwapOrder(foreign_swap_big), - InvestTransition::FulfillSwapOrder(foreign_swap_small), - InvestTransition::CollectInvestment(60), - InvestTransition::CollectInvestment(120), - ]; - - let mut rng = StdRng::seed_from_u64(42); // Determinism for reproduction - - for _ in 0..100000 { - let mut use_case = transitions.clone(); - let use_case = use_case.partial_shuffle(&mut rng, 8).0; - let mut state = InvestState::NoState; - let mut checker = Checker::new(state.clone(), use_case); - - for transition in use_case { - state = match state.transition(transition.clone()) { - Ok(state) => { - checker.check_delta_invariant(&transition, &state); - checker.old_state = state.clone(); - state - } - // We skip the imposible transition and continues with the use case - Err(_) => state, - } - } - } - } -} diff --git a/pallets/foreign-investments/src/impls/mod.rs b/pallets/foreign-investments/src/impls/mod.rs deleted file mode 100644 index d3967b84ec..0000000000 --- a/pallets/foreign-investments/src/impls/mod.rs +++ /dev/null @@ -1,1250 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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. -#![allow(clippy::map_identity)] - -use cfg_traits::{ - investments::{ForeignInvestment, Investment, InvestmentCollector, TrancheCurrency}, - IdentityCurrencyConversion, PoolInspect, StatusNotificationHook, TokenSwaps, -}; -use cfg_types::investments::{ - CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, Swap, -}; -use frame_support::{ensure, traits::Get, transactional}; -use sp_runtime::{ - traits::{EnsureAdd, EnsureAddAssign, EnsureSub, Zero}, - DispatchError, DispatchResult, -}; - -use crate::{ - errors::{InvestError, RedeemError}, - types::{InvestState, InvestTransition, RedeemState, RedeemTransition, TokenSwapReason}, - CollectedInvestment, CollectedRedemption, Config, Error, Event, ForeignInvestmentInfo, - ForeignInvestmentInfoOf, InvestmentPaymentCurrency, InvestmentState, Of, Pallet, - RedemptionPayoutCurrency, RedemptionState, SwapOf, TokenSwapOrderIds, -}; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmark_utils; -mod invest; -mod redeem; - -impl ForeignInvestment for Pallet { - type Amount = T::Balance; - type CurrencyId = T::CurrencyId; - type Error = DispatchError; - type InvestmentId = T::InvestmentId; - - #[transactional] - fn increase_foreign_investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - amount: T::Balance, - foreign_currency: T::CurrencyId, - pool_currency: T::CurrencyId, - ) -> Result<(), DispatchError> { - ensure!( - !T::Investment::investment_requires_collect(who, investment_id), - Error::::InvestError(InvestError::CollectRequired) - ); - - // NOTE: For the MVP, we restrict the investment to the payment currency of the - // one from the initial increase. Once the `InvestmentState` has been cleared, - // another payment currency can be introduced. - let currency_matches = InvestmentPaymentCurrency::::try_mutate_exists( - who, - investment_id, - |maybe_currency| { - if let Some(currency) = maybe_currency { - Ok::(currency == &foreign_currency) - } else { - *maybe_currency = Some(foreign_currency); - Ok::(true) - } - }, - ) - // An error reflects the payment currency has not been set yet - .unwrap_or(true); - ensure!( - currency_matches, - Error::::InvestError(InvestError::InvalidPaymentCurrency) - ); - - let amount_pool_denominated = - T::CurrencyConverter::stable_to_stable(pool_currency, foreign_currency, amount)?; - let pre_state = InvestmentState::::get(who, investment_id); - let post_state = pre_state - .transition(InvestTransition::IncreaseInvestOrder(Swap { - currency_in: pool_currency, - currency_out: foreign_currency, - amount: amount_pool_denominated, - })) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("InvestState transition error: {:?}", e); - Error::::from(InvestError::IncreaseTransition) - })?; - Pallet::::apply_invest_state_transition(who, investment_id, post_state, true)?; - Ok(()) - } - - #[transactional] - fn decrease_foreign_investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - amount: T::Balance, - foreign_currency: T::CurrencyId, - pool_currency: T::CurrencyId, - ) -> Result<(), DispatchError> { - ensure!( - !T::Investment::investment_requires_collect(who, investment_id), - Error::::InvestError(InvestError::CollectRequired) - ); - let payment_currency = InvestmentPaymentCurrency::::get(who, investment_id)?; - ensure!( - payment_currency == foreign_currency, - Error::::InvestError(InvestError::InvalidPaymentCurrency) - ); - - let pre_state = InvestmentState::::get(who, investment_id); - ensure!( - pre_state.get_investing_amount() >= amount, - Error::::InvestError(InvestError::DecreaseAmountOverflow) - ); - - let post_state = pre_state - .transition(InvestTransition::DecreaseInvestOrder(Swap { - currency_in: foreign_currency, - currency_out: pool_currency, - amount, - })) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("InvestState transition error: {:?}", e); - Error::::from(InvestError::DecreaseTransition) - })?; - Pallet::::apply_invest_state_transition(who, investment_id, post_state, true)?; - - Ok(()) - } - - #[transactional] - fn increase_foreign_redemption( - who: &T::AccountId, - investment_id: T::InvestmentId, - amount: T::Balance, - payout_currency: T::CurrencyId, - ) -> Result<(), DispatchError> { - let currency_matches = RedemptionPayoutCurrency::::try_mutate_exists( - who, - investment_id, - |maybe_currency| { - if let Some(currency) = maybe_currency { - Ok::(currency == &payout_currency) - } else { - *maybe_currency = Some(payout_currency); - Ok::(true) - } - }, - ) - // An error reflects the payout currency has not been set yet - .unwrap_or(true); - ensure!( - currency_matches, - Error::::RedeemError(RedeemError::InvalidPayoutCurrency) - ); - ensure!( - !T::Investment::redemption_requires_collect(who, investment_id), - Error::::RedeemError(RedeemError::CollectRequired) - ); - - let pre_state = RedemptionState::::get(who, investment_id); - let post_state = pre_state - .transition(RedeemTransition::IncreaseRedeemOrder(amount)) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("RedeemState transition error: {:?}", e); - Error::::from(RedeemError::IncreaseTransition) - })?; - Pallet::::apply_redeem_state_transition(who, investment_id, post_state)?; - - Ok(()) - } - - #[transactional] - fn decrease_foreign_redemption( - who: &T::AccountId, - investment_id: T::InvestmentId, - amount: T::Balance, - payout_currency: T::CurrencyId, - ) -> Result<(T::Balance, T::Balance), DispatchError> { - let stored_payout_currency = RedemptionPayoutCurrency::::get(who, investment_id)?; - ensure!( - stored_payout_currency == payout_currency, - Error::::RedeemError(RedeemError::InvalidPayoutCurrency) - ); - ensure!( - !T::Investment::redemption_requires_collect(who, investment_id), - Error::::RedeemError(RedeemError::CollectRequired) - ); - - let pre_state = RedemptionState::::get(who, investment_id); - let post_state = pre_state - .transition(RedeemTransition::DecreaseRedeemOrder(amount)) - .map_err(|e| { - log::debug!("RedeemState transition error: {:?}", e); - Error::::from(RedeemError::DecreaseTransition) - })?; - Pallet::::apply_redeem_state_transition(who, investment_id, post_state)?; - - let remaining_amount = T::Investment::redemption(who, investment_id)?; - - Ok((amount, remaining_amount)) - } - - #[transactional] - fn collect_foreign_investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - foreign_payment_currency: T::CurrencyId, - ) -> DispatchResult { - let payment_currency = InvestmentPaymentCurrency::::get(who, investment_id)?; - ensure!( - payment_currency == foreign_payment_currency, - Error::::InvestError(InvestError::InvalidPaymentCurrency) - ); - - // Note: We assume the configured Investment trait to notify about the collected - // amounts via the `CollectedInvestmentHook` which handles incrementing the - // `CollectedInvestment` amount and notifying any consumer of - // `ExecutedForeignInvestmentHook` which is expected to dispatch - // `ExecutedCollectInvest`. - T::Investment::collect_investment(who.clone(), investment_id)?; - - Ok(()) - } - - #[transactional] - fn collect_foreign_redemption( - who: &T::AccountId, - investment_id: T::InvestmentId, - foreign_payout_currency: T::CurrencyId, - pool_currency: T::CurrencyId, - ) -> Result<(), DispatchError> { - let payout_currency = RedemptionPayoutCurrency::::get(who, investment_id)?; - ensure!( - payout_currency == foreign_payout_currency, - Error::::RedeemError(RedeemError::InvalidPayoutCurrency) - ); - ensure!(T::PoolInspect::currency_for(investment_id.of_pool()) - .map(|currency| currency == pool_currency) - .unwrap_or_else(|| { - log::debug!("Corruption: Failed to derive pool currency from investment id when collecting foreign redemption. Should never occur if redemption has been increased beforehand"); - false - }), - DispatchError::Corruption - ); - - // Note: We assume the configured Investment trait to notify about the collected - // amounts via the `CollectedRedemptionHook` which handles incrementing the - // `CollectedRedemption` amount. - T::Investment::collect_redemption(who.clone(), investment_id)?; - - Ok(()) - } - - fn investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - ) -> Result { - T::Investment::investment(who, investment_id) - } - - fn redemption( - who: &T::AccountId, - investment_id: T::InvestmentId, - ) -> Result { - T::Investment::redemption(who, investment_id) - } - - fn accepted_payment_currency(investment_id: T::InvestmentId, currency: T::CurrencyId) -> bool { - if T::Investment::accepted_payment_currency(investment_id, currency) { - true - } else { - T::PoolInspect::currency_for(investment_id.of_pool()) - .map(|pool_currency| T::TokenSwaps::valid_pair(pool_currency, currency)) - .unwrap_or(false) - } - } - - fn accepted_payout_currency(investment_id: T::InvestmentId, currency: T::CurrencyId) -> bool { - if T::Investment::accepted_payout_currency(investment_id, currency) { - true - } else { - T::PoolInspect::currency_for(investment_id.of_pool()) - .map(|pool_currency| T::TokenSwaps::valid_pair(currency, pool_currency)) - .unwrap_or(false) - } - } -} - -impl Pallet { - /// Applies in-memory transitions of `InvestState` to chain storage. Always - /// updates/removes `InvestmentState` and the current investment. Depending - /// on the state, also kills/updates the current token swap order as well as - /// notifies `ExecutedDecreasedHook`. - /// - /// The following execution order must not be changed: - /// - /// 1. If the `InvestState` includes `SwapIntoForeignDone` without - /// `ActiveSwapIntoForeignCurrency`: Prepare "executed decrease" hook & - /// transition state into its form without `SwapIntoForeignDone`. If the - /// state is just `SwapIntoForeignDone`, kill it. - /// - /// 2. Update the `InvestmentState` storage. This step is required as the - /// next step reads this storage entry. - /// - /// 3. Handle the token swap order by either creating, updating or killing - /// it. Depending on the current swap order and the previous and current - /// reason to update it, both the current `InvestmentState` as well as - /// `RedemptionState` might require an update. - /// - /// 4. If the token swap handling resulted in a new `InvestState`, update - /// `InvestmentState` again. Additionally, emit `ForeignInvestmentUpdate` or - /// `ForeignInvestmentCleared`. - /// - /// 5. If the token swap handling resulted in a new `RedeemState`, update - /// `RedemptionState` again. If the result includes `SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency`, remove the - /// `SwapIntoForeignDone` part or kill it. Additionally, emit - /// `ForeignRedemptionUpdate` or `ForeignRedemptionCleared`. - /// - /// 6. Update the investment. This also includes setting it to zero. We - /// assume the impl of `::Investment` handles this case. - /// - /// 7. If "executed decrease" happened, send notification. - /// - /// NOTES: - /// * Must be called after transitioning any `InvestState` via - /// `transition` to update the chain storage. - /// * When updating token swap orders, only `handle_swap_order` should - /// be called. - #[transactional] - pub(crate) fn apply_invest_state_transition( - who: &T::AccountId, - investment_id: T::InvestmentId, - state: InvestState>, - update_swap_order: bool, - ) -> DispatchResult { - // Must not send executed decrease notification before updating redemption - let mut maybe_executed_decrease: Option<(T::CurrencyId, T::Balance)> = None; - // Do first round of updates and forward state, swap as well as invest amount - - match state { - InvestState::NoState => { - InvestmentState::::remove(who, investment_id); - InvestmentPaymentCurrency::::remove(who, investment_id); - - Ok((InvestState::NoState, None, Zero::zero())) - }, - InvestState::InvestmentOngoing { invest_amount } => { - InvestmentState::::insert(who, investment_id, state.clone()); - - Ok((state, None, invest_amount)) - }, - InvestState::ActiveSwapIntoPoolCurrency { swap } | - InvestState::ActiveSwapIntoForeignCurrency { swap } | - // We don't care about `done_amount` until swap into foreign is fulfilled - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } => { - InvestmentState::::insert(who, investment_id, state.clone()); - Ok((state, Some(swap), Zero::zero())) - }, - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, invest_amount } | - InvestState::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { swap, invest_amount } | - // We don't care about `done_amount` until swap into foreign is fulfilled - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { swap,invest_amount, .. } => { - InvestmentState::::insert(who, investment_id, state.clone()); - Ok((state, Some(swap), invest_amount)) - }, - InvestState::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { swap, done_amount } => { - maybe_executed_decrease = Some((swap.currency_out, done_amount)); - - let new_state = InvestState::ActiveSwapIntoPoolCurrency { swap }; - InvestmentState::::insert(who, investment_id, new_state.clone()); - - Ok((new_state, Some(swap), Zero::zero())) - }, - InvestState::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { swap, done_amount, invest_amount } => { - maybe_executed_decrease = Some((swap.currency_out, done_amount)); - - let new_state = InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { swap, invest_amount }; - InvestmentState::::insert(who, investment_id, new_state.clone()); - - Ok((new_state, Some(swap), invest_amount)) - }, - InvestState::SwapIntoForeignDone { done_swap } => { - maybe_executed_decrease = Some((done_swap.currency_in, done_swap.amount)); - - InvestmentState::::remove(who, investment_id); - InvestmentPaymentCurrency::::remove(who, investment_id); - - Ok((InvestState::NoState, None, Zero::zero())) - }, - InvestState::SwapIntoForeignDoneAndInvestmentOngoing { done_swap, invest_amount } => { - maybe_executed_decrease = Some((done_swap.currency_in, done_swap.amount)); - - let new_state = InvestState::InvestmentOngoing { invest_amount }; - InvestmentState::::insert(who, investment_id, new_state.clone()); - - Ok((new_state, None, invest_amount)) - }, - } - .map(|(invest_state, maybe_swap, invest_amount)| { - // Must update investment amount before handling swap as in case of decrease, - // updating the swap transfers the currency from the investment account to the - // investor which is required for placing the swap order - if T::Investment::investment(who, investment_id)? != invest_amount { - T::Investment::update_investment(who, investment_id, invest_amount)?; - } - - // No need to handle swap order, if redeem state transition is applied afterwards - let final_invest_state = if update_swap_order { - Self::handle_swap_order(who, investment_id, maybe_swap, TokenSwapReason::Investment).map(|(maybe_invest_state, maybe_redeem_state)| { - Self::deposit_redemption_event(who, investment_id, maybe_redeem_state); - maybe_invest_state.unwrap_or(invest_state) - })? - } else { - invest_state - }; - Self::deposit_investment_event(who, investment_id, Some(final_invest_state)); - - // Send notification after updating invest as else funds are still locked in investment account - if let Some((foreign_currency, decreased_amount)) = maybe_executed_decrease { - Self::notify_executed_decrease_invest(who, investment_id, foreign_currency, decreased_amount)?; - } - - Ok(()) - }) - .map_err(|e: DispatchError| e)? - } - - /// Applies in-memory transitions of `RedeemState` to chain storage. Always - /// updates/removes `RedemptionState` and the current redemption. Depending - /// on the state, also kills/updates the current token swap order. - /// - /// The following execution order must not be changed: - /// - /// 1. If the `RedeemState` includes `SwapIntoForeignDone` without - /// `ActiveSwapIntoForeignCurrency`, remove the `SwapIntoForeignDone` part - /// or kill it. - /// - /// 2. Update the `RedemptionState` storage. This step is required as the - /// next step reads this storage entry. - /// - /// 3. Handle the token swap order by either creating, updating or killing - /// it. Depending on the current swap order and the previous and current - /// reason to update it, both the current `RedemptionState` as well as - /// `RedemptionState` might require an update. - /// - /// 4. If the token swap handling resulted in a new `RedeemState`, update - /// `RedemptionState` again. If the result includes `SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency`, remove the - /// `SwapIntoForeignDone` part or kill it. Additionally, emit - /// `ForeignRedemptionUpdate` or `ForeignRedemptionCleared`. - /// - /// 5. If the token swap handling resulted in a new `InvestState`, - /// update `InvestmentState`. Additionally, emit `ForeignInvestmentUpdate` - /// or `ForeignInvestmentCleared`. - /// - /// 6. Update the redemption. This also includes setting it to zero. We - /// assume the impl of `::Investment` handles this case. - /// - /// NOTES: - /// * Must be called after transitioning g any `RedeemState` via - /// `transition` to update the chain storage. - /// * When updating token swap orders, only `handle_swap_order` should - /// be called. - #[transactional] - pub(crate) fn apply_redeem_state_transition( - who: &T::AccountId, - investment_id: T::InvestmentId, - state: RedeemState, - ) -> DispatchResult { - let redeeming_amount = state.get_redeeming_amount(); - - // Do first round of updates and forward state as well as swap - match state { - RedeemState::NoState => { - RedemptionState::::remove(who, investment_id); - RedemptionPayoutCurrency::::remove(who, investment_id); - Ok((Some(RedeemState::NoState), None)) - } - RedeemState::Redeeming { .. } => { - RedemptionState::::insert(who, investment_id, state); - Ok((Some(state), None)) - } - RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { swap, .. } - | RedeemState::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, - .. - } - | RedeemState::ActiveSwapIntoForeignCurrency { swap, .. } - | RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } => { - RedemptionState::::insert(who, investment_id, state); - Ok((Some(state), Some(swap))) - } - // Only states left include `SwapIntoForeignDone` without - // `ActiveSwapIntoForeignCurrency` such that we can notify collect - swap_done_state => { - let maybe_new_state = - Self::apply_collect_redeem_transition(who, investment_id, swap_done_state)?; - Ok((maybe_new_state, None)) - } - } - .map(|(maybe_new_state, maybe_swap)| { - let (maybe_new_invest_state, maybe_new_state_prio) = Self::handle_swap_order( - who, - investment_id, - maybe_swap, - TokenSwapReason::Redemption, - )?; - - // Dispatch transition event, post swap state has priority if it exists as it is - // the result of the latest update - if let Some(redeem_state_post_swap) = maybe_new_state_prio { - Self::deposit_redemption_event(who, investment_id, Some(redeem_state_post_swap)); - } else { - Self::deposit_redemption_event(who, investment_id, maybe_new_state); - } - Self::deposit_investment_event(who, investment_id, maybe_new_invest_state); - - if T::Investment::redemption(who, investment_id)? != redeeming_amount { - // Finally, update redemption after all states have been updated - T::Investment::update_redemption(who, investment_id, redeeming_amount)?; - } - - Ok(()) - }) - .map_err(|e: DispatchError| e)? - } - - /// Emits an event indicating the corresponding `InvestState` was either - /// updated or cleared. - /// - /// NOTE: Noop if the provided state is `None`. - fn deposit_investment_event( - who: &T::AccountId, - investment_id: T::InvestmentId, - maybe_state: Option>>, - ) { - match maybe_state { - Some(InvestState::NoState) => { - Self::deposit_event(Event::::ForeignInvestmentCleared { - investor: who.clone(), - investment_id, - }) - } - Some(state) => Self::deposit_event(Event::::ForeignInvestmentUpdated { - investor: who.clone(), - investment_id, - state, - }), - _ => {} - } - } - - /// Emits an event indicating the corresponding `InvestState` was either - /// updated or cleared. - /// - /// NOTE: Noop if the provided state is `None`. - fn deposit_redemption_event( - who: &T::AccountId, - investment_id: T::InvestmentId, - maybe_state: Option>, - ) { - match maybe_state { - Some(RedeemState::NoState) => { - Self::deposit_event(Event::::ForeignRedemptionCleared { - investor: who.clone(), - investment_id, - }) - } - Some(state) => Self::deposit_event(Event::::ForeignRedemptionUpdated { - investor: who.clone(), - investment_id, - state, - }), - None => {} - } - } - - /// Terminates a redeem collection which required swapping into foreign - /// currency. - /// - /// Only acts upon redeem states which include `SwapIntoForeignDone` - /// without `ActiveSwapIntoForeignCurrency`. Other states are ignored. - /// Either updates the corresponding `RedemptionState` or drops it entirely. - /// - /// Emits `notify_executed_collect_redeem`. - /// - /// Returning... - /// * `Some(RedeemState::NoState)` indicates a `ForeignRedemptionCleared` - /// event can be deposited - /// * `Some(state)` indicates a `ForeignRedemptionUpdated` event can be - /// deposited - /// * `None` indicates no state mutation occurred - #[allow(clippy::type_complexity)] - #[transactional] - fn apply_collect_redeem_transition( - who: &T::AccountId, - investment_id: T::InvestmentId, - state: RedeemState, - ) -> Result>, DispatchError> { - let CollectedAmount:: { - amount_payment: amount_payment_tranche_tokens, - .. - } = CollectedRedemption::::get(who, investment_id); - - // Send notification and kill `CollectedRedemptionTrancheTokens` iff the state - // includes `SwapIntoForeignDone` without `ActiveSwapIntoForeignCurrency` - match state { - RedeemState::SwapIntoForeignDone { done_swap, .. } - | RedeemState::RedeemingAndSwapIntoForeignDone { done_swap, .. } => { - Self::notify_executed_collect_redeem( - who, - investment_id, - done_swap.currency_in, - CollectedAmount { - amount_collected: done_swap.amount, - amount_payment: amount_payment_tranche_tokens, - }, - )?; - CollectedRedemption::::remove(who, investment_id); - Ok(()) - } - _ => Ok(()), - } - .map_err(|e: DispatchError| e)?; - - // Update state iff the state includes `SwapIntoForeignDone` without - // `ActiveSwapIntoForeignCurrency` - match state { - RedeemState::SwapIntoForeignDone { .. } => { - RedemptionState::::remove(who, investment_id); - RedemptionPayoutCurrency::::remove(who, investment_id); - Ok(Some(RedeemState::NoState)) - } - RedeemState::RedeemingAndSwapIntoForeignDone { redeem_amount, .. } => { - let new_state = RedeemState::Redeeming { redeem_amount }; - RedemptionState::::insert(who, investment_id, new_state); - Ok(Some(new_state)) - } - _ => Ok(None), - } - } - - /// Updates or kills a token swap order. If the final swap amount is zero, - /// kills the swap order and all associated storage. Else, creates or - /// updates an existing swap order. - /// - /// If the provided reason does not match the latest one stored in - /// `ForeignInvestmentInfo`, also resolves the _merge conflict_ resulting - /// from updating and thus overwriting opposite swaps. See - /// [Self::handle_concurrent_swap_orders] for details. If this results in - /// either an altered invest state and/or an altered redeem state, the - /// corresponding storage is updated and the new states returned. The latter - /// is required for emitting events. - /// - /// NOTE: Must not call any other swap order updating function. - #[allow(clippy::type_complexity)] - #[transactional] - fn handle_swap_order( - who: &T::AccountId, - investment_id: T::InvestmentId, - maybe_swap: Option>, - reason: TokenSwapReason, - ) -> Result< - ( - Option>>, - Option>, - ), - DispatchError, - > { - // check for concurrent conflicting swap orders - if TokenSwapOrderIds::::get(who, investment_id).is_some() { - let (maybe_updated_swap, maybe_invest_state, maybe_redeem_state, swap_reason) = - Self::handle_concurrent_swap_orders(who, investment_id)?; - - // Update or kill swap order with updated order having priority in case it was - // overwritten - if let Some(swap_order) = maybe_updated_swap { - Self::place_swap_order(who, investment_id, swap_order, swap_reason)?; - } else { - Self::kill_swap_order(who, investment_id)?; - } - - // Update invest state and kill if NoState - InvestmentState::::mutate_exists(who, investment_id, |current_invest_state| { - match &maybe_invest_state { - Some(state) if state != &InvestState::NoState => { - *current_invest_state = Some(state.clone()); - } - Some(state) if state == &InvestState::NoState => { - *current_invest_state = None; - } - _ => (), - } - }); - - // Need to check if `SwapReturnDone` is part of state without - // `ActiveSwapIntoForeignCurrency` as this implies the successful termination of - // a collect (with swap into foreign currency). If this is the case, the - // returned redeem state needs to be updated or killed as well. - let returning_redeem_state = Self::apply_collect_redeem_transition( - who, - investment_id, - maybe_redeem_state.unwrap_or_default(), - )? - .map(Some) - .unwrap_or(maybe_redeem_state) - .map(|redeem_state| { - RedemptionState::::mutate_exists(who, investment_id, |current_redeem_state| { - if redeem_state != RedeemState::NoState { - *current_redeem_state = Some(redeem_state); - } else { - *current_redeem_state = None; - } - }); - redeem_state - }); - - Ok((maybe_invest_state, returning_redeem_state)) - } - // Update to provided value, if not none - else if let Some(swap_order) = maybe_swap { - Self::place_swap_order(who, investment_id, swap_order, Some(reason))?; - Ok((None, None)) - } else { - Ok((None, None)) - } - } - - /// Kills all storage associated with token swaps and cancels the - /// potentially active swap order. - #[transactional] - fn kill_swap_order(who: &T::AccountId, investment_id: T::InvestmentId) -> DispatchResult { - if let Some(swap_order_id) = TokenSwapOrderIds::::take(who, investment_id) { - if T::TokenSwaps::is_active(swap_order_id) { - T::TokenSwaps::cancel_order(swap_order_id)?; - } - ForeignInvestmentInfo::::remove(swap_order_id); - } - Ok(()) - } - - /// Sets up `TokenSwapOrderIds` and `ForeignInvestmentInfo` storages, if the - /// order does not exist yet. - /// - /// NOTE: Must only be called in `handle_swap_order`. - #[transactional] - fn place_swap_order( - who: &T::AccountId, - investment_id: T::InvestmentId, - swap: SwapOf, - reason: Option, - ) -> DispatchResult { - if swap.amount.is_zero() { - return Self::kill_swap_order(who, investment_id); - } - - // Determine whether swap order direction changed which would require the order - // to be cancelled and all associated storage to be killed - let maybe_swap_order_id = TokenSwapOrderIds::::get(who, investment_id); - let cancel_swap_order = maybe_swap_order_id - .map(|swap_order_id| { - let cancel_swap_order = T::TokenSwaps::get_order_details(swap_order_id) - .map(|swap_order| { - swap_order.currency_in != swap.currency_in - || swap_order.currency_out != swap.currency_out - }) - .unwrap_or(false); - - if cancel_swap_order { - Self::kill_swap_order(who, investment_id)?; - } - - Ok::(cancel_swap_order) - }) - .transpose()? - .unwrap_or(false); - - match maybe_swap_order_id { - // Swap order is active and matches the swap direction - Some(swap_order_id) - if T::TokenSwaps::is_active(swap_order_id) && !cancel_swap_order => - { - T::TokenSwaps::update_order( - who.clone(), - swap_order_id, - swap.amount, - // The max accepted sell rate is independent of the asset type for now - T::DefaultTokenSellRatio::get(), - )?; - ForeignInvestmentInfo::::insert( - swap_order_id, - ForeignInvestmentInfoOf:: { - owner: who.clone(), - id: investment_id, - last_swap_reason: reason, - }, - ); - } - // Edge case: Only occurs as result of implicit collect when fulfilling a swap - // order. At this point, swap is fulfilled but not propagated to the state yet as - // collecting has to happen beforehand. - Some(swap_order_id) - if !T::TokenSwaps::is_active(swap_order_id) && !cancel_swap_order => - { - Self::kill_swap_order(who, investment_id)?; - } - // Swap order either has not existed at all or was just cancelled - _ => { - let swap_order_id = T::TokenSwaps::place_order( - who.clone(), - swap.currency_in, - swap.currency_out, - swap.amount, - // The max accepted sell rate is independent of the asset type for now - T::DefaultTokenSellRatio::get(), - )?; - TokenSwapOrderIds::::insert(who, investment_id, swap_order_id); - ForeignInvestmentInfo::::insert( - swap_order_id, - ForeignInvestmentInfoOf:: { - owner: who.clone(), - id: investment_id, - last_swap_reason: reason, - }, - ); - } - }; - Ok(()) - } - - /// Determines the correct amount of a token swap based on the current - /// `InvestState` and `RedeemState` corresponding to the `TokenSwapOrderId`. - /// - /// Returns a tuple of the total swap order amount as well as potentially - /// altered invest and redeem states. Any returning tuple element which is - /// `None`, reflects that no change is required for this element. Else, it - /// needs to be applied to the storage. - /// - /// NOTE: Required since there exists at most one swap per `(AccountId, - /// InvestmentId)` pair whereas investments and redemptions can both mutate - /// orders. Assume, as a result of an `InvestState` transition, a token swap - /// order into pool currency is initialized. Then, as a result of a - /// `RedeemState` transition, a token swap order into foreign currency is - /// needed. This handler resolves the _merge conflict_ in situations where - /// the reason to create/update a swap order does not match the previous - /// reason. - /// - /// * Is noop, if the the current reason equals the previous one. - /// * If both states are swapping into foreign currency, i.e. their invest - /// and redeem states include `ActiveSwapIntoForeignCurrency`, the states - /// stay the same. However the total order amount needs to be updated by - /// summing up both swap order amounts. - /// * If the `InvestState` includes swapping into pool currency, i.e. - /// `ActiveSwapIntoPoolCurrency`, whereas the `RedeemState` is swapping - /// into the opposite direction, i.e. `ActiveSwapIntoForeignCurrency`, we - /// need to resolve the delta between both swap order amounts and update - /// the states accordingly. - #[allow(clippy::type_complexity)] - fn handle_concurrent_swap_orders( - who: &T::AccountId, - investment_id: T::InvestmentId, - ) -> Result< - ( - Option>, - Option>>, - Option>, - Option, - ), - DispatchError, - > { - // Read states from storage and determine amounts in possible denominations - let invest_state = InvestmentState::::get(who, investment_id); - let redeem_state = RedemptionState::::get(who, investment_id); - let active_invest_swap = invest_state.get_active_swap(); - let active_redeem_swap = redeem_state.get_active_swap(); - - // Exit early if neither or only a single swap is active such that no merging is - // necessary - if active_invest_swap.is_none() && active_redeem_swap.is_none() { - return Ok((None, None, None, None)); - } else if active_invest_swap.is_none() { - return Ok(( - active_redeem_swap, - None, - Some(redeem_state), - Some(TokenSwapReason::Redemption), - )); - } else if active_redeem_swap.is_none() { - return Ok(( - active_invest_swap, - Some(invest_state), - None, - Some(TokenSwapReason::Investment), - )); - } - - let invest_swap_amount_pool_deno = - invest_state.get_active_swap_amount_pool_denominated()?; - let invest_swap_amount_foreign_deno = - invest_state.get_active_swap_amount_foreign_denominated()?; - let (redeem_swap_amount_foreign_deno, redeem_swap_amount_pool_deno) = redeem_state - .get_active_swap() - .map(|swap| { - // Redemptions can only swap into foreign - let amount_pool_denominated = T::CurrencyConverter::stable_to_stable( - swap.currency_out, - swap.currency_in, - swap.amount, - )?; - Ok::<(T::Balance, T::Balance), DispatchError>(( - swap.amount, - amount_pool_denominated, - )) - }) - .transpose()? - .unwrap_or_default(); - let resolved_amount_pool_deno = - invest_swap_amount_pool_deno.min(redeem_swap_amount_pool_deno); - let swap_amount_opposite_direction_pool_deno = invest_swap_amount_pool_deno - .max(redeem_swap_amount_pool_deno) - .ensure_sub(resolved_amount_pool_deno)?; - let swap_amount_opposite_direction_foreign_deno = invest_swap_amount_foreign_deno - .max(redeem_swap_amount_foreign_deno) - .ensure_sub(invest_swap_amount_foreign_deno.min(redeem_swap_amount_foreign_deno))?; - - let (maybe_token_swap, maybe_new_invest_state, maybe_new_redeem_state, swap_reason) = - match (active_invest_swap, active_redeem_swap) { - // same swap direction - (Some(invest_swap), Some(redeem_swap)) - if invest_swap.currency_in == redeem_swap.currency_in => - { - invest_swap.ensure_currencies_match(&redeem_swap, true)?; - let token_swap = Swap { - amount: invest_swap.amount.ensure_add(redeem_swap.amount)?, - ..invest_swap - }; - Ok(( - Some(token_swap), - None, - None, - Some(TokenSwapReason::InvestmentAndRedemption), - )) - } - // opposite swap direction - (Some(invest_swap), Some(redeem_swap)) - if invest_swap.currency_in == redeem_swap.currency_out => - { - invest_swap.ensure_currencies_match(&redeem_swap, false)?; - let new_redeem_state = redeem_state.fulfill_active_swap_amount( - redeem_swap_amount_foreign_deno.min(invest_swap_amount_foreign_deno), - )?; - - let new_invest_state = match invest_state.clone() { - InvestState::ActiveSwapIntoPoolCurrency { swap: pool_swap } - | InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: pool_swap, - .. - } => { - let new_pool_swap = Swap { - amount: pool_swap.amount.ensure_sub(resolved_amount_pool_deno)?, - ..pool_swap - }; - let new_invest_amount = invest_state - .get_investing_amount() - .ensure_add(resolved_amount_pool_deno)?; - - if pool_swap.amount == resolved_amount_pool_deno { - Ok(InvestState::InvestmentOngoing { - invest_amount: new_invest_amount, - }) - } else { - Ok( - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - invest_amount: new_invest_amount, - swap: new_pool_swap, - }, - ) - } - } - state => Ok(state), - } - .map_err(|e: DispatchError| e)?; - - if invest_swap_amount_foreign_deno > redeem_swap_amount_foreign_deno { - let swap = Swap { - amount: swap_amount_opposite_direction_pool_deno, - ..invest_swap - }; - Ok(( - Some(swap), - Some(new_invest_state), - Some(new_redeem_state), - Some(TokenSwapReason::Investment), - )) - } else { - let swap = Swap { - amount: swap_amount_opposite_direction_foreign_deno, - ..redeem_swap - }; - Ok(( - Some(swap), - Some(new_invest_state), - Some(new_redeem_state), - Some(TokenSwapReason::Redemption), - )) - } - } - _ => Err(DispatchError::Other( - "Uncaught short circuit when merging concurrent swap orders", - )), - } - .map_err(|e: DispatchError| e)?; - - let new_invest_state = match maybe_new_invest_state { - Some(state) if state == invest_state => None, - state => state, - }; - let new_redeem_state = match maybe_new_redeem_state { - Some(state) if state == redeem_state => None, - state => state, - }; - - Ok(( - maybe_token_swap, - new_invest_state, - new_redeem_state, - swap_reason, - )) - } - - /// Increments the collected investment amount and transitions investment - /// state as a result of collecting the investment. - /// - /// NOTE: Does not transfer back the collected tranche tokens. This happens - /// in `notify_executed_collect_invest`. - #[transactional] - pub(crate) fn denote_collected_investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - collected: CollectedAmount, - ) -> DispatchResult { - // Increment by previously stored amounts (via `CollectedInvestmentHook`) - let nothing_collect = CollectedInvestment::::mutate(who, investment_id, |c| { - c.amount_collected - .ensure_add_assign(collected.amount_collected)?; - c.amount_payment - .ensure_add_assign(collected.amount_payment)?; - Ok::(c.amount_collected.is_zero() && c.amount_payment.is_zero()) - })?; - - // No need to transition if nothing was collected - if nothing_collect { - return Ok(()); - } - - // Update invest state to decrease the unprocessed investing amount - let investing_amount = T::Investment::investment(who, investment_id)?; - let pre_state = InvestmentState::::get(who, investment_id); - let post_state = - pre_state.transition(InvestTransition::CollectInvestment(investing_amount))?; - - // Need to send notification before potentially killing the `InvestmentState` if - // all was collected and no swap is remaining - Self::notify_executed_collect_invest(who, investment_id)?; - - Self::apply_invest_state_transition(who, investment_id, post_state, true).map_err(|e| { - log::debug!("InvestState transition error: {:?}", e); - Error::::from(InvestError::CollectTransition) - })?; - - Ok(()) - } - - /// Increments the collected redemption amount and transitions redemption - /// state as a result of collecting the redemption. - /// - /// NOTE: Neither initiates a swap from the collected pool currency into - /// foreign currency nor transfers back any currency to the investor. This - /// happens in `transfer_collected_redemption`. - pub(crate) fn denote_collected_redemption( - who: &T::AccountId, - investment_id: T::InvestmentId, - collected: CollectedAmount, - ) -> DispatchResult { - let foreign_payout_currency = RedemptionPayoutCurrency::::get(who, investment_id)?; - let pool_currency = T::PoolInspect::currency_for(investment_id.of_pool()) - .expect("Impossible to collect redemption for non existing pool at this point"); - - // Increment by previously stored amounts (via `CollectedInvestmentHook`) - let nothing_collect = CollectedRedemption::::mutate(who, investment_id, |c| { - c.amount_collected - .ensure_add_assign(collected.amount_collected)?; - c.amount_payment - .ensure_add_assign(collected.amount_payment)?; - Ok::(c.amount_collected.is_zero() && c.amount_payment.is_zero()) - })?; - - // No need to transition if nothing was collected - if nothing_collect { - return Ok(()); - } - - // Transition state to initiate swap from pool to foreign currency - let pre_state = RedemptionState::::get(who, investment_id); - let amount_unprocessed_redemption = T::Investment::redemption(who, investment_id)?; - // Amount needs to be denominated in foreign currency as it will be swapped into - // foreign currency such that the swap order amount is in the incoming currency - let amount_collected_foreign_denominated = T::CurrencyConverter::stable_to_stable( - foreign_payout_currency, - pool_currency, - collected.amount_collected, - )?; - let post_state = pre_state - .transition(RedeemTransition::CollectRedemption( - amount_unprocessed_redemption, - SwapOf:: { - amount: amount_collected_foreign_denominated, - currency_in: foreign_payout_currency, - currency_out: pool_currency, - }, - )) - .map_err(|e| { - // Inner error holds finer granularity but should never occur - log::debug!("RedeemState transition error: {:?}", e); - Error::::from(RedeemError::CollectTransition) - })?; - - Pallet::::apply_redeem_state_transition(who, investment_id, post_state)?; - - Ok(()) - } - - /// Sends `DecreasedForeignInvestOrderHook` notification such that any - /// potential consumer could act upon that, e.g. Liquidity Pools for - /// `ExecutedDecreaseInvestOrder`. - #[transactional] - pub(crate) fn notify_executed_decrease_invest( - who: &T::AccountId, - investment_id: T::InvestmentId, - foreign_currency: T::CurrencyId, - amount_decreased: T::Balance, - ) -> DispatchResult { - let pool_currency = T::PoolInspect::currency_for(investment_id.of_pool()) - .expect("Pool must exist if decrease was executed; qed."); - let amount_remaining_pool_denominated = T::Investment::investment(who, investment_id)?; - let amount_remaining_foreign_denominated = T::CurrencyConverter::stable_to_stable( - foreign_currency, - pool_currency, - amount_remaining_pool_denominated, - )?; - - T::DecreasedForeignInvestOrderHook::notify_status_change( - cfg_types::investments::ForeignInvestmentInfo:: { - owner: who.clone(), - id: investment_id, - // not relevant here - last_swap_reason: None, - }, - ExecutedForeignDecreaseInvest { - amount_decreased, - foreign_currency, - amount_remaining: amount_remaining_foreign_denominated, - }, - ) - } - - /// Consumes the `CollectedInvestment` amounts and - /// `CollectedForeignInvestmentHook` notification such that any - /// potential consumer could act upon that, e.g. Liquidity Pools for - /// `ExecutedCollectInvest`. - /// - /// NOTE: Converts the collected pool currency payment amount to foreign - /// currency via the `CurrencyConverter` trait. - pub(crate) fn notify_executed_collect_invest( - who: &T::AccountId, - investment_id: T::InvestmentId, - ) -> DispatchResult { - let foreign_payout_currency = InvestmentPaymentCurrency::::get(who, investment_id)?; - let pool_currency = T::PoolInspect::currency_for(investment_id.of_pool()) - .ok_or(Error::::PoolNotFound)?; - let collected = CollectedInvestment::::take(who, investment_id); - - // Determine payout and remaining amounts in foreign currency instead of current - // pool currency denomination - let amount_currency_payout = T::CurrencyConverter::stable_to_stable( - foreign_payout_currency, - pool_currency, - collected.amount_payment, - )?; - let remaining_amount_pool_denominated = T::Investment::investment(who, investment_id)?; - let amount_remaining_invest_foreign_denominated = T::CurrencyConverter::stable_to_stable( - foreign_payout_currency, - pool_currency, - remaining_amount_pool_denominated, - )?; - - T::CollectedForeignInvestmentHook::notify_status_change( - cfg_types::investments::ForeignInvestmentInfo:: { - owner: who.clone(), - id: investment_id, - // not relevant here - last_swap_reason: None, - }, - ExecutedForeignCollect { - currency: foreign_payout_currency, - amount_currency_payout, - amount_tranche_tokens_payout: collected.amount_collected, - amount_remaining: amount_remaining_invest_foreign_denominated, - }, - ) - } - - /// Sends `CollectedForeignRedemptionHook` notification such that any - /// potential consumer could act upon that, e.g. Liquidity Pools for - /// `ExecutedCollectRedeem`. - #[transactional] - pub(crate) fn notify_executed_collect_redeem( - who: &T::AccountId, - investment_id: T::InvestmentId, - currency: T::CurrencyId, - collected: CollectedAmount, - ) -> DispatchResult { - T::CollectedForeignRedemptionHook::notify_status_change( - cfg_types::investments::ForeignInvestmentInfo:: { - owner: who.clone(), - id: investment_id, - // not relevant here - last_swap_reason: None, - }, - ExecutedForeignCollect { - currency, - amount_currency_payout: collected.amount_collected, - amount_tranche_tokens_payout: collected.amount_payment, - amount_remaining: T::Investment::redemption(who, investment_id)?, - }, - ) - } -} diff --git a/pallets/foreign-investments/src/impls/redeem.rs b/pallets/foreign-investments/src/impls/redeem.rs deleted file mode 100644 index 105129994e..0000000000 --- a/pallets/foreign-investments/src/impls/redeem.rs +++ /dev/null @@ -1,721 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 cfg_types::investments::Swap; -use frame_support::{dispatch::fmt::Debug, ensure}; -use sp_runtime::{ - traits::{EnsureAdd, EnsureSub}, - DispatchError, -}; - -use crate::types::{RedeemState, RedeemTransition}; - -impl RedeemState -where - Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, - Currency: Clone + Copy + PartialEq + Debug, -{ - /// Solely apply state machine to transition one `RedeemState` into another - /// based on the transition, see - /// - /// NOTE: MUST call `apply_redeem_state_transition` on the post state to - /// actually mutate storage. - pub fn transition( - &self, - transition: RedeemTransition, - ) -> Result { - match transition { - RedeemTransition::IncreaseRedeemOrder(amount) => Self::handle_increase(self, amount), - RedeemTransition::DecreaseRedeemOrder(amount) => Self::handle_decrease(self, amount), - RedeemTransition::FulfillSwapOrder(swap) => { - Self::handle_fulfilled_swap_order(self, swap) - } - RedeemTransition::CollectRedemption(amount_redeeming, swap) => { - Self::handle_collect(self, amount_redeeming, swap) - } - } - } - - /// Returns the potentially existing active swap into foreign currency: - /// * If the state includes `ActiveSwapIntoForeignCurrency`, it returns the - /// corresponding `Some(swap)`. - /// * Else, it returns `None`. - pub(crate) fn get_active_swap(&self) -> Option> { - match *self { - Self::ActiveSwapIntoForeignCurrency { swap } - | Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, .. } - | Self::RedeemingAndActiveSwapIntoForeignCurrency { swap, .. } - | Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, .. - } => Some(swap), - _ => None, - } - } - - /// Returns the redeeming amount if existent. Else returns zero. - pub(crate) fn get_redeeming_amount(&self) -> Balance { - match *self { - Self::Redeeming { redeem_amount } - | Self::RedeemingAndActiveSwapIntoForeignCurrency { redeem_amount, .. } - | Self::RedeemingAndSwapIntoForeignDone { redeem_amount, .. } - | Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount, - .. - } => redeem_amount, - _ => Balance::zero(), - } - } - - /// Either adds a non existing redeeming amount to the state or overwrites - /// it. - /// * If the value is not zero and the state involves `Redeeming`: Sets the - /// amount. - /// * Else if the value is not zero and the state does not involve - /// `Redeeming`: Adds `Redeeming` to the state with the corresponding - /// amount. - /// * If the value is zero and the state includes `Redeeming`: Removes - /// `Redeeming` from the state. - /// * Else throws. - fn set_redeem_amount(&self, amount: Balance) -> Result { - if amount.is_zero() { - return Self::remove_redeem_amount(self); - } - match *self { - Self::NoState | Self::Redeeming { .. } => Ok(Self::Redeeming { - redeem_amount: amount, - }), - Self::RedeemingAndActiveSwapIntoForeignCurrency { swap, .. } => { - Ok(Self::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: amount, - swap, - }) - } - Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => { - Ok(Self::RedeemingAndSwapIntoForeignDone { - redeem_amount: amount, - done_swap, - }) - } - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, - done_amount, - .. - } => Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount: amount, - swap, - done_amount, - }, - ), - Self::ActiveSwapIntoForeignCurrency { swap } => { - Ok(Self::RedeemingAndActiveSwapIntoForeignCurrency { - swap, - redeem_amount: amount, - }) - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, done_amount } => Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, - done_amount, - redeem_amount: amount, - }, - ), - Self::SwapIntoForeignDone { done_swap } => Ok(Self::RedeemingAndSwapIntoForeignDone { - done_swap, - redeem_amount: amount, - }), - } - } - - /// Removes `Redeeming` from the state. - fn remove_redeem_amount(&self) -> Result { - match *self { - Self::Redeeming { .. } => Ok(Self::NoState), - Self::RedeemingAndActiveSwapIntoForeignCurrency { swap, .. } => { - Ok(Self::ActiveSwapIntoForeignCurrency { swap }) - } - Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => { - Ok(Self::SwapIntoForeignDone { done_swap }) - } - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, - done_amount, - .. - } => Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, done_amount }), - // Throw for states without `Redeeming` - _ => Err(DispatchError::Other( - "Cannot remove redeeming amount of redeem state which does not include `Redeeming`", - )), - } - } - - /// Reduce the amount of an active swap (into foreign currency) by the - /// provided value: - /// * Throws if there is no active swap, i.e. the state does not include - /// `ActiveSwapIntoForeignCurrency` or if the reducible amount exceeds the - /// swap amount - /// * If the provided value equals the swap amount, the state is - /// transitioned into `*AndSwapIntoForeignDone`. - /// * Else, it is transitioned into - /// `*ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone`. - pub(crate) fn fulfill_active_swap_amount( - &self, - amount: Balance, - ) -> Result { - match self { - Self::ActiveSwapIntoForeignCurrency { swap } => { - if amount == swap.amount { - Ok(Self::SwapIntoForeignDone { done_swap: *swap }) - } else { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount.ensure_sub(amount)?, - ..*swap - }, - done_amount: amount, - }) - } - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, done_amount } => { - let done_amount = done_amount.ensure_add(amount)?; - - if amount == swap.amount { - Ok(Self::SwapIntoForeignDone { - done_swap: Swap { - amount: done_amount, - ..*swap - }, - }) - } else { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount.ensure_sub(amount)?, - ..*swap - }, - done_amount, - }) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount, - swap, - } => { - if amount == swap.amount { - Ok(Self::RedeemingAndSwapIntoForeignDone { - done_swap: Swap { amount, ..*swap }, - redeem_amount: *redeem_amount, - }) - } else { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount.ensure_sub(amount)?, - ..*swap - }, - done_amount: amount, - redeem_amount: *redeem_amount, - }, - ) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount, - swap, - done_amount, - } => { - let done_amount = done_amount.ensure_add(amount)?; - - if amount == swap.amount { - Ok(Self::RedeemingAndSwapIntoForeignDone { - done_swap: Swap { - amount: done_amount, - ..*swap - }, - redeem_amount: *redeem_amount, - }) - } else { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount.ensure_sub(amount)?, - ..*swap - }, - done_amount, - redeem_amount: *redeem_amount, - }, - ) - } - } - _ => Err(DispatchError::Other( - "Invalid redeem state when fulfilling active swap amount", - )), - } - } - - /// Transition all states which include `ActiveSwapIntoForeignCurrency`. - /// - /// The resulting transitioned state either includes `*SwapIntoForeignDone` - /// or `*ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone`. - /// - /// Also supports non-foreign swaps, i.e. those with matching in and out - /// currency. - /// - /// Throws if the fulfilled swap direction is not into foreign currency or - /// if the amount exceeds the states active swap amount. - fn transition_fulfilled_swap_order( - &self, - fulfilled_swap: Swap, - ) -> Result { - ensure!( - self.get_active_swap() - .map(|swap| { - swap.amount >= fulfilled_swap.amount - && swap.currency_in == fulfilled_swap.currency_in - && swap.currency_out == fulfilled_swap.currency_out - }) - .unwrap_or(true), - DispatchError::Other("Invalid redeem state when transitioning fulfilled swap order") - ); - - let Swap { amount, .. } = fulfilled_swap; - - // Edge case: if currency_in matches currency_out, we can immediately fulfill - // the swap - match *self { - Self::ActiveSwapIntoForeignCurrency { swap } => { - if amount < swap.amount && swap.currency_in != swap.currency_out { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount - amount, - ..swap - }, - done_amount: amount, - }) - } else { - Ok(Self::SwapIntoForeignDone { done_swap: swap }) - } - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, done_amount } => { - let done_amount = done_amount.ensure_add(amount)?; - if amount < swap.amount && swap.currency_in != swap.currency_out { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount - amount, - ..swap - }, - done_amount, - }) - } else { - Ok(Self::SwapIntoForeignDone { - done_swap: Swap { - amount: done_amount, - ..swap - }, - }) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount, - swap, - } => { - if amount < swap.amount && swap.currency_in != swap.currency_out { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount - amount, - ..swap - }, - done_amount: amount, - redeem_amount, - }, - ) - } else { - Ok(Self::RedeemingAndSwapIntoForeignDone { - done_swap: swap, - redeem_amount, - }) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount, - swap, - done_amount, - } => { - let done_amount = done_amount.ensure_add(amount)?; - if amount < swap.amount && swap.currency_in != swap.currency_out { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: swap.amount - amount, - ..swap - }, - done_amount, - redeem_amount, - }, - ) - } else { - Ok(Self::RedeemingAndSwapIntoForeignDone { - done_swap: Swap { - amount: done_amount, - ..swap - }, - redeem_amount, - }) - } - } - _ => Err(DispatchError::Other( - "Invalid redeem state when transitioning fulfilled swap order", - )), - } - } - - /// Either update or remove the redeeming amount and add - /// `SwapIntoForeignDone` for the provided collected swap. - fn transition_collect_non_foreign( - &self, - amount_redeeming: Balance, - collected_swap: Swap, - ) -> Result { - match *self { - Self::Redeeming { .. } => { - if amount_redeeming.is_zero() { - Ok(Self::SwapIntoForeignDone { - done_swap: collected_swap, - }) - } else { - Ok(Self::RedeemingAndSwapIntoForeignDone { - redeem_amount: amount_redeeming, - done_swap: collected_swap, - }) - } - } - Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => { - let swap = Swap { - amount: done_swap.amount.ensure_add(collected_swap.amount)?, - ..collected_swap - }; - - if amount_redeeming.is_zero() { - Ok(Self::SwapIntoForeignDone { done_swap: swap }) - } else { - Ok(Self::RedeemingAndSwapIntoForeignDone { - redeem_amount: amount_redeeming, - done_swap: swap, - }) - } - } - _ => Err(DispatchError::Other( - "Invalid pre redeem state when transitioning non-foreign collect", - )), - } - } - - /// Apply the transition of the state after collecting a redemption: - /// * Either remove or update the redeeming amount - /// * Either add or update an active swap into foreign currency (or note a - /// fulfilled swap if the in and out currencies are the same). - /// - /// Throws if any of the following holds true - /// * The current state includes an active/done swap and in and out - /// currencies do not match the provided ones - /// * The collected amount is zero but there is a mismatch between the - /// redeeming amounts (which can only be possible if something was - /// collected) - /// * The state does not include `Redeeming` - fn transition_collect( - &self, - amount_redeeming: Balance, - collected_swap: Swap, - ) -> Result { - let redeeming_amount = self.get_redeeming_amount(); - - ensure!( - self.get_active_swap() - .map(|swap| (swap.currency_in, swap.currency_out) - == (collected_swap.currency_in, collected_swap.currency_out)) - .unwrap_or(true), - DispatchError::Other("Invalid swap currencies when transitioning collect redemption") - ); - - // Nothing changed in the executed epoch - if collected_swap.amount.is_zero() { - if redeeming_amount == amount_redeeming { - return Ok(*self); - } else { - return Err(DispatchError::Other( - "Corruption: Redeeming amount changed but nothing was collected", - )); - } - } - - // Take shortcut for same currencies - if collected_swap.currency_in == collected_swap.currency_out { - return Self::transition_collect_non_foreign(self, amount_redeeming, collected_swap); - } - - // Either remove or update redeeming amount and add/update swap into foreign - // currency - match *self { - Self::Redeeming { .. } => { - if amount_redeeming.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrency { - swap: collected_swap, - }) - } else { - Ok(Self::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: amount_redeeming, - swap: collected_swap, - }) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrency { swap, .. } => { - let new_swap = Swap { - amount: swap.amount.ensure_add(collected_swap.amount)?, - ..collected_swap - }; - if amount_redeeming.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrency { swap: new_swap }) - } else { - Ok(Self::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: amount_redeeming, - swap: new_swap, - }) - } - } - Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => { - if amount_redeeming.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: collected_swap, - done_amount: done_swap.amount, - }) - } else { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount: amount_redeeming, - swap: collected_swap, - done_amount: done_swap.amount, - }, - ) - } - } - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap, - done_amount, - .. - } => { - let new_swap = Swap { - amount: swap.amount.ensure_add(collected_swap.amount)?, - ..collected_swap - }; - if amount_redeeming.is_zero() { - Ok(Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: new_swap, - done_amount, - }) - } else { - Ok( - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount: amount_redeeming, - swap: new_swap, - done_amount, - }, - ) - } - } - _ => Err(DispatchError::Other( - "Invalid pre redeem state when transitioning foreign collect", - )), - } - } -} - -// Actual impl of transition -impl RedeemState -where - Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, - Currency: Clone + Copy + PartialEq + Debug, -{ - /// Increments the unprocessed redeeming amount or adds `Redeeming*` to the - /// state with the provided amount. - fn handle_increase(&self, amount: Balance) -> Result { - Self::set_redeem_amount(self, Self::get_redeeming_amount(self).ensure_add(amount)?) - } - - /// Decrement the unprocessed redeeming amount. I.e., if the state includes - /// `Redeeming*`, decreases the redeeming amount. - fn handle_decrease(&self, amount: Balance) -> Result { - Self::set_redeem_amount(self, Self::get_redeeming_amount(self).ensure_sub(amount)?) - } - - /// Update the state if it includes `ActiveSwapIntoForeignCurrency`. - fn handle_fulfilled_swap_order( - &self, - swap: Swap, - ) -> Result { - match self { - Self::NoState => Err(DispatchError::Other( - "Invalid redeem state when transitioning a fulfilled order", - )), - state => state.transition_fulfilled_swap_order(swap), - } - } - - /// Update the state if it includes `Redeeming`. - fn handle_collect( - &self, - amount_redeeming: Balance, - swap: Swap, - ) -> Result { - match self { - Self::NoState => Err(DispatchError::Other( - "Invalid redeem state when transitioning collect", - )), - state => state.transition_collect(amount_redeeming, swap), - } - } -} - -#[cfg(test)] -mod tests { - use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; - - use super::*; - - #[derive(Clone, Copy, PartialEq, Debug)] - enum CurrencyId { - Foreign, - Pool, - } - - type RedeemState = super::RedeemState; - type RedeemTransition = super::RedeemTransition; - - impl RedeemState { - fn get_done_amount(&self) -> u128 { - match *self { - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - done_amount, .. - } => done_amount, - Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - done_amount, - .. - } => done_amount, - Self::SwapIntoForeignDone { done_swap } => done_swap.amount, - Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => done_swap.amount, - _ => 0, - } - } - - fn get_swap_amount(&self) -> u128 { - self.get_active_swap().map(|swap| swap.amount).unwrap_or(0) - } - - fn total(&self) -> u128 { - self.get_redeeming_amount() + self.get_done_amount() + self.get_swap_amount() - } - } - - struct Checker { - old_state: RedeemState, - } - - impl Checker { - fn new(initial_state: RedeemState, use_case: &[RedeemTransition]) -> Self { - println!("Testing use case: {:#?}", use_case); - - Self { - old_state: initial_state, - } - } - - /// Invariants from: https://centrifuge.hackmd.io/IPtRlOrOSrOF9MHjEY48BA?view#Without-storage - fn check_delta_invariant(&self, transition: &RedeemTransition, new_state: &RedeemState) { - println!("Transition: {:#?}", transition); - println!("New state: {:#?}", new_state); - - match *transition { - RedeemTransition::IncreaseRedeemOrder(amount) => { - let diff = new_state.total() - self.old_state.total(); - assert_eq!(diff, amount); - } - RedeemTransition::DecreaseRedeemOrder(amount) => { - let diff = self.old_state.total() - new_state.total(); - assert_eq!(diff, amount); - } - RedeemTransition::FulfillSwapOrder(swap) => { - let diff = new_state.total() - self.old_state.total(); - assert_eq!(diff, 0); - - let done_diff = new_state.get_done_amount() - self.old_state.get_done_amount(); - assert_eq!(done_diff, swap.amount) - } - RedeemTransition::CollectRedemption(value, swap) => { - if self.old_state.get_redeeming_amount() == 0 { - assert_eq!(new_state.get_redeeming_amount(), 0) - } else { - assert_eq!(new_state.get_redeeming_amount(), value); - } - - let swap_diff = new_state.get_swap_amount() - self.old_state.get_swap_amount(); - assert_eq!(swap_diff, swap.amount) - } - }; - } - } - - #[test] - fn fuzzer() { - let foreign_swap_big = Swap { - currency_in: CurrencyId::Foreign, - currency_out: CurrencyId::Pool, - amount: 120, - }; - let foreign_swap_small = Swap { - currency_in: CurrencyId::Foreign, - currency_out: CurrencyId::Pool, - amount: 60, - }; - - let transitions = [ - RedeemTransition::IncreaseRedeemOrder(120), - RedeemTransition::IncreaseRedeemOrder(60), - RedeemTransition::DecreaseRedeemOrder(120), - RedeemTransition::DecreaseRedeemOrder(60), - RedeemTransition::FulfillSwapOrder(foreign_swap_big), - RedeemTransition::FulfillSwapOrder(foreign_swap_small), - RedeemTransition::CollectRedemption(30, foreign_swap_big), - RedeemTransition::CollectRedemption(30, foreign_swap_small), - ]; - - let mut rng = StdRng::seed_from_u64(42); // Determinism for reproduction - - for _ in 0..100000 { - let mut use_case = transitions.clone(); - let use_case = use_case.partial_shuffle(&mut rng, 8).0; - let mut state = RedeemState::NoState; - let mut checker = Checker::new(state.clone(), &use_case); - - for transition in use_case { - state = match state.transition(transition.clone()) { - Ok(state) => { - checker.check_delta_invariant(&transition, &state); - checker.old_state = state.clone(); - state - } - // We skip the imposible transition and continues with the use case - Err(_) => state, - } - } - } - } -} diff --git a/pallets/foreign-investments/src/lib.rs b/pallets/foreign-investments/src/lib.rs index 7ee3f8e485..cea6687b81 100644 --- a/pallets/foreign-investments/src/lib.rs +++ b/pallets/foreign-investments/src/lib.rs @@ -44,15 +44,11 @@ #![cfg_attr(not(feature = "std"), no_std)] use cfg_types::investments::Swap; -/// Edit this file to define custom logic or remove it if it is not needed. -/// Learn more about FRAME and the core library of Substrate FRAME pallets: -/// +pub use impls::{CollectedInvestmentHook, CollectedRedemptionHook, FulfilledSwapOrderHook}; pub use pallet::*; - -pub mod errors; -pub mod hooks; -pub mod impls; -pub mod types; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +pub use swaps::Swaps; #[cfg(test)] mod mock; @@ -60,26 +56,53 @@ mod mock; #[cfg(test)] mod tests; -pub type SwapOf = Swap<::Balance, ::CurrencyId>; -pub type ForeignInvestmentInfoOf = cfg_types::investments::ForeignInvestmentInfo< +mod entities; +mod impls; +mod swaps; + +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, +)] +pub enum Action { + Investment, + Redemption, +} + +/// Identification of a foreign investment/redemption +pub type ForeignId = ( ::AccountId, ::InvestmentId, - crate::types::TokenSwapReason, ->; + Action, +); + +pub type SwapOf = Swap<::Balance, ::CurrencyId>; +pub type TrancheIdOf = <::PoolInspect as cfg_traits::PoolInspect< + ::AccountId, + ::CurrencyId, +>>::TrancheId; +pub type PoolIdOf = <::PoolInspect as cfg_traits::PoolInspect< + ::AccountId, + ::CurrencyId, +>>::PoolId; + +/// Get the pool currency associated to a investment_id +pub fn pool_currency_of( + investment_id: T::InvestmentId, +) -> Result { + use cfg_traits::{investments::TrancheCurrency, PoolInspect}; + + T::PoolInspect::currency_for(investment_id.of_pool()).ok_or(Error::::PoolNotFound.into()) +} #[frame_support::pallet] pub mod pallet { use cfg_traits::{ - investments::{Investment as InvestmentT, InvestmentCollector, TrancheCurrency}, - PoolInspect, StatusNotificationHook, TokenSwaps, - }; - use cfg_types::investments::{ - CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, + investments::{Investment, InvestmentCollector, TrancheCurrency}, + IdentityCurrencyConversion, PoolInspect, StatusNotificationHook, TokenSwaps, }; - use errors::{InvestError, RedeemError}; - use frame_support::{dispatch::HasCompact, pallet_prelude::*}; - use sp_runtime::traits::AtLeast32BitUnsigned; - use types::{InvestState, InvestStateConfig, RedeemState}; + use cfg_types::investments::{ExecutedForeignCollect, ExecutedForeignDecreaseInvest}; + use frame_support::pallet_prelude::*; + use sp_runtime::{traits::AtLeast32BitUnsigned, FixedPointOperand}; use super::*; @@ -90,47 +113,41 @@ pub mod pallet { /// depends. #[pallet::config] pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's - /// definition of an event. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - /// Type representing the weight of this pallet - type WeightInfo: frame_system::WeightInfo; - /// The source of truth for the balance of accounts type Balance: Parameter + Member + AtLeast32BitUnsigned + + FixedPointOperand + Default + Copy + MaybeSerializeDeserialize + MaxEncodedLen; - /// The currency type of transferrable tokens - type CurrencyId: Parameter + Member + Copy + TypeInfo + MaxEncodedLen; + /// Type for price ratio for cost of incoming currency relative to + /// outgoing + type BalanceRatio: Parameter + + Member + + sp_runtime::FixedPointNumber + + sp_runtime::traits::EnsureMul + + sp_runtime::traits::EnsureDiv + + MaybeSerializeDeserialize + + MaxEncodedLen; - /// The pool id type required for the investment identifier - type PoolId: Member - + Parameter - + Default - + Copy - + HasCompact - + MaxEncodedLen - + core::fmt::Debug; + /// The token swap order identifying type + type SwapId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord + MaxEncodedLen; - /// The tranche id type required for the investment identifier - type TrancheId: Member + Parameter + Default + Copy + MaxEncodedLen + TypeInfo; + /// The currency type of transferrable tokens + type CurrencyId: Parameter + Member + Copy + MaxEncodedLen; /// The investment identifying type required for the investment type - type InvestmentId: TrancheCurrency - + Clone - + Member + type InvestmentId: TrancheCurrency, TrancheIdOf> + Parameter + Copy + MaxEncodedLen; /// The internal investment type which handles the actual investment on /// top of the wrapper implementation of this Pallet - type Investment: InvestmentT< + type Investment: Investment< Self::AccountId, Amount = Self::Balance, CurrencyId = Self::CurrencyId, @@ -143,78 +160,34 @@ pub mod pallet { Result = (), >; - /// Type for price ratio for cost of incoming currency relative to - /// outgoing - type BalanceRatio: Parameter - + Member - + sp_runtime::FixedPointNumber - + sp_runtime::traits::EnsureMul - + sp_runtime::traits::EnsureDiv - + MaybeSerializeDeserialize - + TypeInfo - + MaxEncodedLen; - - /// The default sell rate for token swaps which will be applied to all - /// swaps created/updated through Foreign Investments. - /// - /// Example: Say this rate is set to 3/2, then the incoming currency - /// should never cost more than 1.5 of the outgoing currency. - /// - /// NOTE: Can be removed once we implement a - /// more sophisticated swap price discovery. For now, this should be set - /// to one. - #[pallet::constant] - type DefaultTokenSellRatio: Get; - - /// The token swap order identifying type - type TokenSwapOrderId: Parameter - + Member - + Copy - + MaybeSerializeDeserialize - + Ord - + TypeInfo - + MaxEncodedLen; - /// The type which exposes token swap order functionality such as /// placing and cancelling orders type TokenSwaps: TokenSwaps< Self::AccountId, CurrencyId = Self::CurrencyId, Balance = Self::Balance, - OrderId = Self::TokenSwapOrderId, - OrderDetails = Swap, + OrderId = Self::SwapId, + OrderDetails = SwapOf, SellRatio = Self::BalanceRatio, >; /// The hook type which acts upon a finalized investment decrement. type DecreasedForeignInvestOrderHook: StatusNotificationHook< - Id = cfg_types::investments::ForeignInvestmentInfo< - Self::AccountId, - Self::InvestmentId, - (), - >, + Id = (Self::AccountId, Self::InvestmentId), Status = ExecutedForeignDecreaseInvest, Error = DispatchError, >; /// The hook type which acts upon a finalized redemption collection. type CollectedForeignRedemptionHook: StatusNotificationHook< - Id = cfg_types::investments::ForeignInvestmentInfo< - Self::AccountId, - Self::InvestmentId, - (), - >, + Id = (Self::AccountId, Self::InvestmentId), Status = ExecutedForeignCollect, Error = DispatchError, >; /// The hook type which acts upon a finalized redemption collection. type CollectedForeignInvestmentHook: StatusNotificationHook< - Id = cfg_types::investments::ForeignInvestmentInfo< - Self::AccountId, - Self::InvestmentId, - (), - >, + Id = (Self::AccountId, Self::InvestmentId), Status = ExecutedForeignCollect, Error = DispatchError, >; @@ -227,230 +200,76 @@ pub mod pallet { /// restricted to a more sophisticated trait which provides /// unidirectional conversions based on an oracle, dynamic prices or at /// least conversion ratios based on specific currency pairs. - type CurrencyConverter: cfg_traits::IdentityCurrencyConversion< + type CurrencyConverter: IdentityCurrencyConversion< Balance = Self::Balance, Currency = Self::CurrencyId, Error = DispatchError, >; /// The source of truth for pool currencies. - type PoolInspect: PoolInspect< - Self::AccountId, - Self::CurrencyId, - PoolId = Self::PoolId, - TrancheId = Self::TrancheId, - >; + type PoolInspect: PoolInspect; } - /// Aux type for configurations that inherents from `Config` - #[derive(PartialEq)] - pub struct Of(PhantomData); - - impl InvestStateConfig for Of { - type Balance = T::Balance; - type CurrencyConverter = T::CurrencyConverter; - type CurrencyId = T::CurrencyId; - } - - /// Maps an investor and their `InvestmentId` to the corresponding - /// `InvestState`. + /// Contains the information about the foreign investment process /// - /// NOTE: The lifetime of this storage starts with initializing a currency - /// swap into the required pool currency and ends upon fully processing the - /// investment after the potential swap. In case a swap is not required, the - /// investment starts with `InvestState::InvestmentOngoing`. + /// NOTE: The storage is killed once the investment is fully collected, or + /// decreased. #[pallet::storage] - pub type InvestmentState = StorageDoubleMap< + pub(super) type ForeignInvestmentInfo = StorageDoubleMap< _, Blake2_128Concat, T::AccountId, Blake2_128Concat, T::InvestmentId, - InvestState>, - ValueQuery, + entities::InvestmentInfo, >; - /// Maps an investor and their `InvestmentId` to the corresponding - /// `RedeemState`. + /// Contains the information about the foreign redemption process /// - /// NOTE: The lifetime of this storage starts with increasing a redemption - /// which requires owning at least the amount of tranche tokens by which the - /// redemption shall be increased by. It ends with transferring back - /// the swapped return currency to the corresponding source domain from - /// which the investment originated. The lifecycle must go through the - /// following stages: - /// 1. Increase redemption --> Initialize storage - /// 2. Fully process pending redemption - /// 3. Collect redemption - /// 4. Trigger swap from pool to return currency - /// 5. Completely fulfill swap order - /// 6. Transfer back to source domain --> Kill storage entry + /// NOTE: The storage is killed once the redemption is fully collected and + /// fully swapped or decreased #[pallet::storage] - pub type RedemptionState = StorageDoubleMap< + pub(super) type ForeignRedemptionInfo = StorageDoubleMap< _, Blake2_128Concat, T::AccountId, Blake2_128Concat, T::InvestmentId, - RedeemState, - ValueQuery, + entities::RedemptionInfo, >; - /// Maps a token swap order id to the corresponding `ForeignInvestmentInfo` - /// to implicitly enable mapping to `InvestmentState` and `RedemptionState`. + /// Maps a `SwapId` to its corresponding `ForeignId` /// - /// NOTE: The storage is immediately killed when the swap order is - /// completely fulfilled even if the corresponding investment and/or - /// redemption might not be fully processed. + /// NOTE: The storage is killed when the swap order no longer exists #[pallet::storage] - #[pallet::getter(fn foreign_investment_info)] - pub(super) type ForeignInvestmentInfo = - StorageMap<_, Blake2_128Concat, T::TokenSwapOrderId, ForeignInvestmentInfoOf>; - - /// Maps an investor and their `InvestmentId` to the corresponding - /// `TokenSwapOrderId`. - /// - /// NOTE: The storage is immediately killed when the swap order is - /// completely fulfilled even if the investment might not be fully - /// processed. - #[pallet::storage] - #[pallet::getter(fn token_swap_order_ids)] - pub(super) type TokenSwapOrderIds = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::InvestmentId, - T::TokenSwapOrderId, - >; + pub(super) type SwapIdToForeignId = + StorageMap<_, Blake2_128Concat, T::SwapId, ForeignId>; - /// Maps an investor and their `InvestmentId` to the collected investment - /// amount, i.e., the payment amount of pool currency burned for the - /// conversion into collected amount of tranche tokens based on the - /// fulfillment price(s). + /// Maps a `ForeignId` to its corresponding `SwapId` /// - /// NOTE: The lifetime of this storage starts with receiving a notification - /// of an executed investment via the `CollectedInvestmentHook`. It ends - /// with transferring the collected tranche tokens by executing - /// `notify_executed_collect_invest` which is part of - /// `collect_foreign_investment`. + /// NOTE: The storage is killed when the swap order no longer exists #[pallet::storage] - pub type CollectedInvestment = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::InvestmentId, - CollectedAmount, - ValueQuery, - >; - - /// Maps an investor and their `InvestmentId` to the collected redemption - /// amount, i.e., the payment amount of tranche tokens burned for the - /// conversion into collected pool currency based on the - /// fulfillment price(s). - /// - /// NOTE: The lifetime of this storage starts with receiving a notification - /// of an executed redemption collection into pool currency via the - /// `CollectedRedemptionHook`. It ends with having swapped the entire amount - /// to foreign currency which is assumed to be asynchronous. - #[pallet::storage] - pub type CollectedRedemption = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::InvestmentId, - CollectedAmount, - ValueQuery, - >; - - /// Maps an investor and their investment id to the foreign payment currency - /// provided on the initial investment increment. - /// - /// The lifetime is synchronized with the one of - /// `InvestmentState`. - #[pallet::storage] - pub type InvestmentPaymentCurrency = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::InvestmentId, - T::CurrencyId, - ResultQuery::InvestmentPaymentCurrencyNotFound>, - >; - - /// Maps an investor and their investment id to the foreign payout currency - /// requested on the initial redemption increment. - /// - /// The lifetime is synchronized with the one of - /// `RedemptionState`. - #[pallet::storage] - pub type RedemptionPayoutCurrency = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::InvestmentId, - T::CurrencyId, - ResultQuery::RedemptionPayoutCurrencyNotFound>, - >; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - ForeignInvestmentUpdated { - investor: T::AccountId, - investment_id: T::InvestmentId, - state: InvestState>, - }, - ForeignInvestmentCleared { - investor: T::AccountId, - investment_id: T::InvestmentId, - }, - ForeignRedemptionUpdated { - investor: T::AccountId, - investment_id: T::InvestmentId, - state: RedeemState, - }, - ForeignRedemptionCleared { - investor: T::AccountId, - investment_id: T::InvestmentId, - }, - } + pub(super) type ForeignIdToSwapId = + StorageMap<_, Blake2_128Concat, ForeignId, T::SwapId>; #[pallet::error] pub enum Error { - /// Failed to retrieve the foreign payment currency for a collected - /// investment. - /// - /// NOTE: This error can only occur, if a user tries to collect before - /// having increased their investment as this would store the payment - /// currency. - InvestmentPaymentCurrencyNotFound, - /// Failed to retrieve the foreign payout currency for a collected - /// redemption. - /// - /// NOTE: This error can only occur, if a user tries to collect before - /// having increased their redemption as this would store the payout - /// currency. - RedemptionPayoutCurrencyNotFound, - /// Failed to retrieve the `TokenSwapReason` from the given - /// `TokenSwapOrderId`. - InvestmentInfoNotFound, - /// Failed to retrieve the `TokenSwapReason` from the given - /// `TokenSwapOrderId`. - TokenSwapReasonNotFound, - /// The fulfilled token swap amount exceeds the sum of active swap - /// amounts of the corresponding `InvestmentState` and - /// `RedemptionState`. - FulfilledTokenSwapAmountOverflow, - /// Failed to transition the `InvestState`. - InvestError(InvestError), - /// Failed to transition the `RedeemState.` - RedeemError(RedeemError), + /// Failed to retrieve the `ForeignInvestInfo`. + InfoNotFound, + + /// Failed to retrieve the swap order. + SwapOrderNotFound, + /// Failed to retrieve the pool for the given pool id. PoolNotFound, + + /// An action for a different foreign currency is currently in process + /// for the same pool currency, account, and investment. + /// The currenct foreign actions must be finished before starting with a + /// different foreign currency investment / redemption. + MismatchedForeignCurrency, + + /// The decrease is greater than the current investment/redemption + TooMuchDecrease, } } diff --git a/pallets/foreign-investments/src/mock.rs b/pallets/foreign-investments/src/mock.rs index f6fdf7188f..a3170294be 100644 --- a/pallets/foreign-investments/src/mock.rs +++ b/pallets/foreign-investments/src/mock.rs @@ -3,9 +3,7 @@ use cfg_mocks::{ pallet_mock_status_notification, pallet_mock_token_swaps, }; use cfg_traits::investments::TrancheCurrency; -use cfg_types::investments::{ - ExecutedForeignCollect, ExecutedForeignDecreaseInvest, ForeignInvestmentInfo, Swap, -}; +use cfg_types::investments::{ExecutedForeignCollect, ExecutedForeignDecreaseInvest, Swap}; use frame_support::traits::{ConstU16, ConstU32, ConstU64}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -29,7 +27,7 @@ pub type AccountId = u64; pub type Balance = u128; pub type TrancheId = u32; pub type PoolId = u64; -pub type OrderId = u64; +pub type SwapId = u64; pub type CurrencyId = u8; pub type Ratio = FixedU128; @@ -50,10 +48,6 @@ impl TrancheCurrency for InvestmentId { } } -frame_support::parameter_types! { - pub DefaultTokenSellRatio: Ratio = FixedU128::from_float(1.5); -} - // ====================== // Runtime config // ====================== @@ -68,8 +62,8 @@ frame_support::construct_runtime!( MockInvestment: pallet_mock_investment, MockTokenSwaps: pallet_mock_token_swaps, MockDecreaseInvestHook: pallet_mock_status_notification::, - MockCollectRedeemHook: pallet_mock_status_notification::, - MockCollectInvestHook: pallet_mock_status_notification::, + MockCollectInvestHook: pallet_mock_status_notification::, + MockCollectRedeemHook: pallet_mock_status_notification::, MockCurrencyConversion: pallet_mock_currency_conversion, MockPools: pallet_mock_pools, ForeignInvestment: pallet_foreign_investments, @@ -113,25 +107,25 @@ impl pallet_mock_token_swaps::Config for Runtime { type Balance = Balance; type CurrencyId = CurrencyId; type OrderDetails = Swap; - type OrderId = OrderId; + type OrderId = SwapId; type SellRatio = FixedU128; } type Hook1 = pallet_mock_status_notification::Instance1; impl pallet_mock_status_notification::Config for Runtime { - type Id = ForeignInvestmentInfo; + type Id = (AccountId, InvestmentId); type Status = ExecutedForeignDecreaseInvest; } type Hook2 = pallet_mock_status_notification::Instance2; impl pallet_mock_status_notification::Config for Runtime { - type Id = ForeignInvestmentInfo; + type Id = (AccountId, InvestmentId); type Status = ExecutedForeignCollect; } type Hook3 = pallet_mock_status_notification::Instance3; impl pallet_mock_status_notification::Config for Runtime { - type Id = ForeignInvestmentInfo; + type Id = (AccountId, InvestmentId); type Status = ExecutedForeignCollect; } @@ -157,16 +151,11 @@ impl pallet_foreign_investments::Config for Runtime { type CurrencyConverter = MockCurrencyConversion; type CurrencyId = CurrencyId; type DecreasedForeignInvestOrderHook = MockDecreaseInvestHook; - type DefaultTokenSellRatio = DefaultTokenSellRatio; type Investment = MockInvestment; type InvestmentId = InvestmentId; - type PoolId = PoolId; type PoolInspect = MockPools; - type RuntimeEvent = RuntimeEvent; - type TokenSwapOrderId = OrderId; + type SwapId = SwapId; type TokenSwaps = MockTokenSwaps; - type TrancheId = TrancheId; - type WeightInfo = (); } pub fn new_test_ext() -> sp_io::TestExternalities { @@ -174,9 +163,5 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .build_storage::() .unwrap(); - let mut ext = sp_io::TestExternalities::new(storage); - ext.execute_with(|| { - // Initialize default mocking methods here - }); - ext + sp_io::TestExternalities::new(storage) } diff --git a/pallets/foreign-investments/src/swaps.rs b/pallets/foreign-investments/src/swaps.rs new file mode 100644 index 0000000000..c54f38ae93 --- /dev/null +++ b/pallets/foreign-investments/src/swaps.rs @@ -0,0 +1,250 @@ +//! Abstracts the swapping logic + +use cfg_traits::{IdentityCurrencyConversion, TokenSwaps}; +use frame_support::pallet_prelude::*; +use sp_runtime::traits::{EnsureAdd, EnsureSub, One, Zero}; +use sp_std::cmp::Ordering; + +use crate::{ + pallet::{Config, Error}, + Action, ForeignIdToSwapId, SwapIdToForeignId, SwapOf, +}; + +/// Internal type used as result of `Pallet::apply_swap()` +/// Amounts are donominated referenced by the `new_swap` paramenter given to +/// `apply_swap()` +#[derive(RuntimeDebugNoBound, PartialEq)] +pub struct SwapStatus { + /// The amount (in) already swapped and available to use. + pub swapped: T::Balance, + + /// The amount (in) pending to be swapped + pub pending: T::Balance, + + /// The swap id for a possible reminder swap order after `apply_swap()` + pub swap_id: Option, +} + +/// Type that has methods related to swap actions +pub struct Swaps(PhantomData); +impl Swaps { + /// Inserts, updates or removes a swap id associated to a foreign + /// action. + pub fn update_id( + who: &T::AccountId, + investment_id: T::InvestmentId, + action: Action, + new_swap_id: Option, + ) -> DispatchResult { + let previous_swap_id = ForeignIdToSwapId::::get((who, investment_id, action)); + + if previous_swap_id != new_swap_id { + if let Some(new_id) = new_swap_id { + SwapIdToForeignId::::insert(new_id, (who.clone(), investment_id, action)); + ForeignIdToSwapId::::insert((who.clone(), investment_id, action), new_id); + } + + if let Some(old_id) = previous_swap_id { + SwapIdToForeignId::::remove(old_id); + ForeignIdToSwapId::::remove((who.clone(), investment_id, action)); + } + } + + Ok(()) + } + + pub fn foreign_id_from( + swap_id: T::SwapId, + ) -> Result<(T::AccountId, T::InvestmentId, Action), DispatchError> { + SwapIdToForeignId::::get(swap_id).ok_or(Error::::SwapOrderNotFound.into()) + } + + pub fn swap_id_from( + account: &T::AccountId, + investment_id: T::InvestmentId, + action: Action, + ) -> Result { + ForeignIdToSwapId::::get((account, investment_id, action)) + .ok_or(Error::::SwapOrderNotFound.into()) + } + + /// Returns the pending swap amount denominated in the given currency + pub fn any_pending_amount_demominated_in( + who: &T::AccountId, + investment_id: T::InvestmentId, + action: Action, + currency: T::CurrencyId, + ) -> Result { + ForeignIdToSwapId::::get((who, investment_id, action)) + .and_then(T::TokenSwaps::get_order_details) + .map(|swap| { + if swap.currency_in == currency { + Ok(swap.amount_in) + } else { + T::CurrencyConverter::stable_to_stable( + currency, + swap.currency_in, + swap.amount_in, + ) + } + }) + .unwrap_or(Ok(T::Balance::default())) + } + + /// Returns the pending swap amount for the direction that ends up in + /// `currency_in` + pub fn pending_amount_for( + who: &T::AccountId, + investment_id: T::InvestmentId, + action: Action, + currency_in: T::CurrencyId, + ) -> T::Balance { + ForeignIdToSwapId::::get((who, investment_id, action)) + .and_then(T::TokenSwaps::get_order_details) + .filter(|swap| swap.currency_in == currency_in) + .map(|swap| swap.amount_in) + .unwrap_or_default() + } + + /// A wrap over `apply_over_swap()` that makes the swap from an + /// investment PoV + pub fn apply( + who: &T::AccountId, + investment_id: T::InvestmentId, + action: Action, + new_swap: SwapOf, + ) -> Result, DispatchError> { + // Bypassing the swap if both currencies are the same + if new_swap.currency_in == new_swap.currency_out { + return Ok(SwapStatus { + swapped: new_swap.amount_in, + pending: T::Balance::zero(), + swap_id: None, + }); + } + + let swap_id = ForeignIdToSwapId::::get((who, investment_id, action)); + let status = Swaps::::apply_over_swap(who, new_swap.clone(), swap_id)?; + Swaps::::update_id(who, investment_id, action, status.swap_id)?; + + Ok(status) + } + + /// Apply a swap over a current possible swap state. + /// - If there was no previous swap, it adds it. + /// - If there was a swap in the same direction, it increments it. + /// - If there was a swap in the opposite direction: + /// - If the amount is smaller, it decrements it. + /// - If the amount is the same, it removes the inverse swap. + /// - If the amount is greater, it removes the inverse swap and create + /// another with the excess + /// + /// The returned status contains the swapped amounts after this call and + /// the pending amounts to be swapped of both swap directions. + pub fn apply_over_swap( + who: &T::AccountId, + new_swap: SwapOf, + over_swap_id: Option, + ) -> Result, DispatchError> { + match over_swap_id { + None => { + let swap_id = T::TokenSwaps::place_order( + who.clone(), + new_swap.currency_in, + new_swap.currency_out, + new_swap.amount_in, + T::BalanceRatio::one(), + )?; + + Ok(SwapStatus { + swapped: T::Balance::zero(), + pending: new_swap.amount_in, + swap_id: Some(swap_id), + }) + } + Some(swap_id) => { + let swap = T::TokenSwaps::get_order_details(swap_id) + .ok_or(Error::::SwapOrderNotFound)?; + + if swap.is_same_direction(&new_swap)? { + let amount_to_swap = swap.amount_in.ensure_add(new_swap.amount_in)?; + T::TokenSwaps::update_order( + who.clone(), + swap_id, + amount_to_swap, + T::BalanceRatio::one(), + )?; + + Ok(SwapStatus { + swapped: T::Balance::zero(), + pending: amount_to_swap, + swap_id: Some(swap_id), + }) + } else { + let inverse_swap = swap; + + let new_swap_amount_out = T::CurrencyConverter::stable_to_stable( + new_swap.currency_out, + new_swap.currency_in, + new_swap.amount_in, + )?; + + match inverse_swap.amount_in.cmp(&new_swap_amount_out) { + Ordering::Greater => { + let amount_to_swap = + inverse_swap.amount_in.ensure_sub(new_swap_amount_out)?; + + T::TokenSwaps::update_order( + who.clone(), + swap_id, + amount_to_swap, + T::BalanceRatio::one(), + )?; + + Ok(SwapStatus { + swapped: new_swap.amount_in, + pending: T::Balance::zero(), + swap_id: Some(swap_id), + }) + } + Ordering::Equal => { + T::TokenSwaps::cancel_order(swap_id)?; + + Ok(SwapStatus { + swapped: new_swap.amount_in, + pending: T::Balance::zero(), + swap_id: None, + }) + } + Ordering::Less => { + T::TokenSwaps::cancel_order(swap_id)?; + + let inverse_swap_amount_out = T::CurrencyConverter::stable_to_stable( + inverse_swap.currency_out, + inverse_swap.currency_in, + inverse_swap.amount_in, + )?; + + let amount_to_swap = + new_swap.amount_in.ensure_sub(inverse_swap_amount_out)?; + + let swap_id = T::TokenSwaps::place_order( + who.clone(), + new_swap.currency_in, + new_swap.currency_out, + amount_to_swap, + T::BalanceRatio::one(), + )?; + + Ok(SwapStatus { + swapped: inverse_swap_amount_out, + pending: amount_to_swap, + swap_id: Some(swap_id), + }) + } + } + } + } + } + } +} diff --git a/pallets/foreign-investments/src/tests.rs b/pallets/foreign-investments/src/tests.rs index 2d5ca939d8..127c1bc249 100644 --- a/pallets/foreign-investments/src/tests.rs +++ b/pallets/foreign-investments/src/tests.rs @@ -1,321 +1,1365 @@ -use cfg_traits::{investments::ForeignInvestment as ForeignInvestmentT, StatusNotificationHook}; -use cfg_types::investments::ForeignInvestmentInfo as ForeignInvestmentInfoS; -use frame_support::assert_ok; +use cfg_traits::{ + investments::{ForeignInvestment as _, Investment, TrancheCurrency}, + StatusNotificationHook, TokenSwaps, +}; +use cfg_types::investments::{ + CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, Swap, +}; +use frame_support::{assert_err, assert_ok}; +use sp_runtime::traits::One; use crate::{ - hooks::FulfilledSwapOrderHook, + entities::{BaseInfo, InvestmentInfo, RedemptionInfo}, + impls::{CollectedInvestmentHook, CollectedRedemptionHook, FulfilledSwapOrderHook}, mock::*, - types::{InvestState, TokenSwapReason}, + pallet::ForeignInvestmentInfo, + swaps::{SwapStatus, Swaps}, *, }; const USER: AccountId = 1; const INVESTMENT_ID: InvestmentId = InvestmentId(42, 23); -const USER_CURR: CurrencyId = 5; +const FOREIGN_CURR: CurrencyId = 5; const POOL_CURR: CurrencyId = 10; -const ORDER_ID: OrderId = 1; +const SWAP_ID: SwapId = 1; +const STABLE_RATIO: Balance = 10; // Means: 1 foreign curr is 10 pool curr +const TRANCHE_RATIO: Balance = 5; // Means: 1 pool curr is 5 tranche curr +const AMOUNT: Balance = pool_to_foreign(200); +const TRANCHE_AMOUNT: Balance = 1000; + +/// foreign amount to pool amount +pub const fn foreign_to_pool(foreign_amount: Balance) -> Balance { + foreign_amount * STABLE_RATIO +} + +/// pool amount to foreign amount +pub const fn pool_to_foreign(pool_amount: Balance) -> Balance { + pool_amount / STABLE_RATIO +} + +/// pool amount to tranche amount +pub const fn pool_to_tranche(pool_amount: Balance) -> Balance { + pool_amount * TRANCHE_RATIO +} + +/// tranche amount to pool amount +pub const fn tranche_to_pool(tranche_amount: Balance) -> Balance { + tranche_amount / TRANCHE_RATIO +} mod util { use super::*; - pub fn new_invest(order_id: OrderId, amount: Balance) { - MockInvestment::mock_investment_requires_collect(|_, _| false); - MockInvestment::mock_investment(|_, _| Ok(0)); - MockInvestment::mock_update_investment(|_, _, _| Ok(())); - MockTokenSwaps::mock_place_order(move |_, _, _, _, _| Ok(order_id)); - MockCurrencyConversion::mock_stable_to_stable(move |_, _, _| Ok(amount) /* 1:1 */); - - ForeignInvestment::increase_foreign_investment( - &USER, - INVESTMENT_ID, - amount, - USER_CURR, - POOL_CURR, - ) - .unwrap(); + pub fn configure_currency_converter() { + MockCurrencyConversion::mock_stable_to_stable(|to, from, amount_from| match (from, to) { + (POOL_CURR, FOREIGN_CURR) => Ok(pool_to_foreign(amount_from)), + (FOREIGN_CURR, POOL_CURR) => Ok(foreign_to_pool(amount_from)), + _ => Ok(amount_from), + }); + } + + pub fn configure_pool() { + MockPools::mock_currency_for(|pool_id| { + assert_eq!(pool_id, INVESTMENT_ID.of_pool()); + Some(POOL_CURR) + }); + } + + // Setup a basic orderbook system + pub fn config_swaps() { + MockTokenSwaps::mock_get_order_details(|_| None); + + MockTokenSwaps::mock_place_order(|_, curr_in, curr_out, amount_in, _| { + MockTokenSwaps::mock_get_order_details(move |_| { + Some(Swap { + currency_in: curr_in, + currency_out: curr_out, + amount_in: amount_in, + }) + }); + Ok(SWAP_ID) + }); - MockInvestment::mock_investment_requires_collect(|_, _| unimplemented!("no mock")); - MockInvestment::mock_investment(|_, _| unimplemented!("no mock")); - MockInvestment::mock_update_investment(|_, _, _| unimplemented!("no mock")); - MockTokenSwaps::mock_place_order(|_, _, _, _, _| unimplemented!("no mock")); - MockCurrencyConversion::mock_stable_to_stable(|_, _, _| unimplemented!("no mock")); + MockTokenSwaps::mock_update_order(|_, swap_id, amount_in, _| { + let swap = MockTokenSwaps::get_order_details(swap_id).unwrap(); + MockTokenSwaps::mock_get_order_details(move |_| { + Some(Swap { + currency_in: swap.currency_in, + currency_out: swap.currency_out, + amount_in: amount_in, + }) + }); + Ok(()) + }); + + MockTokenSwaps::mock_cancel_order(|_| { + MockTokenSwaps::mock_get_order_details(|_| None); + Ok(()) + }); } - pub fn notify_swaped(order_id: OrderId, amount: Balance) { - MockInvestment::mock_investment_requires_collect(|_, _| false); + // Setup basic investment system + pub fn config_investments() { MockInvestment::mock_investment(|_, _| Ok(0)); - MockInvestment::mock_update_investment(|_, _, _| Ok(())); - MockTokenSwaps::mock_cancel_order(|_| Ok(())); - MockTokenSwaps::mock_is_active(|_| true); - MockCurrencyConversion::mock_stable_to_stable(move |_, _, _| Ok(amount) /* 1:1 */); + + MockInvestment::mock_update_investment(|_, _, new_value| { + MockInvestment::mock_investment(move |_, _| Ok(new_value)); + Ok(()) + }); + + MockInvestment::mock_redemption(|_, _| Ok(0)); + + MockInvestment::mock_update_redemption(|_, _, new_value| { + MockInvestment::mock_redemption(move |_, _| Ok(new_value)); + Ok(()) + }); + } + + pub fn base_configuration() { + util::configure_pool(); + util::configure_currency_converter(); + util::config_swaps(); + util::config_investments(); + } + + /// Emulates a swap partial fulfill + pub fn fulfill_last_swap(action: Action, amount_in: Balance) { + let swap_id = ForeignIdToSwapId::::get((USER, INVESTMENT_ID, action)).unwrap(); + let swap = MockTokenSwaps::get_order_details(swap_id).unwrap(); + MockTokenSwaps::mock_get_order_details(move |_| { + Some(Swap { + amount_in: swap.amount_in - amount_in, + ..swap + }) + }); FulfilledSwapOrderHook::::notify_status_change( - order_id, - Swap { - currency_out: USER_CURR, - currency_in: POOL_CURR, - amount, - }, + swap_id, + Swap { amount_in, ..swap }, ) .unwrap(); + } + + /// Emulates partial collected investment + pub fn allow_collect_investment(pool_amount: Balance) { + let value = MockInvestment::investment(&USER, INVESTMENT_ID).unwrap(); + MockInvestment::mock_collect_investment(move |_, _| { + MockInvestment::mock_investment(move |_, _| Ok(value - pool_amount)); + + CollectedInvestmentHook::::notify_status_change( + (USER, INVESTMENT_ID), + CollectedAmount { + amount_collected: pool_to_tranche(pool_amount), + amount_payment: pool_amount, + }, + ) + }); + } - MockInvestment::mock_investment_requires_collect(|_, _| unimplemented!("no mock")); - MockInvestment::mock_investment(|_, _| unimplemented!("no mock")); - MockInvestment::mock_update_investment(|_, _, _| unimplemented!("no mock")); - MockTokenSwaps::mock_cancel_order(|_| unimplemented!("no mock")); - MockTokenSwaps::mock_is_active(|_| unimplemented!("no mock")); - MockCurrencyConversion::mock_stable_to_stable(|_, _, _| unimplemented!("no mock")); + /// Emulates partial collected redemption + pub fn allow_collect_redemption(tranche_amount: Balance) { + let value = MockInvestment::redemption(&USER, INVESTMENT_ID).unwrap(); + MockInvestment::mock_collect_redemption(move |_, _| { + MockInvestment::mock_redemption(move |_, _| Ok(value - tranche_amount)); + + CollectedRedemptionHook::::notify_status_change( + (USER, INVESTMENT_ID), + CollectedAmount { + amount_collected: tranche_to_pool(tranche_amount), + amount_payment: tranche_amount, + }, + ) + }); } } -mod increase_investment { +mod swaps { use super::*; #[test] - fn create_new() { - const AMOUNT: Balance = 100; - + fn swap_over_no_swap() { new_test_ext().execute_with(|| { - MockInvestment::mock_investment_requires_collect(|account_id, investment_id| { - assert_eq!(account_id, &USER); - assert_eq!(investment_id, INVESTMENT_ID); - false - }); - MockInvestment::mock_investment(|account_id, investment_id| { - assert_eq!(account_id, &USER); - assert_eq!(investment_id, INVESTMENT_ID); - Ok(0) // Nothing initially invested - }); - MockInvestment::mock_update_investment(|account_id, investment_id, amount| { - assert_eq!(account_id, &USER); - assert_eq!(investment_id, INVESTMENT_ID); - assert_eq!(amount, 0); // We still do not have the swap done. - Ok(()) - }); - MockTokenSwaps::mock_place_order(|account_id, curr_in, curr_out, amount, limit| { - assert_eq!(account_id, USER); - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount, AMOUNT); - assert_eq!(limit, DefaultTokenSellRatio::get()); - Ok(ORDER_ID) - }); - MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + MockTokenSwaps::mock_place_order(|who, curr_in, curr_out, amount, ratio| { + assert_eq!(who, USER); assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount_out, AMOUNT); - Ok(amount_out) // 1:1 - }); + assert_eq!(curr_out, FOREIGN_CURR); + assert_eq!(amount, foreign_to_pool(AMOUNT)); + assert_eq!(ratio, Ratio::one()); - assert_ok!(ForeignInvestment::increase_foreign_investment( - &USER, - INVESTMENT_ID, - AMOUNT, - USER_CURR, - POOL_CURR, - )); + Ok(SWAP_ID) + }); - assert_eq!( - InvestmentState::::get(USER, INVESTMENT_ID), - InvestState::ActiveSwapIntoPoolCurrency { - swap: Swap { - currency_out: USER_CURR, + assert_ok!( + Swaps::::apply_over_swap( + &USER, + Swap { currency_in: POOL_CURR, - amount: AMOUNT, - } + currency_out: FOREIGN_CURR, + amount_in: foreign_to_pool(AMOUNT), + }, + None, + ), + SwapStatus { + swapped: 0, + pending: foreign_to_pool(AMOUNT), + swap_id: Some(SWAP_ID), } ); - assert_eq!( - TokenSwapOrderIds::::get(USER, INVESTMENT_ID), - Some(ORDER_ID) - ); - assert_eq!( - ForeignInvestmentInfo::::get(ORDER_ID), - Some(ForeignInvestmentInfoS { - owner: USER, - id: INVESTMENT_ID, - last_swap_reason: Some(TokenSwapReason::Investment), - }) - ); }); } #[test] - fn over_pending() { - const INITIAL_AMOUNT: Balance = 100; - const INCREASE_AMOUNT: Balance = 500; + fn swap_over_same_direction_swap() { + const PREVIOUS_AMOUNT: Balance = AMOUNT + pool_to_foreign(100); new_test_ext().execute_with(|| { - util::new_invest(ORDER_ID, INITIAL_AMOUNT); + MockTokenSwaps::mock_get_order_details(move |swap_id| { + assert_eq!(swap_id, SWAP_ID); - MockInvestment::mock_investment_requires_collect(|account_id, investment_id| { - assert_eq!(account_id, &USER); - assert_eq!(investment_id, INVESTMENT_ID); - false + Some(Swap { + currency_in: POOL_CURR, + currency_out: FOREIGN_CURR, + amount_in: foreign_to_pool(PREVIOUS_AMOUNT), + }) }); - MockInvestment::mock_investment(|_, _| Ok(0)); - MockInvestment::mock_update_investment(|_, _, amount| { - assert_eq!(amount, 0); + MockTokenSwaps::mock_update_order(|who, swap_id, amount, ratio| { + assert_eq!(who, USER); + assert_eq!(swap_id, SWAP_ID); + assert_eq!(amount, foreign_to_pool(PREVIOUS_AMOUNT + AMOUNT)); + assert_eq!(ratio, Ratio::one()); + Ok(()) }); - MockTokenSwaps::mock_is_active(|order_id| { - assert_eq!(order_id, ORDER_ID); - true - }); - MockTokenSwaps::mock_get_order_details(|order_id| { - assert_eq!(order_id, ORDER_ID); + + assert_ok!( + Swaps::::apply_over_swap( + &USER, + Swap { + currency_out: FOREIGN_CURR, + currency_in: POOL_CURR, + amount_in: foreign_to_pool(AMOUNT), + }, + Some(SWAP_ID), + ), + SwapStatus { + swapped: 0, + pending: foreign_to_pool(PREVIOUS_AMOUNT + AMOUNT), + swap_id: Some(SWAP_ID), + } + ); + }); + } + + #[test] + fn swap_over_greater_inverse_swap() { + const PREVIOUS_AMOUNT: Balance = AMOUNT + pool_to_foreign(100); + + new_test_ext().execute_with(|| { + util::configure_currency_converter(); + + MockTokenSwaps::mock_get_order_details(|swap_id| { + assert_eq!(swap_id, SWAP_ID); + + // Inverse swap Some(Swap { - currency_out: USER_CURR, - currency_in: POOL_CURR, - amount: INITIAL_AMOUNT, + currency_in: FOREIGN_CURR, + currency_out: POOL_CURR, + amount_in: PREVIOUS_AMOUNT, }) }); - MockTokenSwaps::mock_update_order(|account_id, order_id, amount, limit| { - assert_eq!(account_id, USER); - assert_eq!(order_id, ORDER_ID); - assert_eq!(amount, INITIAL_AMOUNT + INCREASE_AMOUNT); - assert_eq!(limit, DefaultTokenSellRatio::get()); + MockTokenSwaps::mock_update_order(|who, swap_id, amount, ratio| { + assert_eq!(who, USER); + assert_eq!(swap_id, SWAP_ID); + assert_eq!(amount, PREVIOUS_AMOUNT - AMOUNT); + assert_eq!(ratio, Ratio::one()); + Ok(()) }); - MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount_out, INCREASE_AMOUNT); - Ok(amount_out) // 1:1 + + assert_ok!( + Swaps::::apply_over_swap( + &USER, + Swap { + currency_out: FOREIGN_CURR, + currency_in: POOL_CURR, + amount_in: foreign_to_pool(AMOUNT), + }, + Some(SWAP_ID), + ), + SwapStatus { + swapped: foreign_to_pool(AMOUNT), + pending: 0, + swap_id: Some(SWAP_ID), + } + ); + }); + } + + #[test] + fn swap_over_same_inverse_swap() { + new_test_ext().execute_with(|| { + util::configure_currency_converter(); + + MockTokenSwaps::mock_get_order_details(|swap_id| { + assert_eq!(swap_id, SWAP_ID); + + // Inverse swap + Some(Swap { + currency_in: FOREIGN_CURR, + currency_out: POOL_CURR, + amount_in: AMOUNT, + }) }); + MockTokenSwaps::mock_cancel_order(|swap_id| { + assert_eq!(swap_id, SWAP_ID); - assert_ok!(ForeignInvestment::increase_foreign_investment( - &USER, - INVESTMENT_ID, - INCREASE_AMOUNT, - USER_CURR, - POOL_CURR, - )); + Ok(()) + }); - assert_eq!( - InvestmentState::::get(USER, INVESTMENT_ID), - InvestState::ActiveSwapIntoPoolCurrency { - swap: Swap { - currency_out: USER_CURR, + assert_ok!( + Swaps::::apply_over_swap( + &USER, + Swap { + currency_out: FOREIGN_CURR, currency_in: POOL_CURR, - amount: INITIAL_AMOUNT + INCREASE_AMOUNT, - } + amount_in: foreign_to_pool(AMOUNT), + }, + Some(SWAP_ID), + ), + SwapStatus { + swapped: foreign_to_pool(AMOUNT), + pending: 0, + swap_id: None, } ); }); } #[test] - fn over_ongoing() { - const INITIAL_AMOUNT: Balance = 100; - const INCREASE_AMOUNT: Balance = 500; + fn swap_over_smaller_inverse_swap() { + const PREVIOUS_AMOUNT: Balance = AMOUNT - pool_to_foreign(100); + const NEW_SWAP_ID: SwapId = SWAP_ID + 1; new_test_ext().execute_with(|| { - util::new_invest(ORDER_ID, INITIAL_AMOUNT); - util::notify_swaped(ORDER_ID, INITIAL_AMOUNT); - - MockInvestment::mock_investment_requires_collect(|_, _| false); - MockInvestment::mock_investment(|_, _| Ok(INITIAL_AMOUNT)); - MockTokenSwaps::mock_is_active(|order_id| { - assert_eq!(order_id, ORDER_ID); - false - }); - MockTokenSwaps::mock_place_order(|account_id, curr_in, curr_out, amount, limit| { - assert_eq!(account_id, USER); - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount, INCREASE_AMOUNT); - assert_eq!(limit, DefaultTokenSellRatio::get()); - Ok(ORDER_ID) + util::configure_currency_converter(); + + MockTokenSwaps::mock_get_order_details(|swap_id| { + assert_eq!(swap_id, SWAP_ID); + + // Inverse swap + Some(Swap { + currency_in: FOREIGN_CURR, + currency_out: POOL_CURR, + amount_in: PREVIOUS_AMOUNT, + }) }); - MockInvestment::mock_update_investment(|_, _, amount| { - assert_eq!(amount, 0); + MockTokenSwaps::mock_cancel_order(|swap_id| { + assert_eq!(swap_id, SWAP_ID); + Ok(()) }); - MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + MockTokenSwaps::mock_place_order(|who, curr_in, curr_out, amount, ratio| { + assert_eq!(who, USER); assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount_out, INCREASE_AMOUNT); - Ok(amount_out) // 1:1 - }); + assert_eq!(curr_out, FOREIGN_CURR); + assert_eq!(amount, foreign_to_pool(AMOUNT - PREVIOUS_AMOUNT)); + assert_eq!(ratio, Ratio::one()); - assert_ok!(ForeignInvestment::increase_foreign_investment( - &USER, - INVESTMENT_ID, - INCREASE_AMOUNT, - USER_CURR, - POOL_CURR, - )); + Ok(NEW_SWAP_ID) + }); - assert_eq!( - InvestmentState::::get(USER, INVESTMENT_ID), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - currency_out: USER_CURR, + assert_ok!( + Swaps::::apply_over_swap( + &USER, + Swap { + currency_out: FOREIGN_CURR, currency_in: POOL_CURR, - amount: INCREASE_AMOUNT, + amount_in: foreign_to_pool(AMOUNT), }, - invest_amount: INITIAL_AMOUNT + Some(SWAP_ID), + ), + SwapStatus { + swapped: foreign_to_pool(PREVIOUS_AMOUNT), + pending: foreign_to_pool(AMOUNT - PREVIOUS_AMOUNT), + swap_id: Some(NEW_SWAP_ID), } ); }); } } -mod fulfilled_swap { +mod investment { use super::*; #[test] - fn pending_investment_to_ongoing() { - const AMOUNT: Balance = 100; + fn increase() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_increase() { new_test_ext().execute_with(|| { - util::new_invest(ORDER_ID, AMOUNT); + util::base_configuration(); - MockInvestment::mock_investment_requires_collect(|_, _| false); - MockInvestment::mock_investment(|account_id, investment_id| { - assert_eq!(account_id, &USER); - assert_eq!(investment_id, INVESTMENT_ID); - Ok(0) // Nothing initially invested - }); - MockInvestment::mock_update_investment(|account_id, investment_id, amount| { - assert_eq!(account_id, &USER); + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + MockDecreaseInvestHook::mock_notify_status_change(|(who, investment_id), msg| { + assert_eq!(who, USER); assert_eq!(investment_id, INVESTMENT_ID); - assert_eq!(amount, AMOUNT); + assert_eq!( + msg, + ExecutedForeignDecreaseInvest { + amount_decreased: AMOUNT, + foreign_currency: FOREIGN_CURR, + amount_remaining: 0, + } + ); Ok(()) }); - MockTokenSwaps::mock_is_active(|order_id| { - assert_eq!(order_id, ORDER_ID); - true - }); - MockTokenSwaps::mock_cancel_order(|order_id| { - assert_eq!(order_id, ORDER_ID); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_partial_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + MockDecreaseInvestHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignDecreaseInvest { + amount_decreased: AMOUNT / 4, + foreign_currency: FOREIGN_CURR, + amount_remaining: 3 * AMOUNT / 4, + } + ); Ok(()) }); - MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount_out, AMOUNT); - Ok(amount_out) // 1:1 - }); - assert_ok!(FulfilledSwapOrderHook::::notify_status_change( - ORDER_ID, - Swap { - currency_out: USER_CURR, - currency_in: POOL_CURR, - amount: AMOUNT, - }, + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT / 4, + FOREIGN_CURR )); assert_eq!( - InvestmentState::::get(USER, INVESTMENT_ID), - InvestState::InvestmentOngoing { - invest_amount: AMOUNT - }, + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_big_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_err!( + ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT * 2, + FOREIGN_CURR + ), + Error::::TooMuchDecrease, + ); + }); + } + + #[test] + fn increase_and_partial_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT / 4)); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(AMOUNT / 4)) ); - assert_eq!(TokenSwapOrderIds::::get(USER, INVESTMENT_ID), None); - assert_eq!(ForeignInvestmentInfo::::get(ORDER_ID), None); + }); + } + + #[test] + fn increase_and_partial_fulfill_and_partial_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(3 * AMOUNT / 4)); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT / 2, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: AMOUNT / 4, + }) + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(AMOUNT / 2)) + ); + }); + } + + #[test] + fn increase_and_partial_fulfill_and_partial_decrease_and_increase() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(3 * AMOUNT / 4)); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT / 2, + FOREIGN_CURR + )); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(3 * AMOUNT / 4)) + ); + }); + } + + #[test] + fn increase_and_fulfill_and_decrease_and_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT)); + + MockDecreaseInvestHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignDecreaseInvest { + amount_decreased: AMOUNT, + foreign_currency: FOREIGN_CURR, + amount_remaining: 0, + } + ); + Ok(()) + }); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, AMOUNT); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_fulfill_and_partial_decrease_and_partial_fulfill_and_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT)); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + 3 * AMOUNT / 4, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, AMOUNT / 4); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: AMOUNT / 4, + }) + ); + + MockDecreaseInvestHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignDecreaseInvest { + amount_decreased: 3 * AMOUNT / 4, + foreign_currency: FOREIGN_CURR, + amount_remaining: AMOUNT / 4, + } + ); + Ok(()) + }); + + util::fulfill_last_swap(Action::Investment, AMOUNT / 2); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + decrease_swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(AMOUNT / 4)) + ); + }); + } + + #[test] + fn increase_and_partial_fulfill_and_partial_collect() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT / 2)); + util::allow_collect_investment(foreign_to_pool(AMOUNT / 4)); + + MockCollectInvestHook::mock_notify_status_change(|(who, investment_id), msg| { + assert_eq!(who, USER); + assert_eq!(investment_id, INVESTMENT_ID); + assert_eq!( + msg, + ExecutedForeignCollect { + currency: FOREIGN_CURR, + amount_currency_payout: AMOUNT / 4, + amount_tranche_tokens_payout: pool_to_tranche(foreign_to_pool(AMOUNT / 4)), + amount_remaining: 3 * AMOUNT / 4, + } + ); + Ok(()) + }); + + assert_ok!(ForeignInvestment::collect_foreign_investment( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + Some(InvestmentInfo { + base: BaseInfo { + foreign_currency: FOREIGN_CURR, + collected: CollectedAmount { + amount_collected: pool_to_tranche(foreign_to_pool(AMOUNT / 4)), + amount_payment: foreign_to_pool(AMOUNT / 4) + } + }, + decrease_swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(AMOUNT / 4)) + ); + }); + } + + #[test] + fn increase_and_partial_fulfill_and_partial_collect_and_decrease_and_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT / 2)); + util::allow_collect_investment(foreign_to_pool(AMOUNT / 4)); + + MockCollectInvestHook::mock_notify_status_change(|_, _| Ok(())); + + assert_ok!(ForeignInvestment::collect_foreign_investment( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + MockDecreaseInvestHook::mock_notify_status_change(|_, _| Ok(())); + + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + 3 * AMOUNT / 4, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, AMOUNT / 4); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::investment(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_fulfill_and_collect() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + FOREIGN_CURR + )); + + util::fulfill_last_swap(Action::Investment, foreign_to_pool(AMOUNT)); + util::allow_collect_investment(foreign_to_pool(AMOUNT)); + + MockCollectInvestHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignCollect { + currency: FOREIGN_CURR, + amount_currency_payout: AMOUNT, + amount_tranche_tokens_payout: pool_to_tranche(foreign_to_pool(AMOUNT)), + amount_remaining: 0, + } + ); + Ok(()) + }); + + assert_ok!(ForeignInvestment::collect_foreign_investment( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(0)) + ); + }); + } + + mod same_currencies { + use super::*; + + #[test] + fn increase() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + // Automatically "fulfills" because there no need of swapping + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + foreign_to_pool(AMOUNT), + POOL_CURR + )); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(AMOUNT)) + ); + }); + } + + #[test] + fn increase_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + foreign_to_pool(AMOUNT), + POOL_CURR + )); + + MockDecreaseInvestHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignDecreaseInvest { + amount_decreased: foreign_to_pool(AMOUNT), + foreign_currency: POOL_CURR, + amount_remaining: 0, + } + ); + Ok(()) + }); + + // Automatically "fulfills" because there no need of swapping + assert_ok!(ForeignInvestment::decrease_foreign_investment( + &USER, + INVESTMENT_ID, + foreign_to_pool(AMOUNT), + POOL_CURR + )); + + assert_eq!( + ForeignInvestment::investment(&USER, INVESTMENT_ID), + Ok(foreign_to_pool(0)) + ); + + assert_eq!( + ForeignInvestmentInfo::::get(&USER, INVESTMENT_ID), + None, + ); + }); + } + } +} + +mod redemption { + use super::*; + + #[test] + fn increase() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + Some(RedemptionInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::redemption(&USER, INVESTMENT_ID), + Ok(TRANCHE_AMOUNT) + ); + }); + } + + #[test] + fn increase_and_increase() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + Some(RedemptionInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::redemption(&USER, INVESTMENT_ID), + Ok(TRANCHE_AMOUNT + TRANCHE_AMOUNT) + ); + }); + } + + #[test] + fn increase_and_decrease() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + assert_ok!(ForeignInvestment::decrease_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::redemption(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + #[test] + fn increase_and_partial_collect() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + util::allow_collect_redemption(3 * TRANCHE_AMOUNT / 4); + + assert_ok!(ForeignInvestment::collect_foreign_redemption( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + Some(RedemptionInfo { + base: BaseInfo { + foreign_currency: FOREIGN_CURR, + collected: CollectedAmount { + amount_collected: tranche_to_pool(3 * TRANCHE_AMOUNT / 4), + amount_payment: 3 * TRANCHE_AMOUNT / 4 + } + }, + swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::redemption(&USER, INVESTMENT_ID), + Ok(TRANCHE_AMOUNT / 4) + ); + }); + } + + #[test] + fn increase_and_partial_collect_and_partial_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + util::allow_collect_redemption(3 * TRANCHE_AMOUNT / 4); + + assert_ok!(ForeignInvestment::collect_foreign_redemption( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + util::fulfill_last_swap( + Action::Redemption, + pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT / 2)), + ); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + Some(RedemptionInfo { + base: BaseInfo { + foreign_currency: FOREIGN_CURR, + collected: CollectedAmount { + amount_collected: tranche_to_pool(3 * TRANCHE_AMOUNT / 4), + amount_payment: 3 * TRANCHE_AMOUNT / 4 + } + }, + swapped_amount: pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT / 2)), + }) + ); + + assert_eq!( + ForeignInvestment::redemption(&USER, INVESTMENT_ID), + Ok(TRANCHE_AMOUNT / 4) + ); + }); + } + + #[test] + fn increase_and_partial_collect_and_partial_fulfill_and_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + util::allow_collect_redemption(3 * TRANCHE_AMOUNT / 4); + + assert_ok!(ForeignInvestment::collect_foreign_redemption( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + util::fulfill_last_swap( + Action::Redemption, + pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT / 2)), + ); + + MockCollectRedeemHook::mock_notify_status_change(|(who, investment_id), msg| { + assert_eq!(who, USER); + assert_eq!(investment_id, INVESTMENT_ID); + assert_eq!( + msg, + ExecutedForeignCollect { + currency: FOREIGN_CURR, + amount_currency_payout: pool_to_foreign(tranche_to_pool( + 3 * TRANCHE_AMOUNT / 4 + )), + amount_tranche_tokens_payout: 3 * TRANCHE_AMOUNT / 4, + amount_remaining: TRANCHE_AMOUNT / 4, + } + ); + Ok(()) + }); + + util::fulfill_last_swap( + Action::Redemption, + pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT / 4)), + ); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + Some(RedemptionInfo { + base: BaseInfo::new(FOREIGN_CURR).unwrap(), + swapped_amount: 0, + }) + ); + + assert_eq!( + ForeignInvestment::redemption(&USER, INVESTMENT_ID), + Ok(TRANCHE_AMOUNT / 4) + ); + }); + } + + #[test] + fn increase_and_collect_and_fulfill() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + FOREIGN_CURR + )); + + util::allow_collect_redemption(TRANCHE_AMOUNT); + + assert_ok!(ForeignInvestment::collect_foreign_redemption( + &USER, + INVESTMENT_ID, + FOREIGN_CURR + )); + + MockCollectRedeemHook::mock_notify_status_change(|(who, investment_id), msg| { + assert_eq!(who, USER); + assert_eq!(investment_id, INVESTMENT_ID); + assert_eq!( + msg, + ExecutedForeignCollect { + currency: FOREIGN_CURR, + amount_currency_payout: pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT)), + amount_tranche_tokens_payout: TRANCHE_AMOUNT, + amount_remaining: 0, + } + ); + Ok(()) + }); + + util::fulfill_last_swap( + Action::Redemption, + pool_to_foreign(tranche_to_pool(TRANCHE_AMOUNT)), + ); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::redemption(&USER, INVESTMENT_ID), Ok(0)); + }); + } + + mod same_currencies { + use super::*; + + #[test] + fn increase_and_collect() { + new_test_ext().execute_with(|| { + util::base_configuration(); + + assert_ok!(ForeignInvestment::increase_foreign_redemption( + &USER, + INVESTMENT_ID, + TRANCHE_AMOUNT, + POOL_CURR, + )); + + util::allow_collect_redemption(TRANCHE_AMOUNT); + + MockCollectRedeemHook::mock_notify_status_change(|_, msg| { + assert_eq!( + msg, + ExecutedForeignCollect { + currency: POOL_CURR, + amount_currency_payout: tranche_to_pool(TRANCHE_AMOUNT), + amount_tranche_tokens_payout: TRANCHE_AMOUNT, + amount_remaining: 0, + } + ); + Ok(()) + }); + + // Automatically "fulfills" because there no need of swapping + assert_ok!(ForeignInvestment::collect_foreign_redemption( + &USER, + INVESTMENT_ID, + POOL_CURR + )); + + assert_eq!( + ForeignRedemptionInfo::::get(&USER, INVESTMENT_ID), + None, + ); + + assert_eq!(ForeignInvestment::redemption(&USER, INVESTMENT_ID), Ok(0)); + }); + } + } +} + +mod notifications { + use super::*; + + #[test] + fn fulfill_not_fail_if_not_found() { + new_test_ext().execute_with(|| { + assert_ok!(FulfilledSwapOrderHook::::notify_status_change( + SWAP_ID, + Swap { + amount_in: 0, + currency_in: 0, + currency_out: 0 + }, + )); + }); + } + + #[test] + fn collect_investment_not_fail_if_not_found() { + new_test_ext().execute_with(|| { + assert_ok!(CollectedInvestmentHook::::notify_status_change( + (USER, INVESTMENT_ID), + CollectedAmount { + amount_collected: 0, + amount_payment: 0, + }, + )); + }); + } + + #[test] + fn collect_redemption_not_fail_if_not_found() { + new_test_ext().execute_with(|| { + assert_ok!(CollectedRedemptionHook::::notify_status_change( + (USER, INVESTMENT_ID), + CollectedAmount { + amount_collected: 0, + amount_payment: 0, + }, + )); }); } } diff --git a/pallets/foreign-investments/src/types.rs b/pallets/foreign-investments/src/types.rs deleted file mode 100644 index ab174ad87e..0000000000 --- a/pallets/foreign-investments/src/types.rs +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 cfg_traits::IdentityCurrencyConversion; -use cfg_types::investments::Swap; -use frame_support::{dispatch::fmt::Debug, RuntimeDebugNoBound}; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; -use sp_runtime::traits::{EnsureAdd, EnsureSub, Zero}; - -/// Reflects the reason for the last token swap update such that it can be -/// updated accordingly if the last and current reason mismatch. -#[derive( - Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, -)] -pub enum TokenSwapReason { - Investment, - Redemption, - InvestmentAndRedemption, -} - -/// Restriction of `pallet_foreign_investments::Config` trait to support -/// currency conversion in the `InvestState`. -pub trait InvestStateConfig { - type Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug + Zero; - type CurrencyId: Clone + Copy + PartialEq + Debug; - type CurrencyConverter: IdentityCurrencyConversion< - Balance = Self::Balance, - Currency = Self::CurrencyId, - Error = sp_runtime::DispatchError, - >; -} - -/// Reflects all states a foreign investment can have until it is processed as -/// an investment via `::Investment`. This includes swapping it -/// into a pool currency or back, if the investment is decreased before it is -/// fully processed. -#[derive( - PartialOrd, Ord, PartialEq, Eq, RuntimeDebugNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, -)] -#[scale_info(skip_type_params(T))] -pub enum InvestState { - /// Default state for initialization which will never be actively put into - /// chain state, i.e. if this state is the result of applying transition(s), - /// then the corresponding `InvestmentState` will be cleared. - NoState, - /// The investment is waiting to be processed. - InvestmentOngoing { invest_amount: T::Balance }, - /// The investment is currently swapped into the required pool currency. - ActiveSwapIntoPoolCurrency { - swap: Swap, - }, - /// The unprocessed investment was fully decreased and is currently swapped - /// back into the corresponding foreign currency. - ActiveSwapIntoForeignCurrency { - swap: Swap, - }, - /// The investment is not fully swapped into pool currency and thus split - /// into two parts: - /// * One part is still being swapped. - /// * The remainder is already waiting to be processed as investment. - ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap, - invest_amount: T::Balance, - }, - /// The investment is split into two parts: - /// * One part is waiting to be processed as investment. - /// * The remainder is swapped back into the foreign currency as a result of - /// decreasing the invested amount before being processed. - ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: Swap, - invest_amount: T::Balance, - }, - /// The investment is split into two parts: - /// * The one part is swapping into pool currency. - /// * The remainder was swapped back into the foreign currency as a result - /// of decreasing the invested amount before being processed. - /// - /// NOTE: This state is transitioned into `ActiveSwapIntoPoolCurrency` - /// in the post-processing `apply_invest_state_transition` as the done part - /// invokes `ExecutedDecreaseInvestOrder` dispatch. - ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { - swap: Swap, - done_amount: T::Balance, - }, - /// The investment is swapped back into the foreign currency and was already - /// partially fulfilled. - ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap, - done_amount: T::Balance, - }, - /// The investment is split into three parts: - /// * One part is currently swapping into the pool currency. - /// * The second is already waiting to be processed as investment. - /// * The remainder was swapped back into the foreign currency as a result - /// of decreasing the invested amount before being processed. - /// - /// NOTE: This state is transitioned into - /// `ActiveSwapIntoPoolCurrencyAndInvestmentOngoing` in the post-processing - /// `apply_invest_state_transition` as the done part invokes - /// `ExecutedDecreaseInvestOrder` dispatch. - ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap, - done_amount: T::Balance, - invest_amount: T::Balance, - }, - /// The investment is split into three parts: - /// * One is waiting to be processed as investment. - /// * The second is swapped back into the foreign currency as a result of - /// decreasing the invested amount before being processed. - /// * The remainder was already swapped back into the foreign currency. - /// - /// NOTE: This state must not be transitioned by applying the trigger for - /// the done part but wait until the active swap is fulfilled. - ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: Swap, - done_amount: T::Balance, - invest_amount: T::Balance, - }, - /// The unprocessed investment was swapped back into foreign currency. - /// - /// NOTE: This state is killed in the post-processing - /// `apply_invest_state_transition` as it invokes - /// `ExecutedDecreaseInvestOrder` dispatch. - SwapIntoForeignDone { - done_swap: Swap, - }, - /// The investment is split into two parts: - /// * One part is waiting to be processed as an investment - /// * The swapped back into the foreign currency as a result of decreasing - /// the invested amount before being processed. - /// - /// NOTE: This state is transitioned into `InvestmentOngoing` in the - /// post-processing `apply_invest_state_transition` as the done part invokes - /// `ExecutedDecreaseInvestOrder` dispatch. - SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: Swap, - invest_amount: T::Balance, - }, -} -// NOTE: Needed because `T` of `InvestState` cannot be restricted to impl -// Default -impl Default for InvestState { - fn default() -> Self { - Self::NoState - } -} - -// NOTE: Needed because `T` of `InvestState` cannot be restricted to impl -// Copy -impl Clone for InvestState -where - T::Balance: Clone, - T::CurrencyId: Clone, - Swap: Clone, -{ - fn clone(&self) -> Self { - match self { - Self::NoState => Self::NoState, - Self::InvestmentOngoing { invest_amount } => Self::InvestmentOngoing { - invest_amount: *invest_amount, - }, - Self::ActiveSwapIntoPoolCurrency { swap } => { - Self::ActiveSwapIntoPoolCurrency { swap: *swap } - } - Self::ActiveSwapIntoForeignCurrency { swap } => { - Self::ActiveSwapIntoForeignCurrency { swap: *swap } - } - Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap, - invest_amount, - } => Self::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: *swap, - invest_amount: *invest_amount, - }, - Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap, - invest_amount, - } => Self::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: *swap, - invest_amount: *invest_amount, - }, - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { swap, done_amount } => { - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { - swap: *swap, - done_amount: *done_amount, - } - } - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { swap, done_amount } => { - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: *swap, - done_amount: *done_amount, - } - } - Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - done_amount, - invest_amount, - } => Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: *swap, - done_amount: *done_amount, - invest_amount: *invest_amount, - }, - Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap, - done_amount, - invest_amount, - } => Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { - swap: *swap, - done_amount: *done_amount, - invest_amount: *invest_amount, - }, - Self::SwapIntoForeignDone { done_swap } => Self::SwapIntoForeignDone { - done_swap: *done_swap, - }, - Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap, - invest_amount, - } => Self::SwapIntoForeignDoneAndInvestmentOngoing { - done_swap: *done_swap, - invest_amount: *invest_amount, - }, - } - } -} - -/// Reflects all state transitions of an `InvestmentState` which can be -/// externally triggered, i.e. by (partially) fulfilling a token swap order or -/// updating an unprocessed investment. -#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum InvestTransition< - Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, - Currency: Clone + Copy + PartialEq + Debug, -> { - /// Assumes `swap.amount` to be denominated in pool currency and - /// `swap.currency_in` to be pool currency as we increase here. - IncreaseInvestOrder(Swap), - /// Assumes `swap.amount` to be denominated in foreign currency and - /// `swap.currency_in` to be foreign currency as we increase here. - DecreaseInvestOrder(Swap), - /// Implicitly derives `swap.currency_in` and `swap.currency_out` from - /// previous state: - /// * If the previous state includes `ActiveSwapIntoPoolCurrency`, - /// `currency_in` is the pool currency. - /// * If the previous state includes `ActiveSwapIntoForeignCurrency`, - /// `currency_in` is the foreign currency. - FulfillSwapOrder(Swap), - CollectInvestment(Balance), -} - -/// Reflects all states a foreign redemption can have until transferred to the -/// corresponding source domain. -/// -/// This includes swapping it into a pool currency or back, if the investment is -/// decreased before it is fully processed. -#[derive( - Clone, - Copy, - Default, - PartialOrd, - Ord, - PartialEq, - Eq, - Debug, - Encode, - Decode, - TypeInfo, - MaxEncodedLen, -)] -pub enum RedeemState< - Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, - Currency: Clone + Copy + PartialEq + Debug, -> { - #[default] - /// Default state for initialization which will never be actively put into - /// chain state, i.e. if this state is the result of applying transition(s), - /// then the corresponding `RedemptionState` will be cleared. - NoState, - /// The redemption is pending until it is processed during epoch execution. - Redeeming { redeem_amount: Balance }, - /// The redemption was fully processed and collected and is currently - /// swapping into the foreign currency. - ActiveSwapIntoForeignCurrency { swap: Swap }, - /// The redemption was fully processed, collected and partially swapped into - /// the foreign currency. It is split into two parts: - /// * One part is swapping back into the foreign currency. - /// * The remainder was already swapped back. - ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap, - done_amount: Balance, - }, - /// The redemption was fully processed, collected and swapped into the - /// foreign currency. - /// - /// NOTE: This state is automatically killed in the post-processing - /// `apply_collect_redeem_transition` as it prepares the dispatch of - /// `ExecutedCollectRedeem` message which needs to be triggered manually. - SwapIntoForeignDone { done_swap: Swap }, - /// The redemption is split into two parts: - /// * One part is waiting to be processed as redemption. - /// * The remainder is swapping back into the foreign currency as a result - /// of processing and collecting beforehand. - RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: Balance, - swap: Swap, - }, - /// The redemption is split into two parts: - /// * One part is waiting to be processed as redemption. - /// * The remainder is swapping back into the foreign currency as a result - /// of processing and collecting beforehand. - /// - /// NOTE: This state is automatically transitioned into `Redeeming` in the - /// post-processing `apply_collect_redeem_transition` as the done part - /// prepares the dispatch of `ExecutedCollectRedeem` message which needs to - /// be triggered manually. - RedeemingAndSwapIntoForeignDone { - redeem_amount: Balance, - done_swap: Swap, - }, - /// The redemption is split into three parts: - /// * One part is waiting to be processed as redemption. - /// * The second is swapping back into the foreign currency as a result of - /// processing and collecting beforehand. - /// * The remainder was already swapped back. - RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - redeem_amount: Balance, - swap: Swap, - done_amount: Balance, - }, -} - -/// Reflects all state transitions of a `RedeemState` which can be -/// externally triggered, i.e. by (partially) fulfilling a token swap order or -/// updating an unprocessed redemption. -#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum RedeemTransition< - Balance: Clone + Copy + EnsureAdd + EnsureSub + Ord + Debug, - Currency: Clone + Copy + PartialEq + Debug, -> { - IncreaseRedeemOrder(Balance), - DecreaseRedeemOrder(Balance), - FulfillSwapOrder(Swap), - CollectRedemption(Balance, Swap), -} diff --git a/pallets/investments/src/lib.rs b/pallets/investments/src/lib.rs index 5f606f8a1b..3241018e4c 100644 --- a/pallets/investments/src/lib.rs +++ b/pallets/investments/src/lib.rs @@ -20,10 +20,7 @@ use cfg_traits::{ }; use cfg_types::{ fixed_point::FixedPointNumberExtension, - investments::{ - CollectedAmount, ForeignInvestmentInfo, InvestCollection, InvestmentAccount, - RedeemCollection, - }, + investments::{CollectedAmount, InvestCollection, InvestmentAccount, RedeemCollection}, orders::{FulfillmentWithPrice, Order, TotalOrder}, }; use frame_support::{ @@ -104,7 +101,7 @@ pub enum CollectType { #[frame_support::pallet] pub mod pallet { - use cfg_types::investments::{ForeignInvestmentInfo, InvestmentInfo}; + use cfg_types::investments::InvestmentInfo; use sp_runtime::{traits::AtLeast32BitUnsigned, FixedPointNumber, FixedPointOperand}; use super::*; @@ -174,7 +171,7 @@ pub mod pallet { /// NOTE: NOOP if the investment is not foreign. type CollectedInvestmentHook: StatusNotificationHook< Error = DispatchError, - Id = ForeignInvestmentInfo, + Id = (Self::AccountId, Self::InvestmentId), Status = CollectedAmount, >; @@ -183,7 +180,7 @@ pub mod pallet { /// NOTE: NOOP if the redemption is not foreign. type CollectedRedemptionHook: StatusNotificationHook< Error = DispatchError, - Id = ForeignInvestmentInfo, + Id = (Self::AccountId, Self::InvestmentId), Status = CollectedAmount, >; @@ -734,11 +731,7 @@ impl Pallet { if collected_investment != Default::default() { // Assumption: NOOP if investment is not foreign T::CollectedInvestmentHook::notify_status_change( - ForeignInvestmentInfo { - owner: who, - id: investment_id, - last_swap_reason: None, - }, + (who, investment_id), collected_investment, )?; } @@ -869,11 +862,7 @@ impl Pallet { if collected_redemption != Default::default() { // Assumption: NOOP if investment is not foreign T::CollectedRedemptionHook::notify_status_change( - ForeignInvestmentInfo { - owner: who, - id: investment_id, - last_swap_reason: None, - }, + (who, investment_id), collected_redemption, )?; } diff --git a/pallets/investments/src/mock.rs b/pallets/investments/src/mock.rs index 8f19d8209e..9bd865928e 100644 --- a/pallets/investments/src/mock.rs +++ b/pallets/investments/src/mock.rs @@ -160,7 +160,7 @@ impl cfg_mocks::pallet_mock_pools::Config for MockRuntime { pub struct NoopCollectHook; impl cfg_traits::StatusNotificationHook for NoopCollectHook { type Error = sp_runtime::DispatchError; - type Id = cfg_types::investments::ForeignInvestmentInfo; + type Id = (MockAccountId, InvestmentId); type Status = cfg_types::investments::CollectedAmount; fn notify_status_change(_id: Self::Id, _status: Self::Status) -> DispatchResult { diff --git a/pallets/liquidity-pools/src/benchmarking.rs b/pallets/liquidity-pools/src/benchmarking.rs deleted file mode 100644 index aaec856a1b..0000000000 --- a/pallets/liquidity-pools/src/benchmarking.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// This file is part of 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 cfg_traits::{ - benchmarking::{BenchForeignInvestmentSetupInfo, ForeignInvestmentBenchmarkHelper}, - investments::{ForeignInvestment, TrancheCurrency}, -}; -use frame_benchmarking::v2::*; - -use super::*; -use crate::Pallet; - -#[benchmarks( - where - T::ForeignInvestment: ForeignInvestmentBenchmarkHelper, - T::Balance: From, - T::AccountId: From<[u8; 32]> + Into<[u8; 32]>, -)] -mod benchmarks { - use super::*; - - #[benchmark] - fn inbound_collect_redeem() -> Result<(), BenchmarkError> { - let BenchForeignInvestmentSetupInfo { investor, investment_id, pool_currency, foreign_currency, .. } = ::bench_prepare_foreign_investments_setup(); - - // Fund investor with foreign currency and tranche tokens - T::Tokens::mint_into( - investment_id.clone().into(), - &investor, - (u128::max_value() / 10).into(), - )?; - T::Tokens::mint_into(foreign_currency, &investor, (u128::max_value() / 10).into())?; - - // Increase investment and redemption - ::bench_prep_foreign_investments_worst_case(investor.clone(), investment_id.clone(), pool_currency, foreign_currency); - - let investor_pointer = investor.clone(); - let redeeming_amount = - T::ForeignInvestment::redemption(&investor_pointer, investment_id.clone())?; - let pool_id = investment_id.of_pool(); - let tranche_id = investment_id.of_tranche(); - let foreign_currency_index = Pallet::::try_get_general_index(foreign_currency)?.into(); - - #[block] - { - Pallet::::handle_collect_redemption( - pool_id, - tranche_id, - investor, - foreign_currency_index, - )?; - } - - assert!( - T::ForeignInvestment::redemption(&investor_pointer, investment_id)? < redeeming_amount - ); - - Ok(()) - } -} diff --git a/pallets/liquidity-pools/src/hooks.rs b/pallets/liquidity-pools/src/hooks.rs index 1c877105fc..eb1c529b6e 100644 --- a/pallets/liquidity-pools/src/hooks.rs +++ b/pallets/liquidity-pools/src/hooks.rs @@ -16,7 +16,7 @@ use cfg_traits::{ }; use cfg_types::{ domain_address::{Domain, DomainAddress}, - investments::{ExecutedForeignCollect, ExecutedForeignDecreaseInvest, ForeignInvestmentInfo}, + investments::{ExecutedForeignCollect, ExecutedForeignDecreaseInvest}, }; use frame_support::{ traits::{ @@ -39,19 +39,14 @@ where ::AccountId: Into<[u8; 32]>, { type Error = DispatchError; - type Id = ForeignInvestmentInfo; + type Id = (T::AccountId, T::TrancheCurrency); type Status = ExecutedForeignDecreaseInvest; #[transactional] fn notify_status_change( - id: ForeignInvestmentInfo, + (investor, investment_id): (T::AccountId, T::TrancheCurrency), status: ExecutedForeignDecreaseInvest, ) -> DispatchResult { - let ForeignInvestmentInfo { - id: investment_id, - owner: investor, - .. - } = id; let currency = Pallet::::try_get_general_index(status.foreign_currency)?; let wrapped_token = Pallet::::try_get_wrapped_token(&status.foreign_currency)?; let domain_address: DomainAddress = wrapped_token.into(); @@ -87,19 +82,14 @@ where ::AccountId: Into<[u8; 32]>, { type Error = DispatchError; - type Id = ForeignInvestmentInfo; + type Id = (T::AccountId, T::TrancheCurrency); type Status = ExecutedForeignCollect; #[transactional] fn notify_status_change( - id: ForeignInvestmentInfo, + (investor, investment_id): (T::AccountId, T::TrancheCurrency), status: ExecutedForeignCollect, ) -> DispatchResult { - let ForeignInvestmentInfo { - id: investment_id, - owner: investor, - .. - } = id; let currency = Pallet::::try_get_general_index(status.currency)?; let wrapped_token = Pallet::::try_get_wrapped_token(&status.currency)?; let domain_address: DomainAddress = wrapped_token.into(); @@ -136,19 +126,14 @@ where ::AccountId: Into<[u8; 32]>, { type Error = DispatchError; - type Id = ForeignInvestmentInfo; + type Id = (T::AccountId, T::TrancheCurrency); type Status = ExecutedForeignCollect; #[transactional] fn notify_status_change( - id: ForeignInvestmentInfo, + (investor, investment_id): (T::AccountId, T::TrancheCurrency), status: ExecutedForeignCollect, ) -> DispatchResult { - let ForeignInvestmentInfo { - id: investment_id, - owner: investor, - .. - } = id; let currency = Pallet::::try_get_general_index(status.currency)?; let wrapped_token = Pallet::::try_get_wrapped_token(&status.currency)?; let domain_address: DomainAddress = wrapped_token.into(); diff --git a/pallets/liquidity-pools/src/inbound.rs b/pallets/liquidity-pools/src/inbound.rs index bf7ebd7793..4a321cb050 100644 --- a/pallets/liquidity-pools/src/inbound.rs +++ b/pallets/liquidity-pools/src/inbound.rs @@ -12,8 +12,7 @@ // GNU General Public License for more details. use cfg_traits::{ - investments::ForeignInvestment, liquidity_pools::OutboundQueue, Permissions, PoolInspect, - TimeAsSecs, + investments::ForeignInvestment, liquidity_pools::OutboundQueue, Permissions, TimeAsSecs, }; use cfg_types::{ domain_address::{Domain, DomainAddress}, @@ -104,8 +103,6 @@ where ) -> DispatchResult { let invest_id: T::TrancheCurrency = Self::derive_invest_id(pool_id, tranche_id)?; let payment_currency = Self::try_get_payment_currency(invest_id.clone(), currency_index)?; - let pool_currency = - T::PoolInspect::currency_for(pool_id).ok_or(Error::::PoolNotFound)?; // Mint additional amount of payment currency T::Tokens::mint_into(payment_currency, &investor, amount)?; @@ -115,7 +112,6 @@ where invest_id, amount, payment_currency, - pool_currency, )?; Ok(()) @@ -139,18 +135,15 @@ where ) -> DispatchResult { let invest_id: T::TrancheCurrency = Self::derive_invest_id(pool_id, tranche_id)?; // NOTE: Even though we can assume this currency to have been used as payment, - // the trading pair needs to be registered for the opposite direction in case a - // swap from pool to foreign results from updating the `InvestState` + // the trading pair needs to be registered for the opposite direction as hin + // case a swap from pool to foreign results from updating the `InvestState` let payout_currency = Self::try_get_payout_currency(invest_id.clone(), currency_index)?; - let pool_currency = - T::PoolInspect::currency_for(pool_id).ok_or(Error::::PoolNotFound)?; T::ForeignInvestment::decrease_foreign_investment( &investor, invest_id, amount, payout_currency, - pool_currency, )?; Ok(()) @@ -228,7 +221,7 @@ where pool_id: T::PoolId, tranche_id: T::TrancheId, investor: T::AccountId, - amount: ::Balance, + tranche_tokens_payout: ::Balance, currency_index: GeneralCurrencyIndexOf, destination: DomainAddress, ) -> DispatchResult { @@ -236,16 +229,15 @@ where let currency_u128 = currency_index.index; let payout_currency = Self::try_get_payout_currency(invest_id.clone(), currency_index)?; - let (tranche_tokens_payout, remaining_redeem_amount) = - T::ForeignInvestment::decrease_foreign_redemption( - &investor, - invest_id.clone(), - amount, - payout_currency, - )?; + T::ForeignInvestment::decrease_foreign_redemption( + &investor, + invest_id.clone(), + tranche_tokens_payout, + payout_currency, + )?; T::Tokens::transfer( - invest_id.into(), + invest_id.clone().into(), &investor, &Domain::convert(destination.domain()), tranche_tokens_payout, @@ -255,10 +247,13 @@ where let message: MessageOf = Message::ExecutedDecreaseRedeemOrder { pool_id, tranche_id, - investor: investor.into(), + investor: investor.clone().into(), currency: currency_u128, tranche_tokens_payout, - remaining_redeem_amount, + remaining_redeem_amount: T::ForeignInvestment::redemption( + &investor, + invest_id.clone(), + )?, }; T::OutboundQueue::submit(T::TreasuryAccount::get(), destination.domain(), message)?; @@ -337,15 +332,8 @@ where ) -> DispatchResult { let invest_id: T::TrancheCurrency = Self::derive_invest_id(pool_id, tranche_id)?; let payout_currency = Self::try_get_payout_currency(invest_id.clone(), currency_index)?; - let pool_currency = - T::PoolInspect::currency_for(pool_id).ok_or(Error::::PoolNotFound)?; - T::ForeignInvestment::collect_foreign_redemption( - &investor, - invest_id, - payout_currency, - pool_currency, - )?; + T::ForeignInvestment::collect_foreign_redemption(&investor, invest_id, payout_currency)?; Ok(()) } diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index b0f76a0a19..da9e45f102 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -82,8 +82,6 @@ pub use routers::*; mod contract; pub use contract::*; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; pub mod hooks; mod inbound; diff --git a/pallets/liquidity-pools/src/weights.rs b/pallets/liquidity-pools/src/weights.rs deleted file mode 100644 index 8868e4b53f..0000000000 --- a/pallets/liquidity-pools/src/weights.rs +++ /dev/null @@ -1,109 +0,0 @@ - -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// This file is part of 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. - -//! Autogenerated weights for `pallet_liquidity_pools` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-09-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Apple Macbook Pro M1 Max`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("development-local"), DB CACHE: 1024 - -// Executed Command: -// target/release/centrifuge-chain -// benchmark -// pallet -// --chain=development-local -// --steps=50 -// --repeat=20 -// --pallet=pallet_liquidity_pools -// --extrinsic=* -// --execution=wasm -// --wasm-execution=compiled -// --heap-pages=4096 - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] - -use frame_support::{traits::Get, weights::Weight}; -use sp_std::marker::PhantomData; - -/// Weight functions for `pallet_liquidity_pools`. -pub struct WeightInfo(PhantomData); -impl pallet_liquidity_pools::WeightInfo for WeightInfo { - /// Storage: PoolSystem Pool (r:1 w:0) - /// Proof: PoolSystem Pool (max_values: None, max_size: Some(813), added: 3288, mode: MaxEncodedLen) - /// Storage: OrmlAssetRegistry Metadata (r:2 w:0) - /// Proof Skipped: OrmlAssetRegistry Metadata (max_values: None, max_size: None, mode: Measured) - /// Storage: OrderBook TradingPair (r:1 w:0) - /// Proof: OrderBook TradingPair (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) - /// Storage: ForeignInvestments RedemptionPayoutCurrency (r:1 w:0) - /// Proof: ForeignInvestments RedemptionPayoutCurrency (max_values: None, max_size: Some(113), added: 2588, mode: MaxEncodedLen) - /// Storage: Investments RedeemOrders (r:1 w:1) - /// Proof: Investments RedeemOrders (max_values: None, max_size: Some(112), added: 2587, mode: MaxEncodedLen) - /// Storage: Investments RedeemOrderId (r:1 w:0) - /// Proof: Investments RedeemOrderId (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) - /// Storage: Investments ClearedRedeemOrders (r:1 w:0) - /// Proof: Investments ClearedRedeemOrders (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) - /// Storage: OrmlTokens Accounts (r:3 w:3) - /// Proof: OrmlTokens Accounts (max_values: None, max_size: Some(129), added: 2604, mode: MaxEncodedLen) - /// Storage: System Account (r:1 w:0) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: ForeignInvestments RedemptionState (r:1 w:1) - /// Proof: ForeignInvestments RedemptionState (max_values: None, max_size: Some(187), added: 2662, mode: MaxEncodedLen) - /// Storage: ForeignInvestments CollectedRedemption (r:1 w:1) - /// Proof: ForeignInvestments CollectedRedemption (max_values: None, max_size: Some(120), added: 2595, mode: MaxEncodedLen) - /// Storage: ForeignInvestments TokenSwapOrderIds (r:1 w:1) - /// Proof: ForeignInvestments TokenSwapOrderIds (max_values: None, max_size: Some(96), added: 2571, mode: MaxEncodedLen) - /// Storage: ForeignInvestments InvestmentState (r:1 w:1) - /// Proof: ForeignInvestments InvestmentState (max_values: None, max_size: Some(187), added: 2662, mode: MaxEncodedLen) - /// Storage: OrderBook Orders (r:1 w:2) - /// Proof: OrderBook Orders (max_values: None, max_size: Some(186), added: 2661, mode: MaxEncodedLen) - /// Storage: OrderBook AssetPairOrders (r:2 w:2) - /// Proof: OrderBook AssetPairOrders (max_values: None, max_size: Some(8068), added: 10543, mode: MaxEncodedLen) - /// Storage: OrderBook OrderIdNonceStore (r:1 w:1) - /// Proof: OrderBook OrderIdNonceStore (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) - /// Storage: ForeignInvestments ForeignInvestmentInfo (r:0 w:2) - /// Proof: ForeignInvestments ForeignInvestmentInfo (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) - /// Storage: OrderBook UserOrders (r:0 w:2) - /// Proof: OrderBook UserOrders (max_values: None, max_size: Some(226), added: 2701, mode: MaxEncodedLen) - fn inbound_collect_redeem() -> Weight { - // Proof Size summary in bytes: - // Measured: `5737` - // Estimated: `71940` - // Minimum execution time: 231_000 nanoseconds. - Weight::from_parts(236_000_000, 71940) - .saturating_add(T::DbWeight::get().reads(20)) - .saturating_add(T::DbWeight::get().writes(17)) - } - - fn schedule_upgrade() -> Weight { - Weight::from_parts(236_000_000, 71940) - .saturating_add(T::DbWeight::get().reads(20)) - .saturating_add(T::DbWeight::get().writes(17)) - } - - fn cancel_upgrade() -> Weight { - Weight::from_parts(236_000_000, 71940) - .saturating_add(T::DbWeight::get().reads(20)) - .saturating_add(T::DbWeight::get().writes(17)) - } - - fn update_tranche_token_metadata() -> Weight { - Weight::from_parts(236_000_000, 71940) - .saturating_add(T::DbWeight::get().reads(20)) - .saturating_add(T::DbWeight::get().writes(17)) - } -} diff --git a/pallets/order-book/src/lib.rs b/pallets/order-book/src/lib.rs index c23924d05b..229bf0603e 100644 --- a/pallets/order-book/src/lib.rs +++ b/pallets/order-book/src/lib.rs @@ -651,7 +651,7 @@ pub mod pallet { T::FulfilledOrderHook::notify_status_change( order.order_id, Swap { - amount: buy_amount, + amount_in: buy_amount, currency_in: order.asset_in_id, currency_out: order.asset_out_id, }, @@ -1020,7 +1020,7 @@ pub mod pallet { fn get_order_details(order: Self::OrderId) -> Option> { Orders::::get(order) .map(|order| Swap { - amount: order.buy_amount, + amount_in: order.buy_amount, currency_in: order.asset_in_id, currency_out: order.asset_out_id, }) diff --git a/pallets/order-book/src/tests.rs b/pallets/order-book/src/tests.rs index 1a5e660693..8a71bcce65 100644 --- a/pallets/order-book/src/tests.rs +++ b/pallets/order-book/src/tests.rs @@ -1329,7 +1329,7 @@ fn get_order_details_works() { assert_eq!( OrderBook::get_order_details(order_id), Some(cfg_types::investments::Swap { - amount: 15 * CURRENCY_AUSD_DECIMALS, + amount_in: 15 * CURRENCY_AUSD_DECIMALS, currency_in: DEV_AUSD_CURRENCY_ID, currency_out: DEV_USDT_CURRENCY_ID }) diff --git a/pallets/pool-registry/src/mock.rs b/pallets/pool-registry/src/mock.rs index 1e3f6f850a..b37e8eccae 100644 --- a/pallets/pool-registry/src/mock.rs +++ b/pallets/pool-registry/src/mock.rs @@ -345,7 +345,7 @@ impl orml_tokens::Config for Test { pub struct NoopCollectHook; impl cfg_traits::StatusNotificationHook for NoopCollectHook { type Error = DispatchError; - type Id = cfg_types::investments::ForeignInvestmentInfo; + type Id = (AccountId, TrancheCurrency); type Status = cfg_types::investments::CollectedAmount; fn notify_status_change(_id: Self::Id, _status: Self::Status) -> DispatchResult { diff --git a/pallets/pool-system/src/mock.rs b/pallets/pool-system/src/mock.rs index 021118d2db..4c590315a8 100644 --- a/pallets/pool-system/src/mock.rs +++ b/pallets/pool-system/src/mock.rs @@ -336,7 +336,7 @@ where pub struct NoopCollectHook; impl cfg_traits::StatusNotificationHook for NoopCollectHook { type Error = sp_runtime::DispatchError; - type Id = cfg_types::investments::ForeignInvestmentInfo; + type Id = (MockAccountId, TrancheCurrency); type Status = cfg_types::investments::CollectedAmount; fn notify_status_change(_id: Self::Id, _status: Self::Status) -> DispatchResult { diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 80fdd5c1bc..01e56e5b7c 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -1641,10 +1641,8 @@ impl pallet_investments::Config for Runtime { type Accountant = PoolSystem; type Amount = Balance; type BalanceRatio = Quantity; - type CollectedInvestmentHook = - pallet_foreign_investments::hooks::CollectedInvestmentHook; - type CollectedRedemptionHook = - pallet_foreign_investments::hooks::CollectedRedemptionHook; + type CollectedInvestmentHook = pallet_foreign_investments::CollectedInvestmentHook; + type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = TrancheCurrency; type MaxOutstandingCollects = MaxOutstandingCollects; type PreConditions = IsTrancheInvestor; @@ -1731,7 +1729,7 @@ impl pallet_order_book::Config for Runtime { type Balance = Balance; type DecimalConverter = runtime_common::foreign_investments::NativeBalanceDecimalConverter; - type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook; + type FulfilledOrderHook = pallet_foreign_investments::FulfilledSwapOrderHook; type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; @@ -1815,7 +1813,7 @@ construct_runtime!( LiquidityRewards: pallet_liquidity_rewards::{Pallet, Call, Storage, Event} = 111, GapRewardMechanism: pallet_rewards::mechanism::gap = 112, OrderBook: pallet_order_book::{Pallet, Call, Storage, Event} = 113, - ForeignInvestments: pallet_foreign_investments::{Pallet, Storage, Event} = 114, + ForeignInvestments: pallet_foreign_investments::{Pallet, Storage} = 114, TransferAllowList: pallet_transfer_allowlist::{Pallet, Call, Storage, Event} = 115, OraclePriceFeed: pallet_oracle_feed::{Pallet, Call, Storage, Event} = 116, OraclePriceCollection: pallet_oracle_collection::{Pallet, Call, Storage, Event} = 117, diff --git a/runtime/altair/src/liquidity_pools.rs b/runtime/altair/src/liquidity_pools.rs index eab19f9303..a31e050620 100644 --- a/runtime/altair/src/liquidity_pools.rs +++ b/runtime/altair/src/liquidity_pools.rs @@ -27,7 +27,6 @@ use runtime_common::{ gateway::GatewayAccountProvider, liquidity_pools::LiquidityPoolsMessage, transfer_filter::PreLpTransfer, }; -use sp_runtime::traits::One; use crate::{ ForeignInvestments, Investments, LiquidityPools, LiquidityPoolsGateway, LocationToAccountId, @@ -35,10 +34,6 @@ use crate::{ Timestamp, Tokens, TransferAllowList, TreasuryAccount, }; -parameter_types! { - pub DefaultTokenSellRatio: Ratio = Ratio::one(); -} - impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; type BalanceRatio = Ratio; @@ -47,16 +42,11 @@ impl pallet_foreign_investments::Config for Runtime { type CurrencyConverter = IdentityPoolCurrencyConverter; type CurrencyId = CurrencyId; type DecreasedForeignInvestOrderHook = DecreasedForeignInvestOrderHook; - type DefaultTokenSellRatio = DefaultTokenSellRatio; type Investment = Investments; type InvestmentId = TrancheCurrency; - type PoolId = PoolId; type PoolInspect = PoolSystem; - type RuntimeEvent = RuntimeEvent; - type TokenSwapOrderId = u64; + type SwapId = u64; type TokenSwaps = OrderBook; - type TrancheId = TrancheId; - type WeightInfo = (); } parameter_types! { diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index f27ae72b20..30b0018984 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -1690,10 +1690,8 @@ impl pallet_investments::Config for Runtime { type Accountant = PoolSystem; type Amount = Balance; type BalanceRatio = Quantity; - type CollectedInvestmentHook = - pallet_foreign_investments::hooks::CollectedInvestmentHook; - type CollectedRedemptionHook = - pallet_foreign_investments::hooks::CollectedRedemptionHook; + type CollectedInvestmentHook = pallet_foreign_investments::CollectedInvestmentHook; + type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = TrancheCurrency; type MaxOutstandingCollects = MaxOutstandingCollects; type PreConditions = IsTrancheInvestor; @@ -1842,7 +1840,7 @@ impl pallet_order_book::Config for Runtime { type Balance = Balance; type DecimalConverter = runtime_common::foreign_investments::NativeBalanceDecimalConverter; - type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook; + type FulfilledOrderHook = pallet_foreign_investments::FulfilledSwapOrderHook; type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; @@ -1934,7 +1932,7 @@ construct_runtime!( GapRewardMechanism: pallet_rewards::mechanism::gap = 106, LiquidityPoolsGateway: pallet_liquidity_pools_gateway::{Pallet, Call, Storage, Event, Origin } = 107, OrderBook: pallet_order_book::{Pallet, Call, Storage, Event} = 108, - ForeignInvestments: pallet_foreign_investments::{Pallet, Storage, Event} = 109, + ForeignInvestments: pallet_foreign_investments::{Pallet, Storage} = 109, TransferAllowList: pallet_transfer_allowlist::{Pallet, Call, Storage, Event} = 110, OraclePriceFeed: pallet_oracle_feed::{Pallet, Call, Storage, Event} = 111, OraclePriceCollection: pallet_oracle_collection::{Pallet, Call, Storage, Event} = 112, diff --git a/runtime/centrifuge/src/liquidity_pools.rs b/runtime/centrifuge/src/liquidity_pools.rs index 3085b6d434..98832fb21f 100644 --- a/runtime/centrifuge/src/liquidity_pools.rs +++ b/runtime/centrifuge/src/liquidity_pools.rs @@ -27,7 +27,6 @@ use runtime_common::{ gateway::GatewayAccountProvider, liquidity_pools::LiquidityPoolsMessage, origin::EnsureAccountOrRootOr, transfer_filter::PreLpTransfer, }; -use sp_runtime::traits::One; use crate::{ ForeignInvestments, Investments, LiquidityPools, LiquidityPoolsAxelarGateway, @@ -36,10 +35,6 @@ use crate::{ TreasuryAccount, }; -parameter_types! { - pub DefaultTokenSellRatio: Ratio = Ratio::one(); -} - impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; type BalanceRatio = Ratio; @@ -48,16 +43,11 @@ impl pallet_foreign_investments::Config for Runtime { type CurrencyConverter = IdentityPoolCurrencyConverter; type CurrencyId = CurrencyId; type DecreasedForeignInvestOrderHook = DecreasedForeignInvestOrderHook; - type DefaultTokenSellRatio = DefaultTokenSellRatio; type Investment = Investments; type InvestmentId = TrancheCurrency; - type PoolId = PoolId; type PoolInspect = PoolSystem; - type RuntimeEvent = RuntimeEvent; - type TokenSwapOrderId = u64; + type SwapId = u64; type TokenSwaps = OrderBook; - type TrancheId = TrancheId; - type WeightInfo = (); } parameter_types! { diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 17771518ab..879dd8a580 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -1655,10 +1655,8 @@ impl pallet_investments::Config for Runtime { type Accountant = PoolSystem; type Amount = Balance; type BalanceRatio = Quantity; - type CollectedInvestmentHook = - pallet_foreign_investments::hooks::CollectedInvestmentHook; - type CollectedRedemptionHook = - pallet_foreign_investments::hooks::CollectedRedemptionHook; + type CollectedInvestmentHook = pallet_foreign_investments::CollectedInvestmentHook; + type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = TrancheCurrency; type MaxOutstandingCollects = MaxOutstandingCollects; type PreConditions = IsTrancheInvestor; @@ -1835,7 +1833,7 @@ impl pallet_order_book::Config for Runtime { type Balance = Balance; type DecimalConverter = runtime_common::foreign_investments::NativeBalanceDecimalConverter; - type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook; + type FulfilledOrderHook = pallet_foreign_investments::FulfilledSwapOrderHook; type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; @@ -1926,7 +1924,7 @@ construct_runtime!( GapRewardMechanism: pallet_rewards::mechanism::gap = 114, LiquidityPoolsGateway: pallet_liquidity_pools_gateway::{Pallet, Call, Storage, Event, Origin } = 115, OrderBook: pallet_order_book::{Pallet, Call, Storage, Event} = 116, - ForeignInvestments: pallet_foreign_investments::{Pallet, Storage, Event} = 117, + ForeignInvestments: pallet_foreign_investments::{Pallet, Storage} = 117, OraclePriceFeed: pallet_oracle_feed::{Pallet, Call, Storage, Event} = 118, OraclePriceCollection: pallet_oracle_collection::{Pallet, Call, Storage, Event} = 119, @@ -2647,7 +2645,6 @@ impl_runtime_apis! { add_benchmark!(params, batches, cumulus_pallet_xcmp_queue, XcmpQueue); add_benchmark!(params, batches, pallet_transfer_allowlist, TransferAllowList); add_benchmark!(params, batches, pallet_order_book, OrderBook); - add_benchmark!(params, batches, pallet_liquidity_pools, LiquidityPools); add_benchmark!(params, batches, pallet_nft_sales, NftSales); add_benchmark!(params, batches, pallet_investments, Investments); add_benchmark!(params, batches, pallet_xcm, PolkadotXcm); @@ -2708,7 +2705,6 @@ impl_runtime_apis! { list_benchmark!(list, extra, cumulus_pallet_xcmp_queue, XcmpQueue); list_benchmark!(list, extra, pallet_transfer_allowlist, TransferAllowList); list_benchmark!(list, extra, pallet_order_book, OrderBook); - list_benchmark!(list, extra, pallet_liquidity_pools, LiquidityPools); list_benchmark!(list, extra, pallet_nft_sales, NftSales); list_benchmark!(list, extra, pallet_investments, Investments); list_benchmark!(list, extra, pallet_xcm, PolkadotXcm); diff --git a/runtime/development/src/liquidity_pools.rs b/runtime/development/src/liquidity_pools.rs index 8f433b7814..d073f5119f 100644 --- a/runtime/development/src/liquidity_pools.rs +++ b/runtime/development/src/liquidity_pools.rs @@ -28,7 +28,6 @@ use runtime_common::{ gateway::GatewayAccountProvider, liquidity_pools::LiquidityPoolsMessage, transfer_filter::PreLpTransfer, }; -use sp_runtime::traits::One; use crate::{ ForeignInvestments, Investments, LiquidityPools, LiquidityPoolsAxelarGateway, @@ -37,10 +36,6 @@ use crate::{ TreasuryAccount, }; -parameter_types! { - pub DefaultTokenSellRatio: Ratio = Ratio::one(); -} - impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; type BalanceRatio = Ratio; @@ -49,16 +44,11 @@ impl pallet_foreign_investments::Config for Runtime { type CurrencyConverter = IdentityPoolCurrencyConverter; type CurrencyId = CurrencyId; type DecreasedForeignInvestOrderHook = DecreasedForeignInvestOrderHook; - type DefaultTokenSellRatio = DefaultTokenSellRatio; type Investment = Investments; type InvestmentId = TrancheCurrency; - type PoolId = PoolId; type PoolInspect = PoolSystem; - type RuntimeEvent = RuntimeEvent; - type TokenSwapOrderId = u64; + type SwapId = u64; type TokenSwaps = OrderBook; - type TrancheId = TrancheId; - type WeightInfo = (); } parameter_types! { diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index 5f51b242da..48f912a97f 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -10,9 +10,7 @@ use cfg_traits::{ use cfg_types::{ domain_address::{Domain, DomainAddress}, fixed_point::{Quantity, Ratio}, - investments::{ - ForeignInvestmentInfo, InvestCollection, InvestmentAccount, RedeemCollection, Swap, - }, + investments::{InvestCollection, InvestmentAccount, RedeemCollection}, locations::Location, orders::FulfillmentWithPrice, permissions::{PermissionScope, PoolRole, Role}, @@ -36,12 +34,6 @@ use liquidity_pools_gateway_routers::{ FeeValues, XCMRouter, XcmDomain, DEFAULT_PROOF_SIZE, MAX_AXELAR_EVM_CHAIN_SIZE, }; use orml_traits::{asset_registry::AssetMetadata, MultiCurrency}; -use pallet_foreign_investments::{ - errors::{InvestError, RedeemError}, - types::{InvestState, RedeemState, TokenSwapReason}, - CollectedInvestment, CollectedRedemption, InvestmentPaymentCurrency, InvestmentState, - RedemptionPayoutCurrency, RedemptionState, -}; use pallet_investments::CollectOutcome; use pallet_liquidity_pools::Message; use pallet_liquidity_pools_gateway::{Call as LiquidityPoolsGatewayCall, GatewayOrigin}; @@ -545,6 +537,36 @@ mod development { .into_account_truncating() } + /// Invests the provided amount via `pallet_investments` to not block + /// foreign investments for a different foreign currency. + pub fn do_initial_invest_short_cut( + pool_id: u64, + amount: Balance, + investor: AccountId, + ) { + let pool_currency: CurrencyId = pallet_pool_system::Pallet::::currency_for(pool_id) + .expect("Pool existence checked already"); + + // Make investor the MembersListAdmin of this Pool + crate::generic::utils::give_pool_role::( + investor.clone(), + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); + + assert_ok!(orml_tokens::Pallet::::mint_into( + pool_currency, + &investor, + amount + )); + + assert_ok!(pallet_investments::Pallet::::update_invest_order( + ::RuntimeOrigin::signed(investor.clone()), + default_investment_id::(), + amount + )); + } + /// Sets up required permissions for the investor and executes an /// initial investment via LiquidityPools by executing /// `IncreaseInvestOrder`. @@ -557,9 +579,7 @@ mod development { amount: Balance, investor: AccountId, currency_id: CurrencyId, - clear_investment_payment_currency: bool, ) { - let valid_until = DEFAULT_VALIDITY; let pool_currency: CurrencyId = pallet_pool_system::Pallet::::currency_for(pool_id) .expect("Pool existence checked already"); @@ -586,16 +606,20 @@ mod development { } // Make investor the MembersListAdmin of this Pool - assert_ok!(pallet_permissions::Pallet::::add( - ::RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - investor.clone(), + if !pallet_permissions::Pallet::::has( PermissionScope::Pool(pool_id), + investor.clone(), Role::PoolRole(PoolRole::TrancheInvestor( default_tranche_id::(pool_id), - valid_until + DEFAULT_VALIDITY, )), - )); + ) { + crate::generic::utils::give_pool_role::( + investor.clone(), + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); + } let amount_before = orml_tokens::Pallet::::balance(currency_id, &default_investment_account::()); @@ -608,19 +632,8 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg )); - assert_eq!( - InvestmentPaymentCurrency::::get(&investor, default_investment_id::()) - .unwrap(), - currency_id, - ); if currency_id == pool_currency { - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: amount - } - ); // Verify investment was transferred into investment account assert_eq!( orml_tokens::Pallet::::balance( @@ -629,17 +642,6 @@ mod development { ), final_amount ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: InvestState::InvestmentOngoing { - invest_amount: final_amount, - }, - } - .into() - })); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_investments::Event::::InvestOrderUpdated { @@ -650,31 +652,6 @@ mod development { } .into() })); - } else { - let amount_pool_denominated: u128 = IdentityPoolCurrencyConverter::< - orml_asset_registry::Pallet, - >::stable_to_stable( - pool_currency, currency_id, amount - ) - .unwrap(); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoPoolCurrency { - swap: Swap { - currency_in: pool_currency, - currency_out: currency_id, - amount: amount_pool_denominated - } - } - ); - } - - // NOTE: In some tests, we run this setup with a pool currency to immediately - // set the investment state to `InvestmentOngoing`. However, afterwards we want - // to invest with another currency and treat that investment as the initial one. - // In order to do that, we need to clear the payment currency. - if clear_investment_payment_currency { - InvestmentPaymentCurrency::::remove(&investor, default_investment_id::()); } } @@ -693,8 +670,6 @@ mod development { investor: AccountId, currency_id: CurrencyId, ) { - let valid_until = DEFAULT_VALIDITY; - // Fund `DomainLocator` account of origination domain as redeemed tranche tokens // are transferred from this account instead of minting assert_ok!(orml_tokens::Pallet::::mint_into( @@ -735,33 +710,17 @@ mod development { ); // Make investor the MembersListAdmin of this Pool - assert_ok!(pallet_permissions::Pallet::::add( - ::RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), + crate::generic::utils::give_pool_role::( investor.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id::(pool_id), - valid_until - )), - )); + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); assert_ok!(pallet_liquidity_pools::Pallet::::submit( DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg )); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::Redeeming { - redeem_amount: amount - } - ); - assert_eq!( - RedemptionPayoutCurrency::::get(&investor, default_investment_id::()) - .unwrap(), - currency_id - ); // Verify redemption was transferred into investment account assert_eq!( orml_tokens::Pallet::::balance( @@ -783,21 +742,6 @@ mod development { ), 0 ); - assert_eq!( - frame_system::Pallet::::events() - .iter() - .nth_back(4) - .unwrap() - .event, - pallet_foreign_investments::Event::::ForeignRedemptionUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: RedeemState::Redeeming { - redeem_amount: amount - } - } - .into() - ); assert_eq!( frame_system::Pallet::::events() .iter() @@ -1103,7 +1047,6 @@ mod development { // Finally, verify we can call pallet_liquidity_pools::Pallet::::add_tranche // successfully when given a valid pool + tranche id pair. let new_member = DomainAddress::EVM(crate::utils::MOONBEAM_EVM_CHAIN_ID, [3; 20]); - let valid_until = DEFAULT_VALIDITY; // Make ALICE the MembersListAdmin of this Pool assert_ok!(pallet_permissions::Pallet::::add( @@ -1121,28 +1064,23 @@ mod development { pool_id, tranche_id, new_member.clone(), - valid_until, + DEFAULT_VALIDITY, ), pallet_liquidity_pools::Error::::InvestorDomainAddressNotAMember, ); // Whitelist destination as TrancheInvestor of this Pool - assert_ok!(pallet_permissions::Pallet::::add( - RawOrigin::Signed(Keyring::Alice.into()).into(), - Role::PoolRole(PoolRole::InvestorAdmin), + crate::generic::utils::give_pool_role::( AccountConverter::::convert(new_member.clone()), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id::(pool_id), - valid_until - )), - )); + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); // Verify the Investor role was set as expected in Permissions assert!(pallet_permissions::Pallet::::has( PermissionScope::Pool(pool_id), AccountConverter::::convert(new_member.clone()), - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, valid_until)), + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, DEFAULT_VALIDITY)), )); // Verify it now works @@ -1151,7 +1089,7 @@ mod development { pool_id, tranche_id, new_member, - valid_until, + DEFAULT_VALIDITY, )); // Verify it cannot be called for another member without whitelisting the domain @@ -1162,7 +1100,7 @@ mod development { pool_id, tranche_id, DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [9; 20]), - valid_until, + DEFAULT_VALIDITY, ), pallet_liquidity_pools::Error::::InvestorDomainAddressNotAMember, ); @@ -2018,7 +1956,6 @@ mod development { amount, investor.clone(), currency_id, - false, ); // Verify the order was updated to the amount @@ -2042,12 +1979,6 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: amount * 2 - } - ); }); } @@ -2055,10 +1986,6 @@ mod development { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() .add(genesis::balances::(cfg(1_000))) - // .add(genesis::tokens::(vec![( - // GLMR_CURRENCY_ID, - // DEFAULT_BALANCE_GLMR, - // )])) .storage(), ); @@ -2084,7 +2011,6 @@ mod development { invest_amount, investor.clone(), currency_id, - false, ); // Mock incoming decrease message @@ -2176,7 +2102,6 @@ mod development { invest_amount, investor.clone(), currency_id, - false, ); // Verify investment account holds funds before cancelling @@ -2214,20 +2139,6 @@ mod development { msg )); - // Foreign InvestmentState should be cleared - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - })); - // Verify investment was entirely drained from investment account assert_eq!( orml_tokens::Pallet::::balance( @@ -2295,7 +2206,6 @@ mod development { amount, investor.clone(), currency_id, - false, ); let events_before_collect = frame_system::Pallet::::events(); @@ -2395,29 +2305,7 @@ mod development { .into() })); - assert!(!CollectedInvestment::::contains_key( - investor.clone(), - default_investment_id::() - )); - assert!(!InvestmentPaymentCurrency::::contains_key( - investor.clone(), - default_investment_id::() - )); - assert!(!InvestmentState::::contains_key( - investor.clone(), - default_investment_id::() - )); - // Clearing of foreign InvestState should be dispatched - assert!(events_since_collect.iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - })); - assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { @@ -2463,7 +2351,6 @@ mod development { invest_amount, investor.clone(), currency_id, - false, ); enable_liquidity_pool_transferability::(currency_id); let investment_currency_id: CurrencyId = default_investment_id::().into(); @@ -2495,14 +2382,6 @@ mod development { default_investment_id::() ) ); - assert!(!CollectedInvestment::::contains_key( - &investor, - default_investment_id::() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { invest_amount } - ); // Collecting through Investments should denote amounts and transition // state @@ -2511,31 +2390,13 @@ mod development { investor.clone(), default_investment_id::() )); - assert_eq!( - InvestmentPaymentCurrency::::get( - &investor, - default_investment_id::() - ) - .unwrap(), - currency_id - ); assert!( !pallet_investments::Pallet::::investment_requires_collect( &investor, default_investment_id::() ) ); - // The collected amount is transferred automatically - assert!(!CollectedInvestment::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount / 2 - } - ); + // Tranche Tokens should still be transferred to collected to // domain locator account already assert_eq!( @@ -2617,22 +2478,7 @@ mod development { default_investment_id::() ) ); - assert!(!CollectedInvestment::::contains_key( - &investor, - default_investment_id::() - )); - assert!(!InvestmentPaymentCurrency::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!InvestmentPaymentCurrency::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - )); + // Tranche Tokens should be transferred to collected to // domain locator account already let amount_tranche_tokens = invest_amount * 3; @@ -2690,23 +2536,9 @@ mod development { } .into() })); - // Clearing of foreign InvestState should have been dispatched exactly once - assert_eq!( - frame_system::Pallet::::events() - .iter() - .filter(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - }) - .count(), - 1 - ); - // Should fail to collect if `InvestmentState` does not exist + // Should fail to collect if `InvestmentState` does not + // exist let msg = LiquidityPoolMessage::CollectInvest { pool_id, tranche_id: default_tranche_id::(pool_id), @@ -2718,7 +2550,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg ), - pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + pallet_foreign_investments::Error::::InfoNotFound ); }); } @@ -2778,12 +2610,6 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg )); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::Redeeming { - redeem_amount: amount * 2, - } - ); }); } @@ -2869,19 +2695,6 @@ mod development { decrease_amount ); - // Foreign RedemptionState should be updated - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: RedeemState::Redeeming { - redeem_amount: final_amount, - }, - } - .into() - })); - // Order should have been updated assert!(frame_system::Pallet::::events().iter().any(|e| e.event == pallet_investments::Event::::RedeemOrderUpdated { @@ -2996,20 +2809,6 @@ mod development { ), redeem_amount ); - assert!(!RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - - // Foreign RedemptionState should be updated - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - })); // Order should have been updated assert!(frame_system::Pallet::::events().iter().any(|e| e.event @@ -3162,29 +2961,7 @@ mod development { .into() })); - // Foreign CollectedRedemptionTrancheTokens should be killed - assert!( - !pallet_foreign_investments::CollectedRedemption::::contains_key( - investor.clone(), - default_investment_id::(), - ) - ); - - // Foreign RedemptionState should be killed - assert!(!RedemptionState::::contains_key( - investor.clone(), - default_investment_id::() - )); - // Clearing of foreign RedeemState should be dispatched - assert!(events_since_collect.iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - })); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { @@ -3267,14 +3044,7 @@ mod development { default_investment_id::() ) ); - assert!(!CollectedRedemption::::contains_key( - &investor, - default_investment_id::() - )); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::Redeeming { redeem_amount } - ); + // Collecting through investments should denote amounts and transition // state assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( @@ -3321,16 +3091,6 @@ mod development { ); // Since foreign currency is pool currency, the swap is immediately fulfilled // and ExecutedCollectRedeem dispatched - assert!(!CollectedRedemption::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::Redeeming { - redeem_amount: redeem_amount / 2, - } - ); assert!(frame_system::Pallet::::events().iter().any(|e| e.event == orml_tokens::Event::::Withdrawn { currency_id, @@ -3371,14 +3131,6 @@ mod development { default_investment_id::() ) ); - assert!(!CollectedRedemption::::contains_key( - &investor, - default_investment_id::() - )); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id::() - )); assert!(!frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_investments::Event::::RedeemCollectedForNonClearedOrderId { @@ -3411,20 +3163,6 @@ mod development { } .into())); // Clearing of foreign RedeemState should have been dispatched exactly once - assert_eq!( - frame_system::Pallet::::events() - .iter() - .filter(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionCleared { - investor: investor.clone(), - investment_id: default_investment_id::(), - } - .into() - }) - .count(), - 1 - ); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { @@ -3498,7 +3236,6 @@ mod development { invest_amount, investor.clone(), currency_id, - false, ); enable_liquidity_pool_transferability::(currency_id); @@ -3516,9 +3253,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::DecreaseAmountOverflow - ) + pallet_foreign_investments::Error::::TooMuchDecrease ); }); } @@ -3569,9 +3304,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::DecreaseTransition - ) + DispatchError::Arithmetic(sp_runtime::ArithmeticError::Underflow) ); }); } @@ -3612,7 +3345,6 @@ mod development { amount, investor.clone(), currency_id, - false, ); enable_liquidity_pool_transferability::(currency_id); @@ -3649,9 +3381,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::CollectRequired - ) + pallet_investments::Error::::CollectRequired ); // Should fail to decrease @@ -3667,9 +3397,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::CollectRequired - ) + pallet_investments::Error::::CollectRequired ); }); } @@ -3746,9 +3474,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::CollectRequired - ) + pallet_investments::Error::::CollectRequired ); // Should fail to decrease @@ -3764,9 +3490,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::CollectRequired - ) + pallet_investments::Error::::CollectRequired ); }); } @@ -3809,7 +3533,6 @@ mod development { amount, investor.clone(), pool_currency, - false, ); enable_usdt_trading::( @@ -3821,8 +3544,9 @@ mod development { || {}, ); - // Should fail to increase, decrease or collect for another foreign - // currency as long as `InvestmentState` exists + // Should fail to increase, decrease or collect for + // another foreign currency as long as + // `InvestmentState` exists let increase_msg = LiquidityPoolMessage::IncreaseInvestOrder { pool_id, tranche_id: default_tranche_id::(pool_id), @@ -3835,9 +3559,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::InvalidPaymentCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { pool_id, @@ -3851,9 +3573,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::InvalidPaymentCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); let collect_msg = LiquidityPoolMessage::CollectInvest { pool_id, @@ -3866,9 +3586,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg ), - pallet_foreign_investments::Error::::InvestError( - InvestError::InvalidPaymentCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); }); } @@ -3919,8 +3637,9 @@ mod development { amount, )); - // Should fail to increase, decrease or collect for another foreign - // currency as long as `RedemptionState` exists + // Should fail to increase, decrease or collect for + // another foreign currency as long as + // `RedemptionState` exists let increase_msg = LiquidityPoolMessage::IncreaseRedeemOrder { pool_id, tranche_id: default_tranche_id::(pool_id), @@ -3933,9 +3652,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::InvalidPayoutCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); let decrease_msg = LiquidityPoolMessage::DecreaseRedeemOrder { pool_id, @@ -3949,9 +3666,7 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::InvalidPayoutCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); let collect_msg = LiquidityPoolMessage::CollectRedeem { pool_id, @@ -3964,14 +3679,12 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg ), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::InvalidPayoutCurrency - ) + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); }); } - fn invest_payment_currency_not_found() { + fn redeem_payout_currency_not_found() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() .add(genesis::balances::(cfg(1_000))) @@ -3997,12 +3710,11 @@ mod development { pool_currency, currency_decimals.into(), ); - do_initial_increase_investment::( + do_initial_increase_redemption::( pool_id, amount, investor.clone(), pool_currency, - true, ); enable_usdt_trading::( pool_currency, @@ -4012,10 +3724,16 @@ mod development { true, || {}, ); + assert_ok!(orml_tokens::Pallet::::mint_into( + default_investment_id::().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + amount, + )); - // Should fail to decrease or collect for another foreign currency as - // long as `InvestmentState` exists - let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { + // Should fail to decrease or collect for another + // foreign currency as long as `RedemptionState` + // exists + let decrease_msg = LiquidityPoolMessage::DecreaseRedeemOrder { pool_id, tranche_id: default_tranche_id::(pool_id), investor: investor.clone().into(), @@ -4023,107 +3741,35 @@ mod development { amount: 1, }; assert_noop!( - pallet_liquidity_pools::Pallet::::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), - pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); - let collect_msg = LiquidityPoolMessage::CollectInvest { + let collect_msg = LiquidityPoolMessage::CollectRedeem { pool_id, tranche_id: default_tranche_id::(pool_id), investor: investor.clone().into(), currency: general_currency_index::(foreign_currency), }; assert_noop!( - pallet_liquidity_pools::Pallet::::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), - pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + collect_msg + ), + pallet_foreign_investments::Error::::MismatchedForeignCurrency ); }); } - fn redeem_payout_currency_not_found() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let pool_currency = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let amount = 6 * decimals(18); - - create_currency_pool::( - pool_id, - pool_currency, - currency_decimals.into(), - ); - do_initial_increase_redemption::( - pool_id, - amount, - investor.clone(), - pool_currency, - ); - enable_usdt_trading::( - pool_currency, - amount, - true, - true, - true, - || {}, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - amount, - )); - RedemptionPayoutCurrency::::remove( - &investor, - default_investment_id::(), - ); - - // Should fail to decrease or collect for another foreign currency as - // long as `RedemptionState` exists - let decrease_msg = LiquidityPoolMessage::DecreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: 1, - }; - assert_noop!( - pallet_liquidity_pools::Pallet::::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), - pallet_foreign_investments::Error::::RedemptionPayoutCurrencyNotFound - ); - - let collect_msg = LiquidityPoolMessage::CollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - }; - assert_noop!( - pallet_liquidity_pools::Pallet::::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), - pallet_foreign_investments::Error::::RedemptionPayoutCurrencyNotFound - ); - }); - } - - crate::test_for_runtimes!([development], invalid_invest_payment_currency); - crate::test_for_runtimes!([development], invalid_redeem_payout_currency); - crate::test_for_runtimes!([development], invest_payment_currency_not_found); - crate::test_for_runtimes!([development], redeem_payout_currency_not_found); - } - } - } + crate::test_for_runtimes!([development], invalid_invest_payment_currency); + crate::test_for_runtimes!([development], invalid_redeem_payout_currency); + crate::test_for_runtimes!([development], redeem_payout_currency_not_found); + } + } + } mod mismatching_currencies { use super::*; @@ -4163,18 +3809,14 @@ mod development { || {}, ); - do_initial_increase_investment::( + // Short cut: Invest through investments to bump pending investment amount + do_initial_invest_short_cut::( pool_id, invest_amount_pool_denominated, investor.clone(), - pool_currency, - true, ); - // Increase invest order such that collect payment currency gets overwritten - // NOTE: Overwriting InvestmentPaymentCurrency works here because we manually - // clear that state after investing with pool currency as a short cut for - // testing purposes. + // Increase invest order to initialize ForeignInvestmentInfo let msg = LiquidityPoolMessage::IncreaseInvestOrder { pool_id, tranche_id: default_tranche_id::(pool_id), @@ -4203,14 +3845,6 @@ mod development { investor.clone(), default_investment_id::() )); - assert_eq!( - InvestmentPaymentCurrency::::get( - &investor, - default_investment_id::() - ) - .unwrap(), - foreign_currency - ); assert!(orml_tokens::Pallet::::balance( default_investment_id::().into(), &investor @@ -4234,22 +3868,12 @@ mod development { investor: investor.clone().into(), currency: general_currency_index::(foreign_currency), currency_payout: invest_amount_foreign_denominated, - tranche_tokens_payout: invest_amount_pool_denominated * 2, - remaining_invest_amount: 0, + tranche_tokens_payout: 2 * invest_amount_pool_denominated, + remaining_invest_amount: invest_amount_foreign_denominated, }, } .into() })); - - // Should not be cleared as invest state is swapping into pool currency - assert_eq!( - InvestmentPaymentCurrency::::get( - &investor, - default_investment_id::() - ) - .unwrap(), - foreign_currency - ); }); } @@ -4278,12 +3902,12 @@ mod development { pool_currency, pool_currency_decimals.into(), ); - do_initial_increase_investment::( + + // Short cut: Invest through investments to bump pending investment amount + do_initial_invest_short_cut::( pool_id, invest_amount_pool_denominated, investor.clone(), - pool_currency, - true, ); // USDT investment preparations @@ -4293,55 +3917,17 @@ mod development { false, true, true, - || { - let increase_msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: 1, - }; - // Should fail to increase to an invalid payment currency - assert_noop!( - pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - increase_msg - ), - pallet_liquidity_pools::Error::::InvalidPaymentCurrency - ); - }, + || {}, ); - let increase_msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - // Should be able to invest since InvestmentState does not have an active swap, - // i.e. any tradable pair is allowed to invest at this point - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - increase_msg - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: invest_amount_pool_denominated, - currency_in: pool_currency, - currency_out: foreign_currency, - }, - invest_amount: invest_amount_pool_denominated, - }, - } - .into() - })); + // Should be able to invest with foreign currency as ForeignInvestmentState does + // not exist yet + do_initial_increase_investment::( + pool_id, + invest_amount_foreign_denominated, + investor.clone(), + foreign_currency, + ); // Should be able to to decrease in the swapping foreign currency enable_liquidity_pool_transferability::(foreign_currency); @@ -4356,24 +3942,6 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg_pool_swap_amount )); - // Entire swap amount into pool currency should be nullified - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated - } - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated, - }, - } - .into() - })); // Decrease partial investing amount enable_liquidity_pool_transferability::(foreign_currency); @@ -4389,55 +3957,12 @@ mod development { DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg_partial_invest_amount.clone() )); - // Decreased amount should be taken from investing amount - let expected_state = - InvestState::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: Swap { - amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency, - }, - invest_amount: invest_amount_pool_denominated / 2, - }; - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - expected_state.clone() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: expected_state.clone(), - } - .into() - })); // Consume entire investing amount by sending same message assert_ok!(pallet_liquidity_pools::Pallet::::submit( DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg_partial_invest_amount.clone() )); - let expected_state = InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency, - }, - }; - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - expected_state.clone() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignInvestmentUpdated { - investor: investor.clone(), - investment_id: default_investment_id::(), - state: expected_state.clone(), - } - .into() - })); }); } @@ -4472,1635 +3997,77 @@ mod development { pool_currency, pool_currency_decimals.into(), ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &trader, - invest_amount_pool_denominated - )); - - // Increase such that active swap into USDT is initialized - do_initial_increase_investment::( - pool_id, - invest_amount_foreign_denominated, - investor.clone(), - foreign_currency, - false, - ); - let swap_order_id = - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::(), - ) - .expect("Swap order id created during increase"); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ), - Some(ForeignInvestmentInfo { - owner: investor.clone(), - id: default_investment_id::(), - last_swap_reason: Some(TokenSwapReason::Investment) - }) - ); - - // Fulfilling order should propagate it from `ActiveSwapIntoForeignCurrency` to - // `InvestmentOngoing`. - assert_ok!(pallet_order_book::Pallet::::fill_order_full( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: swap_order_id, - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: false, - fulfillment_amount: invest_amount_pool_denominated, - currency_in: pool_currency, - currency_out: foreign_currency, - sell_rate_limit: Ratio::one(), - } - .into() - })); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated - } - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - - // Decrease by half the investment amount - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated / 2, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { - swap: Swap { - amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency, - }, - invest_amount: invest_amount_pool_denominated / 2, - } - ); - let swap_order_id = - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::(), - ) - .expect("Swap order id created during decrease"); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ), - Some(ForeignInvestmentInfo { - owner: investor.clone(), - id: default_investment_id::(), - last_swap_reason: Some(TokenSwapReason::Investment) - }) - ); - - // Fulfill the decrease swap order - assert_ok!(pallet_order_book::Pallet::::fill_order_full( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: swap_order_id, - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: false, - fulfillment_amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency, - sell_rate_limit: Ratio::one(), - } - .into() - })); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated / 2 - } - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: LiquidityPoolMessage::ExecutedDecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 2, - remaining_invest_amount: invest_amount_foreign_denominated / 2, - }, - } - .into() - })); - }); - } - - /// Verify handling concurrent swap orders works if - /// * Invest is swapping from pool to foreign after decreasing an - /// unprocessed investment - /// * Redeem is swapping from pool to foreign after collecting - fn concurrent_swap_orders_same_direction() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::::convert( - (DOMAIN_MOONBEAM, Keyring::Bob.into()), - ); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let swap_order_id = 1; - create_currency_pool::( - pool_id, - pool_currency, - pool_currency_decimals.into(), - ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - // invest in pool currency to reach `InvestmentOngoing` quickly - do_initial_increase_investment::( - pool_id, - invest_amount_pool_denominated, - investor.clone(), - pool_currency, - true, - ); - // Manually set payment currency since we removed it in the above shortcut setup - InvestmentPaymentCurrency::::insert( - &investor, - default_investment_id::(), - foreign_currency, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - foreign_currency, - &trader, - invest_amount_foreign_denominated * 2 - )); - - // Decrease invest setup to have invest order swapping into foreign currency - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - invest_amount_pool_denominated - )); - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_pool_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &pool_account, - invest_amount_pool_denominated - )); - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::InvestmentAndRedemption - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: invest_amount_pool_denominated / 2, - swap: Swap { - amount: invest_amount_foreign_denominated / 8, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionPayoutCurrency::::get(&investor, default_investment_id::()) - .unwrap(), - foreign_currency - ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), - } - .into() - })); - ensure_executed_collect_redeem_not_dispatched::(); - - // Process remaining redemption at 25% rate, i.e. 1 pool currency = - // 4 tranche tokens - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 4; - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), - } - .into() - })); - - // Fulfilling order should kill both the invest as well as redeem state - assert_ok!(pallet_order_book::Pallet::::fill_order_full( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: swap_order_id, - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: false, - fulfillment_amount: invest_amount_foreign_denominated / 4 * 5, - currency_in: foreign_currency, - currency_out: pool_currency, - sell_rate_limit: Ratio::one(), - } - .into() - })); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - )); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id::() - )); - assert!(!RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: LiquidityPoolMessage::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 4, - tranche_tokens_payout: invest_amount_pool_denominated, - remaining_redeem_amount: 0, - }, - } - .into() - })); - }); - } - - /// Verify handling concurrent swap orders works if - /// * Invest is swapping from foreign to pool after increasing - /// * Redeem is swapping from pool to foreign after collecting - fn concurrent_swap_orders_opposite_direction() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::::convert( - (DOMAIN_MOONBEAM, Keyring::Bob.into()), - ); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let swap_order_id = 1; - create_currency_pool::( - pool_id, - pool_currency, - pool_currency_decimals.into(), - ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - foreign_currency, - &trader, - invest_amount_foreign_denominated * 2 - )); - - // Increase invest setup to have invest order swapping into pool currency - do_initial_increase_investment::( - pool_id, - invest_amount_foreign_denominated, - investor.clone(), - foreign_currency, - false, - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoPoolCurrency { - swap: Swap { - amount: invest_amount_pool_denominated, - currency_in: pool_currency, - currency_out: foreign_currency - } - }, - ); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ), - Some(swap_order_id) - ); - - // Redeem setup: Increase and process - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - 3 * invest_amount_pool_denominated - )); - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &pool_account, - 3 * invest_amount_pool_denominated - )); - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_pool_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ), - Some(swap_order_id) - ); - - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ), - Some(swap_order_id) - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - invest_amount: invest_amount_pool_denominated / 8, - swap: Swap { - amount: invest_amount_pool_denominated / 8 * 7, - currency_in: pool_currency, - currency_out: foreign_currency - } - }, - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::Redeeming { - redeem_amount: invest_amount_pool_denominated / 2, - } - ); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: invest_amount_pool_denominated / 8 * 7, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::(pool_currency), - } - .into() - })); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 8, - tranche_tokens_payout: invest_amount_pool_denominated / 2, - remaining_redeem_amount: invest_amount_pool_denominated / 2, - }, - } - .into() - })); - - // Process remaining redemption at 25% rate, i.e. 1 pool currency = - // 4 tranche tokens - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - invest_amount: invest_amount_pool_denominated / 4, - swap: Swap { - amount: invest_amount_pool_denominated / 4 * 3, - currency_in: pool_currency, - currency_out: foreign_currency - } - } - ); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id::() - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: invest_amount_pool_denominated / 4 * 3, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::(pool_currency), - } - .into() - })); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: LiquidityPoolMessage::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 8, - tranche_tokens_payout: invest_amount_pool_denominated / 2, - remaining_redeem_amount: 0, - }, - } - .into() - })); - - // Redeem again with goal of redemption swap to foreign consuming investment - // swap to pool - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_pool_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - // Process remaining redemption at 200% rate, i.e. 1 tranche token = 2 pool - // currency - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(2, 1).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - // Swap order id should be bumped since swap order update occurred for opposite - // direction (from foreign->pool to foreign->pool) - let swap_order_id = 2; - assert_eq!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ), - Some(swap_order_id) - ); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Redemption - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated - } - ); - let remaining_foreign_swap_amount = 2 * invest_amount_foreign_denominated - - invest_amount_foreign_denominated / 4 * 3; - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - done_amount: invest_amount_foreign_denominated / 4 * 3, - swap: Swap { - amount: remaining_foreign_swap_amount, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Fulfilling order should the invest - assert_ok!(pallet_order_book::Pallet::::fill_order_full( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: swap_order_id, - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: false, - fulfillment_amount: remaining_foreign_swap_amount, - currency_in: foreign_currency, - currency_out: pool_currency, - sell_rate_limit: Ratio::one(), - } - .into() - })); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated - } - ); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: LiquidityPoolMessage::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated * 2, - tranche_tokens_payout: invest_amount_pool_denominated, - remaining_redeem_amount: 0, - }, - } - .into() - })); - }); - } - - /// 1. increase initial invest in pool currency - /// 2. increase invest in foreign - /// 3. process invest - /// 4. fulfill swap order - fn fulfill_invest_swap_order_requires_collect() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .add(genesis::tokens::(vec![ - (AUSD_CURRENCY_ID, AUSD_ED), - (USDT_CURRENCY_ID, USDT_ED), - ])) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::::convert( - (DOMAIN_MOONBEAM, Keyring::Bob.into()), - ); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let swap_order_id = 1; - create_currency_pool::( - pool_id, - pool_currency, - pool_currency_decimals.into(), - ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - // invest in pool currency to reach `InvestmentOngoing` quickly - do_initial_increase_investment::( - pool_id, - invest_amount_pool_denominated, - investor.clone(), - pool_currency, - true, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &trader, - invest_amount_pool_denominated - )); - - // Increase invest have - // InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing - let msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { - swap: Swap { - amount: invest_amount_pool_denominated, - currency_in: pool_currency, - currency_out: foreign_currency, - }, - invest_amount: invest_amount_pool_denominated - } - ); - // Process 50% of investment at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(pallet_investments::Pallet::::process_invest_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::invest_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert!( - pallet_investments::Pallet::::investment_requires_collect( - &investor, - default_investment_id::() - ) - ); - - // Fulfill swap order should implicitly collect, otherwise the unprocessed - // investment amount is unknown - assert_ok!(pallet_order_book::Pallet::::fill_order_full( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id - )); - assert!( - !pallet_investments::Pallet::::investment_requires_collect( - &investor, - default_investment_id::() - ) - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::InvestmentOngoing { - invest_amount: invest_amount_pool_denominated / 2 * 3 - } - ); - }); - } - - /// 1. increase initial redeem - /// 2. process partial redemption - /// 3. collect - /// 4. process redemption - /// 5. fulfill swap order should implicitly collect - fn fulfill_redeem_swap_order_requires_collect() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::::convert( - (DOMAIN_MOONBEAM, Keyring::Bob.into()), - ); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let swap_order_id = 1; - create_currency_pool::( - pool_id, - pool_currency, - pool_currency_decimals.into(), - ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - // invest in pool currency to reach `InvestmentOngoing` quickly - do_initial_increase_investment::( - pool_id, - invest_amount_pool_denominated, - investor.clone(), - pool_currency, - true, - ); - // Manually set payment currency since we removed it in the above shortcut setup - InvestmentPaymentCurrency::::insert( - &investor, - default_investment_id::(), - foreign_currency, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - foreign_currency, - &trader, - invest_amount_foreign_denominated * 2 - )); - - // Decrease invest setup to have invest order swapping into foreign currency - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - invest_amount_pool_denominated - )); - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_pool_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &pool_account, - invest_amount_pool_denominated - )); - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::InvestmentAndRedemption - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: invest_amount_pool_denominated / 2, - swap: Swap { - amount: invest_amount_foreign_denominated / 8, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionPayoutCurrency::::get(&investor, default_investment_id::()) - .unwrap(), - foreign_currency - ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), - } - .into() - })); - ensure_executed_collect_redeem_not_dispatched::(); - - // Process remaining redemption at 25% rate, i.e. 1 pool currency = - // 4 tranche tokens - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 4; - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), - } - .into() - })); - - // Partially fulfilling the swap order below the invest swapping amount should - // still have both states swapping into foreign - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 2 - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: swap_order_id, - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: true, - fulfillment_amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency, - sell_rate_limit: Ratio::one(), - } - .into() - })); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency - }, - done_amount: invest_amount_foreign_denominated / 2 - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - }, - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order for the remaining invest swap amount - // should still clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 2 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - }, - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order below the redeem swap amount should still - // clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 8 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: invest_amount_foreign_denominated / 8, - currency_in: foreign_currency, - currency_out: pool_currency - }, - done_amount: invest_amount_foreign_denominated / 8 - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order below the redeem swap amount should still - // clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 8 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 4, - tranche_tokens_payout: invest_amount_pool_denominated, - remaining_redeem_amount: 0, - }, - } - .into() - })); - }); - } - - /// Similar to [concurrent_swap_orders_same_direction] but with - /// partial fulfillment - fn partial_fulfillment_concurrent_swap_orders_same_direction< - T: Runtime + FudgeSupport, - >() { - let mut env = FudgeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - // Increase invest setup - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::::convert( - (DOMAIN_MOONBEAM, Keyring::Bob.into()), - ); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let swap_order_id = 1; - create_currency_pool::( - pool_id, - pool_currency, - pool_currency_decimals.into(), - ); - let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - || {}, - ); - // invest in pool currency to reach `InvestmentOngoing` quickly - do_initial_increase_investment::( - pool_id, - invest_amount_pool_denominated, - investor.clone(), - pool_currency, - true, - ); - // Manually set payment currency since we removed it in the above shortcut setup - InvestmentPaymentCurrency::::insert( - &investor, - default_investment_id::(), - foreign_currency, - ); - assert_ok!(orml_tokens::Pallet::::mint_into( - foreign_currency, - &trader, - invest_amount_foreign_denominated * 2 - )); - - // Decrease invest setup to have invest order swapping into foreign currency - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - invest_amount_pool_denominated - )); - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - amount: invest_amount_pool_denominated, - }; - assert_ok!(pallet_liquidity_pools::Pallet::::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); + let invest_amount_foreign_denominated: u128 = enable_usdt_trading::( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + || {}, + ); assert_ok!(orml_tokens::Pallet::::mint_into( pool_currency, - &pool_account, + &trader, invest_amount_pool_denominated )); - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() - )); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( - default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), + + // Increase such that active swap into USDT is initialized + do_initial_increase_investment::( + pool_id, + invest_amount_foreign_denominated, investor.clone(), - default_investment_id::() - )); - assert_eq!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::InvestmentAndRedemption - ); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { - redeem_amount: invest_amount_pool_denominated / 2, - swap: Swap { - amount: invest_amount_foreign_denominated / 8, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionPayoutCurrency::::get(&investor, default_investment_id::()) - .unwrap(), - foreign_currency + foreign_currency, ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; + let swap_order_id = pallet_foreign_investments::Swaps::::swap_id_from( + &investor, + default_investment_id::(), + pallet_foreign_investments::Action::Investment, + ) + .expect("Swap order exists; qed"); + + // Fulfilling order should propagate it from swapping to investing + assert_ok!(pallet_order_book::Pallet::::fill_order_full( + RawOrigin::Signed(trader.clone()).into(), + swap_order_id + )); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event - == pallet_order_book::Event::::OrderUpdated { + == pallet_order_book::Event::::OrderFulfillment { order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, + placing_account: investor.clone(), + fulfilling_account: trader.clone(), + partial_fulfillment: false, + fulfillment_amount: invest_amount_pool_denominated, + currency_in: pool_currency, + currency_out: foreign_currency, sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), } .into() })); - ensure_executed_collect_redeem_not_dispatched::(); - // Process remaining redemption at 25% rate, i.e. 1 pool currency = - // 4 tranche tokens - assert_ok!(pallet_investments::Pallet::::process_redeem_orders( - default_investment_id::() + // Decrease by half the investment amount + let msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(foreign_currency), + amount: invest_amount_foreign_denominated / 2, + }; + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() )); - assert_ok!(pallet_investments::Pallet::::redeem_fulfillment( + let swap_order_id = pallet_foreign_investments::Swaps::::swap_id_from( + &investor, default_investment_id::(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( - RawOrigin::Signed(Keyring::Charlie.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - } - } - ); - let swap_amount = - invest_amount_foreign_denominated + invest_amount_foreign_denominated / 4; - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderUpdated { - order_id: swap_order_id, - account: investor.clone(), - buy_amount: swap_amount, - sell_rate_limit: Ratio::one(), - min_fulfillment_amount: min_fulfillment_amount::( - foreign_currency, - ), - } - .into() - })); + pallet_foreign_investments::Action::Investment, + ) + .expect("Swap order exists; qed"); - // Partially fulfilling the swap order below the invest swapping amount should - // still have both states swapping into foreign - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( + // Fulfill the decrease swap order + assert_ok!(pallet_order_book::Pallet::::fill_order_full( RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 2 + swap_order_id )); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event @@ -6108,7 +4075,7 @@ mod development { order_id: swap_order_id, placing_account: investor.clone(), fulfilling_account: trader.clone(), - partial_fulfillment: true, + partial_fulfillment: false, fulfillment_amount: invest_amount_foreign_denominated / 2, currency_in: foreign_currency, currency_out: pool_currency, @@ -6116,174 +4083,20 @@ mod development { } .into() })); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id::()), - InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: invest_amount_foreign_denominated / 2, - currency_in: foreign_currency, - currency_out: pool_currency - }, - done_amount: invest_amount_foreign_denominated / 2 - } - ); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - }, - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order for the remaining invest swap amount - // should still clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 2 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrency { - swap: Swap { - amount: invest_amount_foreign_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency - }, - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order below the redeem swap amount should still - // clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 8 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id::()), - RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { - swap: Swap { - amount: invest_amount_foreign_denominated / 8, - currency_in: foreign_currency, - currency_out: pool_currency - }, - done_amount: invest_amount_foreign_denominated / 8 - } - ); - assert!(RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_some() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_some() - ); - ensure_executed_collect_redeem_not_dispatched::(); - - // Partially fulfilling the swap order below the redeem swap amount should still - // clear the investment state - assert_ok!(pallet_order_book::Pallet::::fill_order_partial( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_foreign_denominated / 8 - )); - assert!(!InvestmentState::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id::() - ),); - assert!(!RedemptionPayoutCurrency::::contains_key( - &investor, - default_investment_id::() - )); - assert!( - pallet_foreign_investments::Pallet::::foreign_investment_info( - swap_order_id - ) - .is_none() - ); - assert!( - pallet_foreign_investments::Pallet::::token_swap_order_ids( - &investor, - default_investment_id::() - ) - .is_none() - ); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), - domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), - message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - currency_payout: invest_amount_foreign_denominated / 4, - tranche_tokens_payout: invest_amount_pool_denominated, - remaining_redeem_amount: 0, - }, - } + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: LiquidityPoolMessage::ExecutedDecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 2, + remaining_invest_amount: invest_amount_foreign_denominated / 2, + }, + } .into() })); }); @@ -6292,14 +4105,6 @@ mod development { crate::test_for_runtimes!([development], collect_foreign_investment_for); crate::test_for_runtimes!([development], invest_increase_decrease); crate::test_for_runtimes!([development], invest_swaps_happy_path); - crate::test_for_runtimes!([development], concurrent_swap_orders_same_direction); - crate::test_for_runtimes!([development], concurrent_swap_orders_opposite_direction); - crate::test_for_runtimes!([development], fulfill_invest_swap_order_requires_collect); - crate::test_for_runtimes!([development], fulfill_redeem_swap_order_requires_collect); - crate::test_for_runtimes!( - [development], - partial_fulfillment_concurrent_swap_orders_same_direction - ); } } @@ -6516,16 +4321,12 @@ mod development { // Whitelist destination as TrancheInvestor of this Pool let valid_until = u64::MAX; - assert_ok!(pallet_permissions::Pallet::::add( - RawOrigin::Signed(receiver.into()).into(), - Role::PoolRole(PoolRole::InvestorAdmin), + + crate::generic::utils::give_pool_role::( AccountConverter::::convert(dest_address.clone()), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id::(pool_id), - valid_until - )), - )); + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), valid_until), + ); // Call the pallet_liquidity_pools::Pallet::::update_member which ensures the // destination address is whitelisted. @@ -6634,16 +4435,11 @@ mod development { )); // Whitelist destination as TrancheInvestor of this Pool - assert_ok!(pallet_permissions::Pallet::::add( - RawOrigin::Signed(receiver.clone()).into(), - Role::PoolRole(PoolRole::InvestorAdmin), + crate::generic::utils::give_pool_role::( receiver.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id::(pool_id), - valid_until - )), - )); + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), valid_until), + ); // Finally, verify that we can now transfer the tranche to the destination // address @@ -6690,37 +4486,29 @@ mod development { assert!(pallet_pool_system::Pallet::::pool(invalid_pool_id).is_none()); // Make Keyring::Bob the MembersListAdmin of both pools - assert_ok!(pallet_permissions::Pallet::::add( - ::RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), + crate::generic::utils::give_pool_role::( Keyring::Bob.into(), - PermissionScope::Pool(valid_pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - assert_ok!(pallet_permissions::Pallet::::add( - ::RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), + valid_pool_id, + PoolRole::InvestorAdmin, + ); + crate::generic::utils::give_pool_role::( Keyring::Bob.into(), - PermissionScope::Pool(invalid_pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); + invalid_pool_id, + PoolRole::InvestorAdmin, + ); // Give Keyring::Bob investor role for (valid_pool_id, invalid_tranche_id) and // (invalid_pool_id, valid_tranche_id) - assert_ok!(pallet_permissions::Pallet::::add( - RawOrigin::Signed(Keyring::Bob.into()).into(), - Role::PoolRole(PoolRole::InvestorAdmin), + crate::generic::utils::give_pool_role::( AccountConverter::::convert(dest_address.clone()), - PermissionScope::Pool(invalid_pool_id), - Role::PoolRole(PoolRole::TrancheInvestor(valid_tranche_id, valid_until)), - )); - assert_ok!(pallet_permissions::Pallet::::add( - RawOrigin::Signed(Keyring::Bob.into()).into(), - Role::PoolRole(PoolRole::InvestorAdmin), + invalid_pool_id, + PoolRole::TrancheInvestor(valid_tranche_id, valid_until), + ); + crate::generic::utils::give_pool_role::( AccountConverter::::convert(dest_address.clone()), - PermissionScope::Pool(valid_pool_id), - Role::PoolRole(PoolRole::TrancheInvestor(invalid_tranche_id, valid_until)), - )); + valid_pool_id, + PoolRole::TrancheInvestor(invalid_tranche_id, valid_until), + ); assert_noop!( pallet_liquidity_pools::Pallet::::transfer_tranche_tokens( RawOrigin::Signed(Keyring::Bob.into()).into(), diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index 7befe67601..10ea1815be 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -137,7 +137,7 @@ pub trait Runtime: Balance = Balance, InvestmentId = TrancheCurrency, CurrencyId = CurrencyId, - TokenSwapOrderId = u64, + SwapId = u64, > + pallet_preimage::Config + pallet_collective::Config + pallet_democracy::Config> @@ -191,7 +191,6 @@ pub trait Runtime: + From> + From> + From> - + From> + From> + From> + From>