diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index f43091e5fb..6d42bdc062 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -297,7 +297,6 @@ pub trait ForeignInvestment { type CurrencyId; type Error: Debug; type InvestmentId; - type CollectInvestResult; /// Initiates the increment of a foreign investment amount in /// `foreign_payment_currency` of who into the investment class @@ -365,8 +364,7 @@ pub trait ForeignInvestment { who: &AccountId, investment_id: Self::InvestmentId, foreign_currency: Self::CurrencyId, - pool_currency: Self::CurrencyId, - ) -> Result; + ) -> Result<(), Self::Error>; /// Collect the results of a user's foreign redeem orders for the given /// investment. If any amounts are not fulfilled they are directly diff --git a/libs/types/src/investments.rs b/libs/types/src/investments.rs index 25d6008c50..99bdbd2885 100644 --- a/libs/types/src/investments.rs +++ b/libs/types/src/investments.rs @@ -185,7 +185,7 @@ pub struct Swap< pub currency_in: Currency, /// The outgoing currency, i.e. the one which should be replaced. pub currency_out: Currency, - /// The amount of outgoing currency which shall be exchanged. + /// The amount of incoming currency which shall be bought. pub amount: Balance, } @@ -237,30 +237,29 @@ pub struct ExecutedForeignDecreaseInvest { pub amount_remaining: Balance, } -/// A representation of an executed collected investment. +/// A representation of an executed collected foreign investment or redemption. #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] -pub struct ExecutedForeignCollectInvest { - /// The amount that was actually collected - pub amount_currency_payout: Balance, - /// The amount of tranche tokens received for the investment made - pub amount_tranche_tokens_payout: Balance, - /// The unprocessed plus processed but not yet collected investment amount - /// denominated in foreign currency - pub amount_remaining_invest: Balance, -} - -/// A representation of an executed collected redemption. -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] - -pub struct ExecutedForeignCollectRedeem { - /// The foreign currency in which the payout takes place +pub struct ExecutedForeignCollect { + /// The foreign currency in which ... + /// * If investment: the payment took place + /// * If redemption: the payout takes place pub currency: Currency, - /// The amount of `currency` being paid out to the investor + + /// The amount of `currency`... + /// * If investment: that was collected + /// * If redemption: paid out to the investor pub amount_currency_payout: Balance, - /// How many tranche tokens were actually redeemed + + /// The amount of tranche tokens... + /// * If investment: received for the investment made + /// * If redemption: which were actually redeemed pub amount_tranche_tokens_payout: Balance, - /// The unprocessed plus processed but not yet collected redemption amount - /// of tranche tokens - pub amount_remaining_redeem: Balance, + + /// The unprocessed ... + /// * If investment: investment amount of `currency` (denominated in foreign + /// currency) + /// * If redemption: redemption amount of tranche tokens (denominated in + /// pool currency) + pub amount_remaining: Balance, } diff --git a/pallets/foreign-investments/src/errors.rs b/pallets/foreign-investments/src/errors.rs index a1e670bac2..d24ea12eaa 100644 --- a/pallets/foreign-investments/src/errors.rs +++ b/pallets/foreign-investments/src/errors.rs @@ -32,6 +32,12 @@ pub enum InvestError { 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)] @@ -41,13 +47,6 @@ pub enum RedeemError { IncreaseTransition, /// Failed to collect the redemption. CollectTransition, - /// 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. - CollectPayoutCurrencyNotFound, /// The desired decreasing amount exceeds the max amount. DecreaseAmountOverflow, /// Failed to transition the state as a result of a decrease. @@ -56,6 +55,12 @@ pub enum RedeemError { 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 { diff --git a/pallets/foreign-investments/src/impls/mod.rs b/pallets/foreign-investments/src/impls/mod.rs index 4b82327e70..8ff180b510 100644 --- a/pallets/foreign-investments/src/impls/mod.rs +++ b/pallets/foreign-investments/src/impls/mod.rs @@ -17,8 +17,7 @@ use cfg_traits::{ IdentityCurrencyConversion, PoolInspect, StatusNotificationHook, TokenSwaps, }; use cfg_types::investments::{ - CollectedAmount, ExecutedForeignCollectInvest, ExecutedForeignCollectRedeem, - ExecutedForeignDecreaseInvest, Swap, + CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, Swap, }; use frame_support::{ensure, traits::Get, transactional}; use sp_runtime::{ @@ -30,8 +29,8 @@ use crate::{ errors::{InvestError, RedeemError}, types::{InvestState, InvestTransition, RedeemState, RedeemTransition, TokenSwapReason}, CollectedInvestment, CollectedRedemption, Config, Error, Event, ForeignInvestmentInfo, - ForeignInvestmentInfoOf, InvestmentState, Pallet, RedemptionPayoutCurrency, RedemptionState, - SwapOf, TokenSwapOrderIds, + ForeignInvestmentInfoOf, InvestmentPaymentCurrency, InvestmentState, Pallet, + RedemptionPayoutCurrency, RedemptionState, SwapOf, TokenSwapOrderIds, }; mod invest; @@ -39,7 +38,6 @@ mod redeem; impl ForeignInvestment for Pallet { type Amount = T::Balance; - type CollectInvestResult = ExecutedForeignCollectInvest; type CurrencyId = T::CurrencyId; type Error = DispatchError; type InvestmentId = T::InvestmentId; @@ -56,6 +54,29 @@ impl ForeignInvestment for Pallet { !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); @@ -86,6 +107,11 @@ impl ForeignInvestment for Pallet { !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!( @@ -116,18 +142,23 @@ impl ForeignInvestment for Pallet { amount: T::Balance, payout_currency: T::CurrencyId, ) -> Result<(), DispatchError> { - let currency_matches = - RedemptionPayoutCurrency::::mutate(who, investment_id, |maybe_currency| { + let currency_matches = RedemptionPayoutCurrency::::try_mutate_exists( + who, + investment_id, + |maybe_currency| { if let Some(currency) = maybe_currency { - currency == &payout_currency + Ok::(currency == &payout_currency) } else { *maybe_currency = Some(payout_currency); - true + Ok::(true) } - }); + }, + ) + // An error reflects the payout currency has not been set yet + .unwrap_or(true); ensure!( currency_matches, - Error::::InvalidRedemptionPayoutCurrency + Error::::RedeemError(RedeemError::InvalidPayoutCurrency) ); ensure!( !T::Investment::redemption_requires_collect(who, investment_id), @@ -154,14 +185,10 @@ impl ForeignInvestment for Pallet { amount: T::Balance, payout_currency: T::CurrencyId, ) -> Result<(T::Balance, T::Balance), DispatchError> { + let stored_payout_currency = RedemptionPayoutCurrency::::get(who, investment_id)?; ensure!( - RedemptionPayoutCurrency::::get(who, investment_id) - .map(|currency| currency == payout_currency) - .unwrap_or_else(|| { - log::debug!("Redemption payout currency missing when calling decrease. Should never occur if redemption has been increased beforehand"); - false - }), - Error::::InvalidRedemptionPayoutCurrency + stored_payout_currency == payout_currency, + Error::::RedeemError(RedeemError::InvalidPayoutCurrency) ); ensure!( !T::Investment::redemption_requires_collect(who, investment_id), @@ -186,20 +213,22 @@ impl ForeignInvestment for Pallet { fn collect_foreign_investment( who: &T::AccountId, investment_id: T::InvestmentId, - foreign_payout_currency: T::CurrencyId, - pool_currency: T::CurrencyId, - ) -> Result, DispatchError> { + 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. + // `CollectedInvestment` amount and notifying any consumer of + // `ExecutedForeignInvestmentHook` which is expected to dispatch + // `ExecutedCollectInvest`. T::Investment::collect_investment(who.clone(), investment_id)?; - Self::transfer_collected_investment( - who, - investment_id, - foreign_payout_currency, - pool_currency, - ) + Ok(()) } #[transactional] @@ -209,14 +238,10 @@ impl ForeignInvestment for Pallet { foreign_payout_currency: T::CurrencyId, pool_currency: T::CurrencyId, ) -> Result<(), DispatchError> { + let payout_currency = RedemptionPayoutCurrency::::get(who, investment_id)?; ensure!( - RedemptionPayoutCurrency::::get(who, investment_id) - .map(|currency| currency == foreign_payout_currency) - .unwrap_or_else(|| { - log::debug!("Corruption: Redemption payout currency missing when calling decrease. Should never occur if redemption has been increased beforehand"); - false - }), - Error::::InvalidRedemptionPayoutCurrency + payout_currency == foreign_payout_currency, + Error::::RedeemError(RedeemError::InvalidPayoutCurrency) ); ensure!(T::PoolInspect::currency_for(investment_id.of_pool()) .map(|currency| currency == pool_currency) @@ -325,6 +350,7 @@ impl Pallet { match state { InvestState::NoState => { InvestmentState::::remove(who, investment_id); + InvestmentPaymentCurrency::::remove(who, investment_id); Ok((InvestState::NoState, None, Zero::zero())) }, @@ -367,6 +393,7 @@ impl Pallet { 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())) }, @@ -456,6 +483,7 @@ impl Pallet { match state { RedeemState::NoState => { RedemptionState::::remove(who, investment_id); + RedemptionPayoutCurrency::::remove(who, investment_id); Ok((Some(RedeemState::NoState), None)) } RedeemState::Redeeming { .. } => { @@ -610,6 +638,7 @@ impl Pallet { match state { RedeemState::SwapIntoForeignDone { .. } => { RedemptionState::::remove(who, investment_id); + RedemptionPayoutCurrency::::remove(who, investment_id); Ok(Some(RedeemState::NoState)) } RedeemState::RedeemingAndSwapIntoForeignDone { redeem_amount, .. } => { @@ -1017,28 +1046,37 @@ impl Pallet { /// state as a result of collecting the investment. /// /// NOTE: Does not transfer back the collected tranche tokens. This happens - /// in `transfer_collected_investment`. + /// 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`) - CollectedInvestment::::mutate(who, investment_id, |collected_before| { - collected_before - .amount_collected + let nothing_collect = CollectedInvestment::::mutate(who, investment_id, |c| { + c.amount_collected .ensure_add_assign(collected.amount_collected)?; - collected_before - .amount_payment + c.amount_payment .ensure_add_assign(collected.amount_payment)?; - Ok::<(), DispatchError>(()) + 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) @@ -1047,39 +1085,6 @@ impl Pallet { Ok(()) } - /// Consumes the `CollectedInvestment` amounts and returns these. - /// - /// NOTE: Converts the collected pool currency payment amount to foreign - /// currency via the `CurrencyConverter` trait. - pub(crate) fn transfer_collected_investment( - who: &T::AccountId, - investment_id: T::InvestmentId, - foreign_payout_currency: T::CurrencyId, - pool_currency: T::CurrencyId, - ) -> Result, DispatchError> { - 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, - )?; - - Ok(ExecutedForeignCollectInvest { - amount_currency_payout, - amount_tranche_tokens_payout: collected.amount_collected, - amount_remaining_invest: amount_remaining_invest_foreign_denominated, - }) - } - /// Increments the collected redemption amount and transitions redemption /// state as a result of collecting the redemption. /// @@ -1091,22 +1096,24 @@ impl Pallet { investment_id: T::InvestmentId, collected: CollectedAmount, ) -> DispatchResult { - let foreign_payout_currency = RedemptionPayoutCurrency::::get(who, investment_id) - .ok_or(Error::::RedeemError( - RedeemError::CollectPayoutCurrencyNotFound, - ))?; + 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`) - CollectedRedemption::::mutate(who, investment_id, |old| { - old.amount_collected + let nothing_collect = CollectedRedemption::::mutate(who, investment_id, |c| { + c.amount_collected .ensure_add_assign(collected.amount_collected)?; - old.amount_payment + c.amount_payment .ensure_add_assign(collected.amount_payment)?; - Ok::<(), DispatchError>(()) + 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)?; @@ -1171,9 +1178,55 @@ impl Pallet { ) } + /// 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 - /// `ExecutedCollectRedeemOrder`. + /// `ExecutedCollectRedeem`. #[transactional] pub(crate) fn notify_executed_collect_redeem( who: &T::AccountId, @@ -1188,11 +1241,11 @@ impl Pallet { // not relevant here last_swap_reason: None, }, - ExecutedForeignCollectRedeem { + ExecutedForeignCollect { currency, amount_currency_payout: collected.amount_collected, amount_tranche_tokens_payout: collected.amount_payment, - amount_remaining_redeem: T::Investment::redemption(who, investment_id)?, + amount_remaining: T::Investment::redemption(who, investment_id)?, }, ) } diff --git a/pallets/foreign-investments/src/lib.rs b/pallets/foreign-investments/src/lib.rs index 04721d6aee..34907f4790 100644 --- a/pallets/foreign-investments/src/lib.rs +++ b/pallets/foreign-investments/src/lib.rs @@ -68,7 +68,7 @@ pub mod pallet { PoolInspect, StatusNotificationHook, TokenSwaps, }; use cfg_types::investments::{ - CollectedAmount, ExecutedForeignCollectRedeem, ExecutedForeignDecreaseInvest, + CollectedAmount, ExecutedForeignCollect, ExecutedForeignDecreaseInvest, }; use errors::{InvestError, RedeemError}; use frame_support::{dispatch::HasCompact, pallet_prelude::*}; @@ -211,7 +211,18 @@ pub mod pallet { Self::InvestmentId, (), >, - Status = ExecutedForeignCollectRedeem, + 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, + (), + >, + Status = ExecutedForeignCollect, Error = DispatchError, >; @@ -318,7 +329,7 @@ pub mod pallet { /// 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 - /// `transfer_collected_investment` which is part of + /// `notify_executed_collect_invest` which is part of /// `collect_foreign_investment`. #[pallet::storage] pub type CollectedInvestment = StorageDoubleMap< @@ -351,12 +362,27 @@ pub mod pallet { 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. /// - /// TODO(future): The lifetime of this storage is currently defensively - /// indefinite. It should most likely mirror the one of `RedemptionState` - /// though right now it + /// The lifetime is synchronized with the one of + /// `RedemptionState`. #[pallet::storage] pub type RedemptionPayoutCurrency = StorageDoubleMap< _, @@ -365,6 +391,7 @@ pub mod pallet { Blake2_128Concat, T::InvestmentId, T::CurrencyId, + ResultQuery::RedemptionPayoutCurrencyNotFound>, >; #[pallet::event] @@ -392,15 +419,23 @@ pub mod pallet { #[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, - /// The provided currency does not match the one provided 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. - InvalidRedemptionPayoutCurrency, /// Failed to retrieve the `TokenSwapReason` from the given /// `TokenSwapOrderId`. TokenSwapReasonNotFound, @@ -412,17 +447,7 @@ pub mod pallet { InvestError(InvestError), /// Failed to transition the `RedeemState.` RedeemError(RedeemError), + /// Failed to retrieve the pool for the given pool id. + PoolNotFound, } - - // 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/liquidity-pools/src/hooks.rs b/pallets/liquidity-pools/src/hooks.rs index abf123b8b3..88167eaed0 100644 --- a/pallets/liquidity-pools/src/hooks.rs +++ b/pallets/liquidity-pools/src/hooks.rs @@ -15,12 +15,15 @@ use cfg_traits::{ investments::TrancheCurrency, liquidity_pools::OutboundQueue, StatusNotificationHook, }; use cfg_types::{ - domain_address::DomainAddress, - investments::{ExecutedForeignDecreaseInvest, ForeignInvestmentInfo}, + domain_address::{Domain, DomainAddress}, + investments::{ExecutedForeignCollect, ExecutedForeignDecreaseInvest, ForeignInvestmentInfo}, +}; +use frame_support::{ + traits::fungibles::{Mutate, Transfer}, + transactional, }; -use frame_support::{traits::fungibles::Mutate, transactional}; use sp_core::Get; -use sp_runtime::{DispatchError, DispatchResult}; +use sp_runtime::{traits::Convert, DispatchError, DispatchResult}; use sp_std::marker::PhantomData; use crate::{pallet::Config, Message, MessageOf, Pallet}; @@ -76,12 +79,12 @@ where { type Error = DispatchError; type Id = ForeignInvestmentInfo; - type Status = cfg_types::investments::ExecutedForeignCollectRedeem; + type Status = ExecutedForeignCollect; #[transactional] fn notify_status_change( id: ForeignInvestmentInfo, - status: cfg_types::investments::ExecutedForeignCollectRedeem, + status: ExecutedForeignCollect, ) -> DispatchResult { let ForeignInvestmentInfo { id: investment_id, @@ -101,7 +104,56 @@ where currency, currency_payout: status.amount_currency_payout, tranche_tokens_payout: status.amount_tranche_tokens_payout, - remaining_redeem_amount: status.amount_remaining_redeem, + remaining_redeem_amount: status.amount_remaining, + }; + + T::OutboundQueue::submit(T::TreasuryAccount::get(), domain_address.domain(), message)?; + + Ok(()) + } +} + +/// The hook struct which acts upon a finalized investment collection. +pub struct CollectedForeignInvestmentHook(PhantomData); + +impl StatusNotificationHook for CollectedForeignInvestmentHook +where + ::AccountId: Into<[u8; 32]>, +{ + type Error = DispatchError; + type Id = ForeignInvestmentInfo; + type Status = ExecutedForeignCollect; + + #[transactional] + fn notify_status_change( + id: ForeignInvestmentInfo, + 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(); + + T::Tokens::transfer( + investment_id.clone().into(), + &investor, + &Domain::convert(domain_address.domain()), + status.amount_tranche_tokens_payout, + false, + )?; + + let message: MessageOf = Message::ExecutedCollectInvest { + pool_id: investment_id.of_pool(), + tranche_id: investment_id.of_tranche(), + investor: investor.into(), + currency, + currency_payout: status.amount_currency_payout, + tranche_tokens_payout: status.amount_tranche_tokens_payout, + remaining_invest_amount: status.amount_remaining, }; T::OutboundQueue::submit(T::TreasuryAccount::get(), domain_address.domain(), message)?; diff --git a/pallets/liquidity-pools/src/inbound.rs b/pallets/liquidity-pools/src/inbound.rs index 68d24cbc15..d6ef0abbf2 100644 --- a/pallets/liquidity-pools/src/inbound.rs +++ b/pallets/liquidity-pools/src/inbound.rs @@ -16,7 +16,6 @@ use cfg_traits::{ }; use cfg_types::{ domain_address::{Domain, DomainAddress}, - investments::ExecutedForeignCollectInvest, permissions::{PermissionScope, PoolRole, Role}, }; use frame_support::{ @@ -138,7 +137,10 @@ where amount: ::Balance, ) -> 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)?; + // 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` + 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)?; @@ -146,7 +148,7 @@ where &investor, invest_id, amount, - payment_currency, + payout_currency, pool_currency, )?; @@ -195,7 +197,6 @@ where // Transfer tranche tokens from `DomainLocator` account of // origination domain - // TODO(@review): Should this rather be part of `increase_foreign_redemption`? T::Tokens::transfer( invest_id.clone().into(), &Domain::convert(sending_domain.domain()), @@ -305,44 +306,13 @@ where tranche_id: T::TrancheId, investor: T::AccountId, currency_index: GeneralCurrencyIndexOf, - destination: DomainAddress, ) -> DispatchResult { let invest_id: T::TrancheCurrency = Self::derive_invest_id(pool_id, tranche_id)?; - let currency_index_u128 = currency_index.index; - 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)?; - - let ExecutedForeignCollectInvest:: { - amount_currency_payout, - amount_tranche_tokens_payout, - amount_remaining_invest, - } = T::ForeignInvestment::collect_foreign_investment( - &investor, - invest_id.clone(), - payout_currency, - pool_currency, - )?; - - T::Tokens::transfer( - invest_id.into(), - &investor, - &Domain::convert(destination.domain()), - amount_tranche_tokens_payout, - false, - )?; - - let message: MessageOf = Message::ExecutedCollectInvest { - pool_id, - tranche_id, - investor: investor.into(), - currency: currency_index_u128, - currency_payout: amount_currency_payout, - tranche_tokens_payout: amount_tranche_tokens_payout, - remaining_invest_amount: amount_remaining_invest, - }; + let payment_currency = Self::try_get_payment_currency(invest_id.clone(), currency_index)?; - T::OutboundQueue::submit(T::TreasuryAccount::get(), destination.domain(), message)?; + // NOTE: Dispatch of `ExecutedCollectInvest` is handled by + // `ExecutedCollectInvestHook` + T::ForeignInvestment::collect_foreign_investment(&investor, invest_id, payment_currency)?; Ok(()) } diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index 4ee732d2b8..5c07b3f0ef 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -44,7 +44,6 @@ use core::convert::TryFrom; use cfg_traits::liquidity_pools::{InboundQueue, OutboundQueue}; use cfg_types::{ domain_address::{Domain, DomainAddress}, - investments::ExecutedForeignCollectInvest, tokens::GeneralCurrencyIndex, }; use cfg_utils::vec_to_fixed_array; @@ -227,7 +226,6 @@ pub mod pallet { CurrencyId = CurrencyIdOf, Error = DispatchError, InvestmentId = ::TrancheCurrency, - CollectInvestResult = ExecutedForeignCollectInvest, >; /// The source of truth for the transferability of assets via the @@ -679,7 +677,7 @@ pub mod pallet { /// pool on the domain derived from the given currency. #[pallet::call_index(9)] #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] - pub fn allow_pool_currency( + pub fn allow_investment_currency( origin: OriginFor, pool_id: T::PoolId, tranche_id: T::TrancheId, @@ -690,12 +688,19 @@ pub mod pallet { // See spec: https://centrifuge.hackmd.io/SERpps-URlG4hkOyyS94-w?view#fn-add_pool_currency let who = ensure_signed(origin)?; - // Ensure currency matches the currency of the pool + // Ensure currency is allowed as payment and payout currency for pool let invest_id = Self::derive_invest_id(pool_id, tranche_id)?; + // Required for increasing and collecting investments ensure!( - T::ForeignInvestment::accepted_payment_currency(invest_id, currency_id), + T::ForeignInvestment::accepted_payment_currency(invest_id.clone(), currency_id), Error::::InvalidPaymentCurrency ); + // Required for decreasing investments as well as increasing, decreasing and + // collecting redemptions + ensure!( + T::ForeignInvestment::accepted_payout_currency(invest_id, currency_id), + Error::::InvalidPayoutCurrency + ); // Ensure the currency is enabled as pool_currency let metadata = @@ -799,8 +804,6 @@ pub mod pallet { }, ) } - - // TODO(@future): pub fn update_tranche_investment_limit } impl Pallet { @@ -917,13 +920,18 @@ pub mod pallet { /// Ensures that currency id can be derived from the /// GeneralCurrencyIndex and that the former is an accepted payout /// currency for the given investment id. - /// - /// NOTE: Exactly the same as try_get_payment_currency for now. pub fn try_get_payout_currency( invest_id: ::TrancheCurrency, currency_index: GeneralCurrencyIndexOf, ) -> Result, DispatchError> { - Self::try_get_payment_currency(invest_id, currency_index) + let currency = Self::try_get_currency_id(currency_index)?; + + ensure!( + T::ForeignInvestment::accepted_payout_currency(invest_id, currency), + Error::::InvalidPaymentCurrency + ); + + Ok(currency) } } @@ -1025,7 +1033,6 @@ pub mod pallet { tranche_id, T::DomainAccountToAccountId::convert((sender.domain(), investor)), currency.into(), - sender, ), Message::CollectRedeem { pool_id, diff --git a/pallets/liquidity-pools/src/message.rs b/pallets/liquidity-pools/src/message.rs index e84a431b27..7a9a2cb358 100644 --- a/pallets/liquidity-pools/src/message.rs +++ b/pallets/liquidity-pools/src/message.rs @@ -1006,7 +1006,7 @@ mod tests { } #[test] - fn allow_pool_currency() { + fn allow_investment_currency() { test_encode_decode_identity( LiquidityPoolsMessage::AllowInvestmentCurrency { currency: TOKEN_ID, @@ -1017,7 +1017,7 @@ mod tests { } #[test] - fn allow_pool_currency_zero() { + fn allow_investment_currency_zero() { test_encode_decode_identity( LiquidityPoolsMessage::AllowInvestmentCurrency { currency: 0, diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index c6ac5c4300..3ce9b7f2fb 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -1394,6 +1394,8 @@ parameter_types! { impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; + type CollectedForeignInvestmentHook = + pallet_liquidity_pools::hooks::CollectedForeignInvestmentHook; type CollectedForeignRedemptionHook = pallet_liquidity_pools::hooks::CollectedForeignRedemptionHook; type CurrencyConverter = diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index 2755f2beda..d5f308fbe0 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -434,6 +434,8 @@ parameter_types! { impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; + type CollectedForeignInvestmentHook = + pallet_liquidity_pools::hooks::CollectedForeignInvestmentHook; type CollectedForeignRedemptionHook = pallet_liquidity_pools::hooks::CollectedForeignRedemptionHook; type CurrencyConverter = diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index ca70d87651..e676316962 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -1550,6 +1550,8 @@ parameter_types! { impl pallet_foreign_investments::Config for Runtime { type Balance = Balance; + type CollectedForeignInvestmentHook = + pallet_liquidity_pools::hooks::CollectedForeignInvestmentHook; type CollectedForeignRedemptionHook = pallet_liquidity_pools::hooks::CollectedForeignRedemptionHook; type CurrencyConverter = diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/add_allow_upgrade.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/add_allow_upgrade.rs index 656fd5de9e..b6938b01f6 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/add_allow_upgrade.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/add_allow_upgrade.rs @@ -37,7 +37,7 @@ use cfg_types::{ }, }; use development_runtime::{ - LiquidityPools, LocationToAccountId, OrmlAssetRegistry, OrmlTokens, Permissions, + LiquidityPools, LocationToAccountId, OrderBook, OrmlAssetRegistry, OrmlTokens, Permissions, Runtime as DevelopmentRuntime, RuntimeOrigin, System, TreasuryAccount, XTokens, XcmTransactor, }; use frame_support::{assert_noop, assert_ok, traits::fungibles::Mutate}; @@ -426,7 +426,7 @@ fn add_currency_should_fail() { } #[test] -fn allow_pool_currency() { +fn allow_investment_currency() { TestNet::reset(); Development::execute_with(|| { setup_pre_requirements(); @@ -461,7 +461,7 @@ fn allow_pool_currency() { }) )); - assert_ok!(LiquidityPools::allow_pool_currency( + assert_ok!(LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, default_tranche_id(pool_id), @@ -482,7 +482,7 @@ fn allow_pool_should_fail() { setup_pre_requirements(); // Should fail if pool does not exist assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, // Tranche id is arbitrary in this case as pool does not exist @@ -509,10 +509,10 @@ fn allow_pool_should_fail() { // Create pool create_currency_pool(pool_id, currency_id, 10_000 * dollar(12)); - // Should fail if asset is not pool currency + // Should fail if asset is not payment currency assert!(currency_id != ausd_currency_id); assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, default_tranche_id(pool_id), @@ -521,6 +521,25 @@ fn allow_pool_should_fail() { pallet_liquidity_pools::Error::::InvalidPaymentCurrency ); + // Allow as payment but not payout currency + assert_ok!(OrderBook::add_trading_pair( + RuntimeOrigin::root(), + currency_id, + ausd_currency_id, + Default::default() + )); + // Should fail if asset is not payout currency + enable_liquidity_pool_transferability(ausd_currency_id); + assert_noop!( + LiquidityPools::allow_investment_currency( + RuntimeOrigin::signed(BOB.into()), + pool_id, + default_tranche_id(pool_id), + ausd_currency_id, + ), + pallet_liquidity_pools::Error::::InvalidPayoutCurrency + ); + // Should fail if currency is not liquidityPools transferable assert_ok!(OrmlAssetRegistry::update_asset( RuntimeOrigin::root(), @@ -540,7 +559,7 @@ fn allow_pool_should_fail() { }), )); assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, default_tranche_id(pool_id), @@ -568,7 +587,7 @@ fn allow_pool_should_fail() { }), )); assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, default_tranche_id(pool_id), @@ -592,7 +611,7 @@ fn allow_pool_should_fail() { None, )); assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id, default_tranche_id(pool_id), @@ -618,7 +637,7 @@ fn allow_pool_should_fail() { create_currency_pool(pool_id + 1, CurrencyId::AUSD, 10_000 * dollar(12)); // Should fail if currency is not foreign asset assert_noop!( - LiquidityPools::allow_pool_currency( + LiquidityPools::allow_investment_currency( RuntimeOrigin::signed(BOB.into()), pool_id + 1, // Tranche id is arbitrary in this case, so we don't need to check for the exact diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs index 40a3f044ab..493e5a8e33 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs @@ -55,7 +55,8 @@ use frame_support::{ use orml_traits::{asset_registry::AssetMetadata, FixedConversionRateProvider, MultiCurrency}; use pallet_foreign_investments::{ types::{InvestState, RedeemState}, - CollectedRedemption, InvestmentState, RedemptionState, + CollectedInvestment, CollectedRedemption, InvestmentPaymentCurrency, InvestmentState, + RedemptionPayoutCurrency, RedemptionState, }; use pallet_investments::CollectOutcome; use runtime_common::{ @@ -91,8 +92,7 @@ use crate::{ }; mod same_currencies { - - use pallet_foreign_investments::{CollectedInvestment, InvestmentState}; + use pallet_foreign_investments::errors::InvestError; use super::*; @@ -115,7 +115,7 @@ mod same_currencies { create_currency_pool(pool_id, currency_id, currency_decimals.into()); // Set permissions and execute initial investment - do_initial_increase_investment(pool_id, amount, investor.clone(), currency_id); + do_initial_increase_investment(pool_id, amount, investor.clone(), currency_id, false); // Verify the order was updated to the amount assert_eq!( @@ -165,7 +165,13 @@ mod same_currencies { create_currency_pool(pool_id, currency_id, currency_decimals.into()); // Set permissions and execute initial investment - do_initial_increase_investment(pool_id, invest_amount, investor.clone(), currency_id); + do_initial_increase_investment( + pool_id, + invest_amount, + investor.clone(), + currency_id, + false, + ); // Mock incoming decrease message let msg = LiquidityPoolMessage::DecreaseInvestOrder { @@ -179,9 +185,9 @@ mod same_currencies { // Expect failure if transferability is disabled since this is required for // preparing the `ExecutedDecreaseInvest` message. assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg.clone()), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable - ); + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg.clone()), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); enable_liquidity_pool_transferability(currency_id); // Execute byte message @@ -239,7 +245,13 @@ mod same_currencies { create_currency_pool(pool_id, currency_id, currency_decimals.into()); // Set permissions and execute initial investment - do_initial_increase_investment(pool_id, invest_amount, investor.clone(), currency_id); + do_initial_increase_investment( + pool_id, + invest_amount, + investor.clone(), + currency_id, + false, + ); // Verify investment account holds funds before cancelling assert_eq!( @@ -326,13 +338,14 @@ mod same_currencies { let currency_id = AUSD_CURRENCY_ID; let currency_decimals = currency_decimals::AUSD; let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); + enable_liquidity_pool_transferability(currency_id); // Create new pool create_currency_pool(pool_id, currency_id, currency_decimals.into()); let investment_currency_id: CurrencyId = default_investment_id().into(); // Set permissions and execute initial investment - do_initial_increase_investment(pool_id, amount, investor.clone(), currency_id); + do_initial_increase_investment(pool_id, amount, investor.clone(), currency_id, false); let events_before_collect = System::events(); // Process and fulfill order @@ -358,8 +371,6 @@ mod same_currencies { investor: investor.clone().into(), currency: general_currency_index(currency_id), }; - - // Execute byte message assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); // Remove events before collect execution @@ -419,15 +430,20 @@ mod same_currencies { .into() })); - // Foreign CollectedInvestment should be killed - assert!(!pallet_foreign_investments::CollectedInvestment::< - DevelopmentRuntime, - >::contains_key(investor.clone(), default_investment_id())); - - // Foreign InvestmentState should be killed - assert!(!pallet_foreign_investments::InvestmentState::< - DevelopmentRuntime, - >::contains_key(investor.clone(), default_investment_id())); + 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| { @@ -457,7 +473,13 @@ mod same_currencies { let currency_decimals = currency_decimals::AUSD; let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); create_currency_pool(pool_id, currency_id, currency_decimals.into()); - do_initial_increase_investment(pool_id, invest_amount, investor.clone(), currency_id); + do_initial_increase_investment( + pool_id, + invest_amount, + investor.clone(), + currency_id, + false, + ); enable_liquidity_pool_transferability(currency_id); let investment_currency_id: CurrencyId = default_investment_id().into(); @@ -499,34 +521,35 @@ mod same_currencies { default_investment_id() )); + assert_eq!( + InvestmentPaymentCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + currency_id + ); assert!(!Investments::investment_requires_collect( &investor, default_investment_id() )); - // The collected amount is only transferred to the user if they send a - // `CollectInvest` message - assert_eq!( - CollectedInvestment::::get(&investor, default_investment_id()), - CollectedAmount { - amount_collected: invest_amount / 2 * 4, - amount_payment: invest_amount / 2, - } - ); + // 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 investor's wallet (i.e. not be collected to - // domain) - assert_eq!( - Tokens::balance(investment_currency_id, &investor), - invest_amount * 2 - ); + // Tranche Tokens should still be transferred to collected to + // domain locator account already + assert_eq!(Tokens::balance(investment_currency_id, &investor), 0); assert_eq!( Tokens::balance(investment_currency_id, &sending_domain_locator), - 0 + invest_amount * 2 ); assert!(System::events().iter().any(|e| { e.event @@ -575,26 +598,37 @@ mod same_currencies { &investor, default_investment_id() )); - assert_eq!( - CollectedInvestment::::get(&investor, default_investment_id()), - CollectedAmount { - amount_collected: invest_amount * 3, - amount_payment: invest_amount, - } + 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 still be investor's wallet (i.e. not be collected to - // domain) + // Tranche Tokens should be transferred to collected to + // domain locator account already + let amount_tranche_tokens = invest_amount * 3; assert_eq!( - Tokens::balance(investment_currency_id, &investor), - invest_amount * 3 + Tokens::total_issuance(investment_currency_id), + amount_tranche_tokens ); + assert!(Tokens::balance(investment_currency_id, &investor).is_zero()); assert_eq!( Tokens::balance(investment_currency_id, &sending_domain_locator), - 0 + amount_tranche_tokens ); assert!(!System::events().iter().any(|e| { e.event @@ -634,26 +668,16 @@ mod same_currencies { 1 ); - // User collects through foreign investments + // Should fail to collect if `InvestmentState` does not exist let msg = LiquidityPoolMessage::CollectInvest { pool_id, tranche_id: default_tranche_id(pool_id), investor: investor.clone().into(), currency: general_currency_index(currency_id), }; - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - assert!(!CollectedInvestment::::contains_key( - &investor, - default_investment_id() - )); - assert_eq!( - Tokens::total_issuance(investment_currency_id), - invest_amount * 3 - ); - assert!(Tokens::balance(investment_currency_id, &investor).is_zero()); - assert_eq!( - Tokens::balance(investment_currency_id, &sending_domain_locator), - invest_amount * 3 + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg), + pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound ); }); } @@ -867,6 +891,12 @@ mod same_currencies { Tokens::balance(default_investment_id().into(), &sending_domain_locator), redeem_amount ); + assert!( + !RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); // Foreign RedemptionState should be updated assert!(System::events().iter().any(|e| { @@ -1244,6 +1274,7 @@ mod same_currencies { invest_amount, investor.clone(), currency_id, + false, ); enable_liquidity_pool_transferability(currency_id); @@ -1322,7 +1353,13 @@ mod same_currencies { let currency_id: CurrencyId = AUSD_CURRENCY_ID; let currency_decimals = currency_decimals::AUSD; create_currency_pool(pool_id, currency_id, currency_decimals.into()); - do_initial_increase_investment(pool_id, amount, investor.clone(), currency_id); + do_initial_increase_investment( + pool_id, + amount, + investor.clone(), + currency_id, + false, + ); enable_liquidity_pool_transferability(currency_id); // Prepare collection @@ -1439,121 +1476,453 @@ mod same_currencies { }); } } - } -} - -mod mismatching_currencies { - use cfg_traits::investments::ForeignInvestment; - use cfg_types::investments::{ForeignInvestmentInfo, Swap}; - use development_runtime::{DefaultTokenSellRate, OrderBook}; - use pallet_foreign_investments::{types::TokenSwapReason, InvestmentState}; - use super::*; - use crate::{ - liquidity_pools::pallet::development::{setup::CHARLIE, tests::register_usdt}, - utils::{GLMR_CURRENCY_ID, USDT_CURRENCY_ID}, - }; + mod payment_payout_currency { + use super::*; + use crate::{ + liquidity_pools::pallet::development::tests::{ + liquidity_pools::foreign_investments::setup::enable_usdt_trading, + }, + utils::USDT_CURRENCY_ID, + }; - /// Invest in pool currency, then increase in allowed foreign currency, then - /// decrease in same foreign currency multiple times. - #[test] - fn invest_increase_decrease() { - TestNet::reset(); - Development::execute_with(|| { - setup_pre_requirements(); - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - BOB, - )); - 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 = 6_000_000_000_000_000; - create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); - do_initial_increase_investment( - pool_id, - invest_amount_pool_denominated, - investor.clone(), - pool_currency, - ); + #[test] + fn invalid_invest_payment_currency() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = AccountConverter::< + DevelopmentRuntime, + LocationToAccountId, + >::convert((DOMAIN_MOONBEAM, BOB)); + let pool_currency = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount = 6 * dollar(18); - // USDT investment preparations - register_usdt(); - let invest_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - invest_amount_pool_denominated, - ) - .unwrap(); + create_currency_pool(pool_id, pool_currency, currency_decimals.into()); + do_initial_increase_investment( + pool_id, + amount, + investor.clone(), + pool_currency, + false, + ); + enable_usdt_trading(pool_currency, amount, true, true, true, || {}); - // Should fail to increase to an invalid payment currency - assert!(!ForeignInvestments::accepted_payment_currency( - default_investment_id(), - foreign_currency - )); - 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, - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg.clone()), - pallet_liquidity_pools::Error::::InvalidPaymentCurrency - ); + // 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), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: 1, + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg), + pallet_foreign_investments::Error::::InvestError( + InvestError::InvalidPaymentCurrency + ) + ); + let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: 1, + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), + pallet_foreign_investments::Error::::InvestError( + InvestError::InvalidPaymentCurrency + ) + ); + let collect_msg = LiquidityPoolMessage::CollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), + pallet_foreign_investments::Error::::InvestError( + InvestError::InvalidPaymentCurrency + ) + ); + }); + } - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert!(ForeignInvestments::accepted_payment_currency( - default_investment_id(), - foreign_currency - )); - assert!(!ForeignInvestments::accepted_payout_currency( - default_investment_id(), - foreign_currency - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); - assert!(ForeignInvestments::accepted_payout_currency( - default_investment_id(), - foreign_currency - )); + #[test] + fn invalid_redeem_payout_currency() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = AccountConverter::< + DevelopmentRuntime, + LocationToAccountId, + >::convert((DOMAIN_MOONBEAM, BOB)); + let pool_currency = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount = 6 * dollar(18); - // 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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - increase_msg - )); - assert!(System::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() - })); + 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!(Tokens::mint_into( + default_investment_id().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + amount, + )); - // Should be able to to decrease in the swapping foreign currency + // 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), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: 1, + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::InvalidPayoutCurrency + ) + ); + 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!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::InvalidPayoutCurrency + ) + ); + 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!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::InvalidPayoutCurrency + ) + ); + }); + } + + #[test] + fn invest_payment_currency_not_found() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = AccountConverter::< + DevelopmentRuntime, + LocationToAccountId, + >::convert((DOMAIN_MOONBEAM, BOB)); + let pool_currency = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount = 6 * dollar(18); + + create_currency_pool(pool_id, pool_currency, currency_decimals.into()); + do_initial_increase_investment( + pool_id, + amount, + investor.clone(), + pool_currency, + true, + ); + enable_usdt_trading(pool_currency, amount, true, true, true, || {}); + + // Should fail to decrease or collect for another foreign currency as + // long as `InvestmentState` exists + let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: 1, + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), + pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + ); + let collect_msg = LiquidityPoolMessage::CollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + }; + assert_noop!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), + pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + ); + }); + } + + #[test] + fn redeem_payout_currency_not_found() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = AccountConverter::< + DevelopmentRuntime, + LocationToAccountId, + >::convert((DOMAIN_MOONBEAM, BOB)); + let pool_currency = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount = 6 * dollar(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!(Tokens::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!( + LiquidityPools::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!( + LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), + pallet_foreign_investments::Error::::RedemptionPayoutCurrencyNotFound + ); + }); + } + } + } +} + +mod mismatching_currencies { + use cfg_traits::investments::ForeignInvestment; + use cfg_types::investments::{ForeignInvestmentInfo, Swap}; + use development_runtime::{DefaultTokenSellRate, OrderBook}; + use pallet_foreign_investments::{types::TokenSwapReason, InvestmentState}; + + use super::*; + use crate::{ + liquidity_pools::pallet::development::{ + setup::CHARLIE, + tests::{ + liquidity_pools::foreign_investments::setup::enable_usdt_trading, register_usdt, + }, + }, + utils::{GLMR_CURRENCY_ID, USDT_CURRENCY_ID}, + }; + + #[test] + fn collect_foreign_investment_for() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + BOB, + )); + 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 = 6 * dollar(18); + let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); + 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, + // not needed because we don't initialize a swap from pool to foreign here + false, + || {}, + ); + + do_initial_increase_investment( + 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. + let msg = LiquidityPoolMessage::IncreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: 1, + }; + assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); + + // Process 100% of investment at 50% rate (1 pool currency = 2 tranche tokens) + assert_ok!(Investments::process_invest_orders(default_investment_id())); + assert_ok!(Investments::invest_fulfillment( + default_investment_id(), + FulfillmentWithPrice { + of_amount: Perquintill::one(), + price: Quantity::checked_from_rational(1, 2).unwrap(), + } + )); + assert_ok!(Investments::collect_investments_for( + RuntimeOrigin::signed(ALICE.into()), + investor.clone(), + default_investment_id() + )); + assert_eq!( + InvestmentPaymentCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + foreign_currency + ); + assert!(Tokens::balance(default_investment_id().into(), &investor).is_zero()); + assert_eq!( + Tokens::balance(default_investment_id().into(), &sending_domain_locator), + invest_amount_pool_denominated * 2 + ); + + // Should not be cleared as invest state is swapping into pool currency + assert_eq!( + InvestmentPaymentCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + foreign_currency + ); + }); + } + + /// Invest in pool currency, then increase in allowed foreign currency, then + /// decrease in same foreign currency multiple times. + #[test] + fn invest_increase_decrease() { + TestNet::reset(); + Development::execute_with(|| { + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + BOB, + )); + 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 = 6 * dollar(18); + create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); + do_initial_increase_investment( + pool_id, + invest_amount_pool_denominated, + investor.clone(), + pool_currency, + true, + ); + + // USDT investment preparations + let invest_amount_foreign_denominated = enable_usdt_trading( + pool_currency, + invest_amount_pool_denominated, + 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!( + LiquidityPools::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!(LiquidityPools::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + increase_msg + )); + assert!(System::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 to decrease in the swapping foreign currency enable_liquidity_pool_transferability(foreign_currency); let decrease_msg_pool_swap_amount = LiquidityPoolMessage::DecreaseInvestOrder { pool_id, @@ -1667,37 +2036,21 @@ mod mismatching_currencies { 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_000_000_000_000_000; + let invest_amount_pool_denominated: u128 = 10 * dollar(18); create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); - - // USDT investment preparations - register_usdt(); - // Overwrite multilocation to enable LP transferability - enable_liquidity_pool_transferability(foreign_currency); + let invest_amount_foreign_denominated: u128 = enable_usdt_trading( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + || {}, + ); assert_ok!(Tokens::mint_into( pool_currency, &trader, invest_amount_pool_denominated )); - let invest_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - invest_amount_pool_denominated, - ) - .unwrap(); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); // Increase such that active swap into USDT is initialized do_initial_increase_investment( @@ -1705,6 +2058,7 @@ mod mismatching_currencies { invest_amount_foreign_denominated, investor.clone(), foreign_currency, + false, ); let swap_order_id = ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) @@ -1841,44 +2195,36 @@ mod mismatching_currencies { 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_000_000_000_000_000; + let invest_amount_pool_denominated: u128 = 10 * dollar(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, ); - - // USDT setup - register_usdt(); - enable_liquidity_pool_transferability(foreign_currency); - let invest_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - invest_amount_pool_denominated, - ) - .unwrap(); assert_ok!(Tokens::mint_into( foreign_currency, &trader, invest_amount_foreign_denominated * 2 )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); // Decrease invest setup to have invest order swapping into foreign currency let msg = LiquidityPoolMessage::DecreaseInvestOrder { @@ -1967,6 +2313,14 @@ mod mismatching_currencies { } } ); + assert_eq!( + RedemptionPayoutCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + foreign_currency + ); let swap_amount = invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; assert!(System::events().iter().any(|e| { @@ -2057,6 +2411,12 @@ mod mismatching_currencies { &investor, default_investment_id() )); + assert!( + !RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); assert!( ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) @@ -2084,37 +2444,22 @@ mod mismatching_currencies { 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_000_000_000_000_000; + let invest_amount_pool_denominated: u128 = 10 * dollar(18); let swap_order_id = 1; create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); - - // USDT setup - register_usdt(); - enable_liquidity_pool_transferability(foreign_currency); - let invest_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - invest_amount_pool_denominated, - ) - .unwrap(); + let invest_amount_foreign_denominated: u128 = enable_usdt_trading( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + || {}, + ); assert_ok!(Tokens::mint_into( foreign_currency, &trader, invest_amount_foreign_denominated * 2 )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); // Increase invest setup to have invest order swapping into pool currency do_initial_increase_investment( @@ -2122,6 +2467,7 @@ mod mismatching_currencies { invest_amount_foreign_denominated, investor.clone(), foreign_currency, + false, ); assert_eq!( InvestmentState::::get(&investor, default_investment_id()), @@ -2394,44 +2740,30 @@ mod mismatching_currencies { 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_000_000_000_000_000; + let invest_amount_pool_denominated: u128 = 10 * dollar(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, ); - - // USDT setup - register_usdt(); - enable_liquidity_pool_transferability(foreign_currency); - let invest_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - invest_amount_pool_denominated, - ) - .unwrap(); assert_ok!(Tokens::mint_into( pool_currency, &trader, invest_amount_pool_denominated )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); // Increase invest have // InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing @@ -2512,45 +2844,29 @@ mod mismatching_currencies { let pool_currency: CurrencyId = AUSD_CURRENCY_ID; let foreign_currency: CurrencyId = USDT_CURRENCY_ID; let pool_currency_decimals = currency_decimals::AUSD; - let redeem_amount_pool_denominated: u128 = 10_000_000_000_000_000; + let redeem_amount_pool_denominated: u128 = 10 * dollar(18); let swap_order_id = 1; create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); + let redeem_amount_foreign_denominated: u128 = enable_usdt_trading( + pool_currency, + redeem_amount_pool_denominated, + true, + true, + true, + || {}, + ); assert_ok!(Tokens::mint_into( pool_currency, &pool_account, redeem_amount_pool_denominated )); - - // USDT setup - register_usdt(); - enable_liquidity_pool_transferability(foreign_currency); - let redeem_amount_foreign_denominated: u128 = - IdentityPoolCurrencyConverter::::stable_to_stable( - foreign_currency, - pool_currency, - redeem_amount_pool_denominated, - ) - .unwrap(); assert_ok!(Tokens::mint_into( foreign_currency, &trader, redeem_amount_foreign_denominated )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - pool_currency, - foreign_currency, - 1 - )); - assert_ok!(OrderBook::add_trading_pair( - RuntimeOrigin::root(), - foreign_currency, - pool_currency, - 1 - )); - do_initial_increase_redemption( pool_id, redeem_amount_pool_denominated, @@ -2632,8 +2948,16 @@ mod mismatching_currencies { } mod setup { + use cfg_traits::investments::ForeignInvestment; + use development_runtime::OrderBook; + use super::*; - use crate::liquidity_pools::pallet::development::tests::liquidity_pools::setup::DEFAULT_OTHER_DOMAIN_ADDRESS; + use crate::{ + liquidity_pools::pallet::development::tests::{ + liquidity_pools::setup::DEFAULT_OTHER_DOMAIN_ADDRESS, register_usdt, + }, + utils::USDT_CURRENCY_ID, + }; /// Sets up required permissions for the investor and executes an /// initial investment via LiquidityPools by executing @@ -2647,6 +2971,7 @@ mod setup { amount: Balance, investor: AccountId, currency_id: CurrencyId, + clear_investment_payment_currency: bool, ) { let valid_until = DEFAULT_VALIDITY; let pool_currency: CurrencyId = @@ -2690,6 +3015,14 @@ mod setup { // Execute byte message assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); + assert_eq!( + InvestmentPaymentCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + currency_id, + ); if currency_id == pool_currency { assert_eq!( @@ -2742,6 +3075,17 @@ mod setup { } ); } + + // 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(), + ); + } } /// Sets up required permissions for the investor and executes an @@ -2817,6 +3161,11 @@ mod setup { redeem_amount: amount } ); + assert_eq!( + RedemptionPayoutCurrency::::get(&investor, default_investment_id()) + .unwrap(), + currency_id + ); // Verify redemption was transferred into investment account assert_eq!( Tokens::balance( @@ -2869,4 +3218,67 @@ mod setup { 0 ); } + + /// Registers USDT currency, adds bidirectional trading pairs and returns + /// the amount in foreign denomination + pub(crate) fn enable_usdt_trading( + pool_currency: CurrencyId, + amount_pool_denominated: Balance, + enable_lp_transferability: bool, + enable_foreign_to_pool_pair: bool, + enable_pool_to_foreign_pair: bool, + pre_add_trading_pair_check: impl FnOnce() -> (), + ) -> Balance { + register_usdt(); + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount_foreign_denominated: u128 = + IdentityPoolCurrencyConverter::::stable_to_stable( + foreign_currency, + pool_currency, + amount_pool_denominated, + ) + .unwrap(); + + if enable_lp_transferability { + enable_liquidity_pool_transferability(foreign_currency); + } + + pre_add_trading_pair_check(); + + if enable_foreign_to_pool_pair { + assert!(!ForeignInvestments::accepted_payment_currency( + default_investment_id(), + foreign_currency + )); + assert_ok!(OrderBook::add_trading_pair( + RuntimeOrigin::root(), + pool_currency, + foreign_currency, + 1 + )); + assert!(ForeignInvestments::accepted_payment_currency( + default_investment_id(), + foreign_currency + )); + } + if enable_pool_to_foreign_pair { + assert!(!ForeignInvestments::accepted_payout_currency( + default_investment_id(), + foreign_currency + )); + + assert_ok!(OrderBook::add_trading_pair( + RuntimeOrigin::root(), + foreign_currency, + pool_currency, + 1 + )); + assert!(ForeignInvestments::accepted_payout_currency( + default_investment_id(), + foreign_currency + )); + } + + amount_foreign_denominated + } }