diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index b6832c6364..382b2ff9de 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -1,22 +1,59 @@ -use cfg_primitives::{currency_decimals, parachains, AccountId, Balance}; +use cfg_primitives::{currency_decimals, parachains, AccountId, Balance, PoolId, TrancheId}; +use cfg_traits::{ + investments::{ForeignInvestment, Investment, OrderManager, TrancheCurrency}, + liquidity_pools::InboundQueue, + ConversionToAssetBalance, IdentityCurrencyConversion, Permissions, PoolInspect, PoolMutate, + Seconds, +}; use cfg_types::{ + domain_address::{Domain, DomainAddress}, + fixed_point::{Quantity, Ratio}, + investments::{ + ForeignInvestmentInfo, InvestCollection, InvestmentAccount, RedeemCollection, Swap, + }, + orders::FulfillmentWithPrice, + permissions::{PermissionScope, PoolRole, Role}, + pools::TrancheMetadata, tokens::{CrossChainTransferability, CurrencyId, CustomMetadata}, xcm::XcmMetadata, }; use cfg_utils::vec_to_fixed_array; use codec::Encode; -use frame_support::{assert_noop, assert_ok, dispatch::RawOrigin, traits::OriginTrait}; +use frame_support::{ + assert_noop, assert_ok, + dispatch::{RawOrigin, Weight}, + traits::{ + fungibles::{Inspect, Mutate}, + OriginTrait, PalletInfo, + }, +}; +use liquidity_pools_gateway_routers::{ + DomainRouter, EthereumXCMRouter, XCMRouter, XcmDomain, DEFAULT_PROOF_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_pool_system::tranches::{TrancheInput, TrancheLoc, TrancheType}; use polkadot_parachain::primitives::Id; use runtime_common::{ + account_conversion::AccountConverter, + foreign_investments::IdentityPoolCurrencyConverter, xcm::general_key, xcm_fees::{default_per_second, ksm_per_second}, }; +use sp_core::{Get, H160}; use sp_runtime::{ - traits::{AccountIdConversion, BadOrigin, ConstU32, Convert as C2}, - WeakBoundedVec, + traits::{AccountIdConversion, BadOrigin, ConstU32, Convert as C2, EnsureAdd, One, Zero}, + BoundedVec, DispatchError, FixedPointNumber, Perquintill, SaturatedConversion, WeakBoundedVec, }; use xcm::{ + latest::NetworkId, prelude::XCM_VERSION, v3::{ AssetId, Fungibility, Junction, Junction::*, Junctions, Junctions::*, MultiAsset, @@ -33,7 +70,7 @@ use crate::{ envs::fudge_env::{handle::FudgeHandle, FudgeEnv, FudgeSupport}, utils::{genesis, genesis::Genesis}, }, - utils::{accounts::Keyring, AUSD_CURRENCY_ID}, + utils::{accounts::Keyring, AUSD_CURRENCY_ID, AUSD_ED, USDT_CURRENCY_ID, USDT_ED}, }; mod utils { @@ -104,49 +141,5930 @@ mod utils { )); }); - env.pass(Blocks::ByNumber(1)); - } + env.pass(Blocks::ByNumber(1)); + } + + pub fn setup_usdc_xcm(env: &mut FudgeEnv) { + env.parachain_state_mut(|| { + // Set the XCM version used when sending XCM messages to USDC parachain. + assert_ok!(pallet_xcm::Pallet::::force_xcm_version( + ::RuntimeOrigin::root(), + Box::new(MultiLocation::new( + 1, + Junctions::X1(Junction::Parachain(1000)), + )), + XCM_VERSION, + )); + }); + + env.relay_state_mut(|| { + assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< + FudgeRelayRuntime, + >::force_open_hrmp_channel( + as frame_system::Config>::RuntimeOrigin::root(), + Id::from(T::FudgeHandle::PARA_ID), + Id::from(1000), + 10, + 1024, + )); + + assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< + FudgeRelayRuntime, + >::force_process_hrmp_open( + as frame_system::Config>::RuntimeOrigin::root(), + 0, + )); + }); + + env.pass(Blocks::ByNumber(1)); + } + + pub fn register_ausd() { + let meta: AssetMetadata = AssetMetadata { + decimals: 12, + name: "Acala Dollar".into(), + symbol: "AUSD".into(), + existential_deposit: 1_000_000_000, + location: Some(VersionedMultiLocation::V3(MultiLocation::new( + 1, + X2( + Parachain(T::FudgeHandle::SIBLING_ID), + general_key(parachains::kusama::karura::AUSD_KEY), + ), + ))), + additional: CustomMetadata { + transferability: CrossChainTransferability::Xcm(Default::default()), + pool_currency: true, + ..CustomMetadata::default() + }, + }; + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + meta, + Some(AUSD_CURRENCY_ID) + )); + } + + pub fn ausd(amount: Balance) -> Balance { + amount * dollar(currency_decimals::AUSD) + } + + pub fn ausd_fee() -> Balance { + fee(currency_decimals::AUSD) + } + + pub fn cfg(amount: Balance) -> Balance { + amount * dollar(currency_decimals::NATIVE) + } + + pub fn dollar(decimals: u32) -> Balance { + 10u128.saturating_pow(decimals) + } + + pub fn fee(decimals: u32) -> Balance { + calc_fee(default_per_second(decimals)) + } + + pub fn calc_fee(fee_per_second: Balance) -> Balance { + // We divide the fee to align its unit and multiply by 4 as that seems to be the + // unit of time the tests take. + // NOTE: it is possible that in different machines this value may differ. We + // shall see. + fee_per_second.div_euclid(10_000) * 8 + } +} + +type FudgeRelayRuntime = <::FudgeHandle as FudgeHandle>::RelayRuntime; + +use utils::*; + +mod development { + use development_runtime::{LocationToAccountId, MinFulfillmentAmountNative, TreasuryAccount}; + + use super::*; + + pub const GLMR_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(4); + pub const GLMR_ED: Balance = 1_000_000; + pub const DEFAULT_BALANCE_GLMR: Balance = 10_000_000_000_000_000_000; + + pub const DEFAULT_POOL_ID: PoolId = 42; + pub const MOONBEAM_EVM_CHAIN_ID: u64 = 1284; + pub const DEFAULT_EVM_ADDRESS_MOONBEAM: [u8; 20] = [99; 20]; + pub const DEFAULT_VALIDITY: Seconds = 2555583502; + pub const DOMAIN_MOONBEAM: Domain = Domain::EVM(MOONBEAM_EVM_CHAIN_ID); + pub const DEFAULT_DOMAIN_ADDRESS_MOONBEAM: DomainAddress = + DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, DEFAULT_EVM_ADDRESS_MOONBEAM); + pub const DEFAULT_OTHER_DOMAIN_ADDRESS: DomainAddress = + DomainAddress::EVM(crate::utils::MOONBEAM_EVM_CHAIN_ID, [0; 20]); + + pub type LiquidityPoolMessage = Message; + + mod utils { + use super::*; + + /// Creates a new pool for the given id with + /// * BOB as admin and depositor + /// * Two tranches + /// * AUSD as pool currency with max reserve 10k. + pub fn create_ausd_pool(pool_id: u64) { + create_currency_pool::(pool_id, AUSD_CURRENCY_ID, dollar(currency_decimals::AUSD)) + } + + /// Creates a new pool for for the given id with the provided currency. + /// * BOB as admin and depositor + /// * Two tranches + /// * The given `currency` as pool currency with of + /// `currency_decimals`. + pub fn create_currency_pool( + pool_id: u64, + currency_id: CurrencyId, + currency_decimals: Balance, + ) { + assert_ok!(pallet_pool_system::Pallet::::create( + Keyring::Bob.into(), + Keyring::Bob.into(), + pool_id, + vec![ + TrancheInput { + tranche_type: TrancheType::Residual, + seniority: None, + metadata: TrancheMetadata { + // NOTE: For now, we have to set these metadata fields of the first + // tranche to be convertible to the 32-byte size expected by the + // liquidity pools AddTranche message. + token_name: BoundedVec::< + u8, + ::MaxTokenNameLength, + >::try_from("A highly advanced tranche".as_bytes().to_vec()) + .expect(""), + token_symbol: BoundedVec::< + u8, + ::MaxTokenSymbolLength, + >::try_from("TrNcH".as_bytes().to_vec()) + .expect(""), + } + }, + TrancheInput { + tranche_type: TrancheType::NonResidual { + interest_rate_per_sec: One::one(), + min_risk_buffer: Perquintill::from_percent(10), + }, + seniority: None, + metadata: TrancheMetadata { + token_name: BoundedVec::default(), + token_symbol: BoundedVec::default(), + } + } + ], + currency_id, + currency_decimals, + )); + } + + pub fn register_glmr() { + let meta: AssetMetadata = AssetMetadata { + decimals: 18, + name: "Glimmer".into(), + symbol: "GLMR".into(), + existential_deposit: GLMR_ED, + location: Some(VersionedMultiLocation::V3(MultiLocation::new( + 1, + X2(Parachain(T::FudgeHandle::SIBLING_ID), general_key(&[0, 1])), + ))), + additional: CustomMetadata { + transferability: CrossChainTransferability::Xcm(Default::default()), + ..CustomMetadata::default() + }, + }; + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + meta, + Some(GLMR_CURRENCY_ID) + )); + } + + pub fn set_test_domain_router( + evm_chain_id: u64, + xcm_domain_location: VersionedMultiLocation, + currency_id: CurrencyId, + ) { + let ethereum_xcm_router = EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: XcmDomain { + location: Box::new(xcm_domain_location), + ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), + contract_address: H160::from(DEFAULT_EVM_ADDRESS_MOONBEAM), + max_gas_limit: 500_000, + transact_required_weight_at_most: Weight::from_parts( + 12530000000, + DEFAULT_PROOF_SIZE.saturating_div(2), + ), + overall_weight: Weight::from_parts(15530000000, DEFAULT_PROOF_SIZE), + fee_currency: currency_id, + // 0.2 token + fee_amount: 200000000000000000, + }, + _marker: Default::default(), + }, + _marker: Default::default(), + }; + + let domain_router = DomainRouter::EthereumXCM(ethereum_xcm_router); + let domain = Domain::EVM(evm_chain_id); + + assert_ok!( + pallet_liquidity_pools_gateway::Pallet::::set_domain_router( + ::RuntimeOrigin::root(), + domain, + domain_router, + ) + ); + } + + pub fn default_tranche_id(pool_id: u64) -> TrancheId { + let pool_details = + pallet_pool_system::pallet::Pool::::get(pool_id).expect("Pool should exist"); + pool_details + .tranches + .tranche_id(TrancheLoc::Index(0)) + .expect("Tranche at index 0 exists") + } + + /// Returns a `VersionedMultiLocation` that can be converted into + /// `LiquidityPoolsWrappedToken` which is required for cross chain asset + /// registration and transfer. + pub fn liquidity_pools_transferable_multilocation( + chain_id: u64, + address: [u8; 20], + ) -> VersionedMultiLocation { + VersionedMultiLocation::V3(MultiLocation { + parents: 0, + interior: X3( + PalletInstance( + ::PalletInfo::index::< + pallet_liquidity_pools::Pallet, + >() + .expect("LiquidityPools should have pallet index") + .saturated_into(), + ), + GlobalConsensus(NetworkId::Ethereum { chain_id }), + AccountKey20 { + network: None, + key: address, + }, + ), + }) + } + + /// Enables `LiquidityPoolsTransferable` in the custom asset metadata + /// for the given currency_id. + /// + /// NOTE: Sets the location to the `MOONBEAM_EVM_CHAIN_ID` with dummy + /// address as the location is required for LiquidityPoolsWrappedToken + /// conversions. + pub fn enable_liquidity_pool_transferability( + currency_id: CurrencyId, + ) { + let metadata = orml_asset_registry::Metadata::::get(currency_id) + .expect("Currency should be registered"); + let location = Some(Some(liquidity_pools_transferable_multilocation::( + MOONBEAM_EVM_CHAIN_ID, + // Value of evm_address is irrelevant here + [1u8; 20], + ))); + + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + location, + Some(CustomMetadata { + // Changed: Allow liquidity_pools transferability + transferability: CrossChainTransferability::LiquidityPools, + ..metadata.additional + }) + )); + } + + pub fn setup_test(env: &mut FudgeEnv) { + setup_xcm(env); + + env.parachain_state_mut(|| { + register_ausd::(); + register_glmr::(); + + assert_ok!(orml_tokens::Pallet::::set_balance( + ::RuntimeOrigin::root(), + ::Sender::get().into(), + GLMR_CURRENCY_ID, + DEFAULT_BALANCE_GLMR, + 0, + )); + + set_test_domain_router::( + MOONBEAM_EVM_CHAIN_ID, + MultiLocation::new( + 1, + Junctions::X1(Junction::Parachain(T::FudgeHandle::SIBLING_ID)), + ) + .into(), + GLMR_CURRENCY_ID, + ); + }); + } + + /// Returns the derived general currency index. + /// + /// Throws if the provided currency_id is not + /// `CurrencyId::ForeignAsset(id)`. + pub fn general_currency_index(currency_id: CurrencyId) -> u128 { + pallet_liquidity_pools::Pallet::::try_get_general_index(currency_id) + .expect("ForeignAsset should convert into u128") + } + + /// Returns the investment_id of the given pool and tranche ids. + pub fn investment_id( + pool_id: u64, + tranche_id: TrancheId, + ) -> cfg_types::tokens::TrancheCurrency { + ::TrancheCurrency::generate(pool_id, tranche_id) + } + + pub fn default_investment_id( + ) -> cfg_types::tokens::TrancheCurrency { + ::TrancheCurrency::generate( + DEFAULT_POOL_ID, + default_tranche_id::(DEFAULT_POOL_ID), + ) + } + + /// Returns the default investment account derived from the + /// `DEFAULT_POOL_ID` and its default tranche. + pub fn default_investment_account() -> AccountId { + InvestmentAccount { + investment_id: default_investment_id::(), + } + .into_account_truncating() + } + + /// Sets up required permissions for the investor and executes an + /// initial investment via LiquidityPools by executing + /// `IncreaseInvestOrder`. + /// + /// Assumes `setup_pre_requirements` and + /// `investments::create_currency_pool` to have been called + /// beforehand + pub fn do_initial_increase_investment( + pool_id: u64, + 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"); + + // Mock incoming increase invest message + let msg = LiquidityPoolMessage::IncreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount, + }; + + // Should fail if investor does not have investor role yet + // However, failure is async for foreign currencies as part of updating the + // investment after the swap was fulfilled + if currency_id == pool_currency { + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + DispatchError::Other("Account does not have the TrancheInvestor permission.") + ); + } + + // Make investor the MembersListAdmin of this Pool + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + investor.clone(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + valid_until + )), + )); + + let amount_before = + orml_tokens::Pallet::::balance(currency_id, &default_investment_account::()); + let final_amount = amount_before + .ensure_add(amount) + .expect("Should not overflow when incrementing amount"); + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + 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( + currency_id, + &default_investment_account::() + ), + 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 { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: final_amount, + } + .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::()); + } + } + + /// Sets up required permissions for the investor and executes an + /// initial redemption via LiquidityPools by executing + /// `IncreaseRedeemOrder`. + /// + /// Assumes `setup_pre_requirements` and + /// `investments::create_currency_pool` to have been called + /// beforehand. + /// + /// NOTE: Mints exactly the redeeming amount of tranche tokens. + pub fn do_initial_increase_redemption( + pool_id: u64, + amount: Balance, + 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( + default_investment_id::().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + amount + )); + + // Verify redemption has not been made yet + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &default_investment_account::(), + ), + 0 + ); + assert_eq!( + orml_tokens::Pallet::::balance(default_investment_id::().into(), &investor), + 0 + ); + + // Mock incoming increase invest message + let msg = LiquidityPoolMessage::IncreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount, + }; + + // Should fail if investor does not have investor role yet + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + DispatchError::Other("Account does not have the TrancheInvestor permission.") + ); + + // Make investor the MembersListAdmin of this Pool + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + investor.clone(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + valid_until + )), + )); + + 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( + default_investment_id::().into(), + &default_investment_account::(), + ), + amount + ); + assert_eq!( + orml_tokens::Pallet::::balance(default_investment_id::().into(), &investor), + 0 + ); + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &AccountConverter::::convert( + DEFAULT_OTHER_DOMAIN_ADDRESS + ) + ), + 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() + .last() + .unwrap() + .event, + pallet_investments::Event::::RedeemOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor, + amount + } + .into() + ); + + // Verify order id is 0 + assert_eq!( + pallet_investments::Pallet::::redeem_order_id(investment_id::( + pool_id, + default_tranche_id::(pool_id) + )), + 0 + ); + } + + /// Register USDT in the asset registry and enable LiquidityPools cross + /// chain transferability. + /// + /// NOTE: Assumes to be executed within an externalities environment. + fn register_usdt() { + let meta: AssetMetadata = AssetMetadata { + decimals: 6, + name: "Tether USDT".into(), + symbol: "USDT".into(), + existential_deposit: USDT_ED, + location: Some(VersionedMultiLocation::V3(MultiLocation::new( + 1, + X3(Parachain(1000), PalletInstance(50), GeneralIndex(1984)), + ))), + additional: CustomMetadata { + transferability: CrossChainTransferability::LiquidityPools, + pool_currency: true, + ..CustomMetadata::default() + }, + }; + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + meta, + Some(USDT_CURRENCY_ID) + )); + } + + /// Registers USDT currency, adds bidirectional trading pairs and + /// returns the amount in foreign denomination + pub 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!( + !pallet_foreign_investments::Pallet::::accepted_payment_currency( + default_investment_id::(), + foreign_currency + ) + ); + assert_ok!(pallet_order_book::Pallet::::add_trading_pair( + ::RuntimeOrigin::root(), + pool_currency, + foreign_currency, + 1 + )); + assert!( + pallet_foreign_investments::Pallet::::accepted_payment_currency( + default_investment_id::(), + foreign_currency + ) + ); + } + if enable_pool_to_foreign_pair { + assert!( + !pallet_foreign_investments::Pallet::::accepted_payout_currency( + default_investment_id::(), + foreign_currency + ) + ); + + assert_ok!(pallet_order_book::Pallet::::add_trading_pair( + ::RuntimeOrigin::root(), + foreign_currency, + pool_currency, + 1 + )); + assert!( + pallet_foreign_investments::Pallet::::accepted_payout_currency( + default_investment_id::(), + foreign_currency + ) + ); + } + + amount_foreign_denominated + } + + pub fn ensure_executed_collect_redeem_not_dispatched() { + assert!(frame_system::Pallet::::events().into_iter().any(|e| { + match &e.event.try_into() { + Ok(r) => match r { + pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + message, + .. + } => match message { + LiquidityPoolMessage::ExecutedCollectRedeem { .. } => false, + _ => true, + }, + _ => true, + }, + Err(_) => true, + } + })); + } + + pub fn min_fulfillment_amount( + currency_id: CurrencyId, + ) -> Balance { + runtime_common::foreign_investments::NativeBalanceDecimalConverter::< + orml_asset_registry::Pallet, + >::to_asset_balance(MinFulfillmentAmountNative::get(), currency_id) + .expect("CurrencyId should be registered in AssetRegistry") + } + } + + use utils::*; + + mod add_allow_upgrade { + use super::*; + + fn add_pool() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + + // Verify that the pool must exist before we can call + // pallet_liquidity_pools::Pallet::::add_pool + assert_noop!( + pallet_liquidity_pools::Pallet::::add_pool( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::PoolNotFound + ); + + // Now create the pool + create_ausd_pool::(pool_id); + + // Verify ALICE can't call `add_pool` given she is not the `PoolAdmin` + assert_noop!( + pallet_liquidity_pools::Pallet::::add_pool( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::NotPoolAdmin + ); + + // Verify that it works if it's BOB calling it (the pool admin) + assert_ok!(pallet_liquidity_pools::Pallet::::add_pool( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + )); + }); + } + + fn add_tranche() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + // Now create the pool + let pool_id = DEFAULT_POOL_ID; + create_ausd_pool::(pool_id); + + // Verify we can't call pallet_liquidity_pools::Pallet::::add_tranche with a + // non-existing tranche_id + let nonexistent_tranche = [71u8; 16]; + + assert_noop!( + pallet_liquidity_pools::Pallet::::add_tranche( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + nonexistent_tranche, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::TrancheNotFound + ); + let tranche_id = default_tranche_id::(pool_id); + + // Verify ALICE can't call `add_tranche` given she is not the `PoolAdmin` + assert_noop!( + pallet_liquidity_pools::Pallet::::add_tranche( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::NotPoolAdmin + ); + + // Finally, verify we can call pallet_liquidity_pools::Pallet::::add_tranche + // successfully when called by the PoolAdmin with the right pool + tranche id + // pair. + assert_ok!(pallet_liquidity_pools::Pallet::::add_tranche( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + )); + + // Edge case: Should throw if tranche exists but metadata does not exist + let tranche_currency_id = CurrencyId::Tranche(pool_id, tranche_id); + + orml_asset_registry::Metadata::::remove(tranche_currency_id); + + assert_noop!( + pallet_liquidity_pools::Pallet::::update_tranche_token_metadata( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::TrancheMetadataNotFound + ); + }); + } + + fn update_member() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + // Now create the pool + let pool_id = DEFAULT_POOL_ID; + + create_ausd_pool::(pool_id); + + let tranche_id = default_tranche_id::(pool_id); + + // 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( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + Keyring::Alice.into(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::InvestorAdmin), + )); + + // Verify it fails if the destination is not whitelisted yet + assert_noop!( + pallet_liquidity_pools::Pallet::::update_member( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + tranche_id, + new_member.clone(), + valid_until, + ), + 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), + AccountConverter::::convert(new_member.clone()), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + valid_until + )), + )); + + // 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)), + )); + + // Verify it now works + assert_ok!(pallet_liquidity_pools::Pallet::::update_member( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + tranche_id, + new_member, + valid_until, + )); + + // Verify it cannot be called for another member without whitelisting the domain + // beforehand + assert_noop!( + pallet_liquidity_pools::Pallet::::update_member( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + tranche_id, + DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [9; 20]), + valid_until, + ), + pallet_liquidity_pools::Error::::InvestorDomainAddressNotAMember, + ); + }); + } + + fn update_token_price() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let currency_id = AUSD_CURRENCY_ID; + let pool_id = DEFAULT_POOL_ID; + + enable_liquidity_pool_transferability::(currency_id); + + create_ausd_pool::(pool_id); + + assert_ok!(pallet_liquidity_pools::Pallet::::update_token_price( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + )); + }); + } + + fn add_currency() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let gateway_sender = ::Sender::get(); + + let currency_id = AUSD_CURRENCY_ID; + + enable_liquidity_pool_transferability::(currency_id); + + assert_eq!( + orml_tokens::Pallet::::free_balance(GLMR_CURRENCY_ID, &gateway_sender), + DEFAULT_BALANCE_GLMR + ); + + assert_ok!(pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + currency_id, + )); + + assert_eq!( + orml_tokens::Pallet::::free_balance(GLMR_CURRENCY_ID, &gateway_sender), + // Ensure it only charged the 0.2 GLMR of fee + DEFAULT_BALANCE_GLMR - dollar(18).saturating_div(5) + ); + }); + } + + fn add_currency_should_fail() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + CurrencyId::ForeignAsset(42) + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + CurrencyId::Native + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards) + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards) + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + + // Should fail to add currency_id which is missing a registered + // MultiLocation + let currency_id = CurrencyId::ForeignAsset(100); + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + AssetMetadata { + name: "Test".into(), + symbol: "TEST".into(), + decimals: 12, + location: None, + existential_deposit: 1_000_000, + additional: CustomMetadata { + transferability: CrossChainTransferability::LiquidityPools, + mintable: false, + permissioned: false, + pool_currency: false, + }, + }, + Some(currency_id) + )); + + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + currency_id + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken + ); + + // Add convertable MultiLocation to metadata but remove transferability + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + // Changed: Add multilocation to metadata for some random EVM chain id for + // which no instance is registered + Some(Some(liquidity_pools_transferable_multilocation::( + u64::MAX, + [1u8; 20], + ))), + Some(CustomMetadata { + // Changed: Disallow liquidityPools transferability + transferability: CrossChainTransferability::Xcm(Default::default()), + mintable: Default::default(), + permissioned: Default::default(), + pool_currency: Default::default(), + }), + )); + + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + currency_id + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + + // Switch transferability from XCM to None + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + None, + Some(CustomMetadata { + // Changed: Disallow cross chain transferability entirely + transferability: CrossChainTransferability::None, + mintable: Default::default(), + permissioned: Default::default(), + pool_currency: Default::default(), + }) + )); + + assert_noop!( + pallet_liquidity_pools::Pallet::::add_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + currency_id + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + }); + } + + fn allow_investment_currency() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let currency_id = AUSD_CURRENCY_ID; + let pool_id = DEFAULT_POOL_ID; + let evm_chain_id: u64 = MOONBEAM_EVM_CHAIN_ID; + let evm_address = [1u8; 20]; + + // Create an AUSD pool + create_ausd_pool::(pool_id); + + enable_liquidity_pool_transferability::(currency_id); + + // Enable LiquidityPools transferability + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + // Changed: Add location which can be converted to LiquidityPoolsWrappedToken + Some(Some(liquidity_pools_transferable_multilocation::( + evm_chain_id, + evm_address, + ))), + Some(CustomMetadata { + // Changed: Allow liquidity_pools transferability + transferability: CrossChainTransferability::LiquidityPools, + mintable: Default::default(), + permissioned: Default::default(), + pool_currency: true, + }) + )); + + assert_ok!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ) + ); + }); + } + + fn allow_pool_should_fail() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + let currency_id = CurrencyId::ForeignAsset(42); + let ausd_currency_id = AUSD_CURRENCY_ID; + + // Should fail if pool does not exist + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + // Tranche id is arbitrary in this case as pool does not exist + [0u8; 16], + currency_id, + ), + pallet_liquidity_pools::Error::::PoolNotFound + ); + + // Register currency_id with pool_currency set to true + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + AssetMetadata { + name: "Test".into(), + symbol: "TEST".into(), + decimals: 12, + location: None, + existential_deposit: 1_000_000, + additional: CustomMetadata { + transferability: Default::default(), + mintable: false, + permissioned: false, + pool_currency: true, + }, + }, + Some(currency_id) + )); + + // Create pool + create_currency_pool::(pool_id, currency_id, 10_000 * dollar(12)); + + // Should fail if asset is not payment currency + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + ausd_currency_id, + ), + pallet_liquidity_pools::Error::::InvalidPaymentCurrency + ); + + // Allow as payment but not payout currency + assert_ok!(pallet_order_book::Pallet::::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!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).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!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + None, + Some(CustomMetadata { + // Disallow any cross chain transferability + transferability: CrossChainTransferability::None, + mintable: Default::default(), + permissioned: Default::default(), + // Changed: Allow to be usable as pool currency + pool_currency: true, + }), + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + + // Should fail if currency does not have any MultiLocation in metadata + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + None, + Some(CustomMetadata { + // Changed: Allow liquidityPools transferability + transferability: CrossChainTransferability::LiquidityPools, + mintable: Default::default(), + permissioned: Default::default(), + // Still allow to be pool currency + pool_currency: true, + }), + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken + ); + + // Should fail if currency does not have LiquidityPoolsWrappedToken location in + // metadata + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + // Changed: Add some location which cannot be converted to + // LiquidityPoolsWrappedToken + Some(Some(VersionedMultiLocation::V3(Default::default()))), + // No change for transferability required as it is already allowed for + // LiquidityPools + None, + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken + ); + + // Create new pool for non foreign asset + // NOTE: Can be removed after merging https://github.com/centrifuge/centrifuge-chain/pull/1343 + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + AssetMetadata { + name: "Acala Dollar".into(), + symbol: "AUSD".into(), + decimals: 12, + location: None, + existential_deposit: 1_000_000, + additional: CustomMetadata { + transferability: Default::default(), + mintable: false, + permissioned: false, + pool_currency: true, + }, + }, + Some(CurrencyId::AUSD) + )); + + create_currency_pool::(pool_id + 1, CurrencyId::AUSD, 10_000 * dollar(12)); + + // Should fail if currency is not foreign asset + assert_noop!( + pallet_liquidity_pools::Pallet::::allow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id + 1, + // Tranche id is arbitrary in this case, so we don't need to check for the + // exact pool_id + default_tranche_id::(pool_id + 1), + CurrencyId::AUSD, + ), + DispatchError::Token(sp_runtime::TokenError::Unsupported) + ); + }); + } + + fn schedule_upgrade() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + // Only Root can call `schedule_upgrade` + assert_noop!( + pallet_liquidity_pools::Pallet::::schedule_upgrade( + RawOrigin::Signed(Keyring::Bob.into()).into(), + MOONBEAM_EVM_CHAIN_ID, + [7; 20] + ), + BadOrigin + ); + + // Now it finally works + assert_ok!(pallet_liquidity_pools::Pallet::::schedule_upgrade( + ::RuntimeOrigin::root(), + MOONBEAM_EVM_CHAIN_ID, + [7; 20] + )); + }); + } + + fn cancel_upgrade() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + // Only Root can call `cancel_upgrade` + assert_noop!( + pallet_liquidity_pools::Pallet::::cancel_upgrade( + RawOrigin::Signed(Keyring::Bob.into()).into(), + MOONBEAM_EVM_CHAIN_ID, + [7; 20] + ), + BadOrigin + ); + + // Now it finally works + assert_ok!(pallet_liquidity_pools::Pallet::::cancel_upgrade( + ::RuntimeOrigin::root(), + MOONBEAM_EVM_CHAIN_ID, + [7; 20] + )); + }); + } + + fn update_tranche_token_metadata() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + // NOTE: Default pool admin is BOB + create_ausd_pool::(pool_id); + + // Missing tranche token should throw + let nonexistent_tranche = [71u8; 16]; + + assert_noop!( + pallet_liquidity_pools::Pallet::::update_tranche_token_metadata( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + nonexistent_tranche, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::TrancheNotFound + ); + + let tranche_id = default_tranche_id::(pool_id); + + // Should throw if called by anything but `PoolAdmin` + assert_noop!( + pallet_liquidity_pools::Pallet::::update_tranche_token_metadata( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::NotPoolAdmin + ); + + assert_ok!( + pallet_liquidity_pools::Pallet::::update_tranche_token_metadata( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ) + ); + + // Edge case: Should throw if tranche exists but metadata does not exist + let tranche_currency_id = CurrencyId::Tranche(pool_id, tranche_id); + + orml_asset_registry::Metadata::::remove(tranche_currency_id); + + assert_noop!( + pallet_liquidity_pools::Pallet::::update_tranche_token_metadata( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + tranche_id, + Domain::EVM(MOONBEAM_EVM_CHAIN_ID), + ), + pallet_liquidity_pools::Error::::TrancheMetadataNotFound + ); + }); + } + + crate::test_for_runtimes!([development], add_pool); + crate::test_for_runtimes!([development], add_tranche); + crate::test_for_runtimes!([development], update_member); + crate::test_for_runtimes!([development], update_token_price); + crate::test_for_runtimes!([development], add_currency); + crate::test_for_runtimes!([development], add_currency_should_fail); + crate::test_for_runtimes!([development], allow_investment_currency); + crate::test_for_runtimes!([development], allow_pool_should_fail); + crate::test_for_runtimes!([development], schedule_upgrade); + crate::test_for_runtimes!([development], cancel_upgrade); + crate::test_for_runtimes!([development], update_tranche_token_metadata); + } + + mod foreign_investments { + use super::*; + + mod same_currencies { + use super::*; + + fn increase_invest_order() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + let amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + + // Create new pool + 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, + false, + ); + + // Verify the order was updated to the amount + assert_eq!( + pallet_investments::Pallet::::acc_active_invest_order( + default_investment_id::(), + ) + .amount, + amount + ); + + // Increasing again should just bump invest_amount + let msg = LiquidityPoolMessage::IncreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount, + }; + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + assert_eq!( + InvestmentState::::get(&investor, default_investment_id::()), + InvestState::InvestmentOngoing { + invest_amount: amount * 2 + } + ); + }); + } + + fn decrease_invest_order() { + 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(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + let invest_amount: u128 = 10 * dollar(12); + let decrease_amount = invest_amount / 3; + let final_amount = invest_amount - decrease_amount; + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id: CurrencyId = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + + // Create new pool + 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, + false, + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: decrease_amount, + }; + + // Expect failure if transferability is disabled since this is required for + // preparing the `ExecutedDecreaseInvest` message. + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + enable_liquidity_pool_transferability::(currency_id); + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify investment was decreased into investment account + assert_eq!( + orml_tokens::Pallet::::balance( + currency_id, + &default_investment_account::() + ), + final_amount + ); + // Since the investment was done in the pool currency, the decrement happens + // synchronously and thus it must be burned from investor's holdings + assert_eq!(orml_tokens::Pallet::::balance(currency_id, &investor), 0); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == pallet_investments::Event::::InvestOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: final_amount + } + .into())); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == orml_tokens::Event::::Withdrawn { + currency_id, + who: investor.clone(), + amount: decrease_amount + } + .into())); + assert_eq!( + pallet_investments::Pallet::::acc_active_invest_order( + default_investment_id::(), + ) + .amount, + final_amount + ); + }); + } + + fn cancel_invest_order() { + 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 invest_amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + + // Create new pool + 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, + false, + ); + + // Verify investment account holds funds before cancelling + assert_eq!( + orml_tokens::Pallet::::balance( + currency_id, + &default_investment_account::() + ), + invest_amount + ); + + // Mock incoming cancel message + let msg = LiquidityPoolMessage::CancelInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + }; + + // Expect failure if transferability is disabled since this is required for + // preparing the `ExecutedDecreaseInvest` message. + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + + enable_liquidity_pool_transferability::(currency_id); + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + 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( + currency_id, + &default_investment_account::() + ), + 0 + ); + // Since the investment was done in the pool currency, the decrement happens + // synchronously and thus it must be burned from investor's holdings + assert_eq!(orml_tokens::Pallet::::balance(currency_id, &investor), 0); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == pallet_investments::Event::::InvestOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: 0 + } + .into())); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == orml_tokens::Event::::Withdrawn { + currency_id, + who: investor.clone(), + amount: invest_amount + } + .into())); + assert_eq!( + pallet_investments::Pallet::::acc_active_invest_order( + default_investment_id::(), + ) + .amount, + 0 + ); + }); + } + + fn collect_invest_order() { + 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 amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + 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, + false, + ); + let events_before_collect = frame_system::Pallet::::events(); + + // Process and fulfill order + // NOTE: Without this step, the order id is not cleared and + // `Event::InvestCollectedForNonClearedOrderId` be dispatched + assert_ok!(pallet_investments::Pallet::::process_invest_orders( + default_investment_id::() + )); + + // Tranche tokens will be minted upon fulfillment + assert_eq!( + orml_tokens::Pallet::::total_issuance(investment_currency_id), + 0 + ); + assert_ok!(pallet_investments::Pallet::::invest_fulfillment( + default_investment_id::(), + FulfillmentWithPrice { + of_amount: Perquintill::one(), + price: Ratio::one(), + } + )); + assert_eq!( + orml_tokens::Pallet::::total_issuance(investment_currency_id), + amount + ); + + // Mock collection message msg + 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!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Remove events before collect execution + let events_since_collect: Vec<_> = frame_system::Pallet::::events() + .into_iter() + .filter(|e| !events_before_collect.contains(e)) + .collect(); + + // Verify investment was transferred to the domain locator + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &sending_domain_locator + ), + amount + ); + + // Order should have been cleared by fulfilling investment + assert_eq!( + pallet_investments::Pallet::::acc_active_invest_order( + default_investment_id::(), + ) + .amount, + 0 + ); + assert!(!events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::InvestCollectedForNonClearedOrderId { + investment_id: default_investment_id::(), + who: investor.clone(), + } + .into() + })); + + // Order should not have been updated since everything is collected + assert!(!events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::InvestOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: 0, + } + .into() + })); + + // Order should have been fully collected + assert!(events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::InvestOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![0], + who: investor.clone(), + collection: InvestCollection:: { + payout_investment_invest: amount, + remaining_investment_invest: 0, + }, + outcome: CollectOutcome::FullyCollected, + } + .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 { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: LiquidityPoolMessage::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + currency_payout: amount, + tranche_tokens_payout: amount, + remaining_invest_amount: 0, + }, + } + .into() + })); + }); + } + + fn partially_collect_investment_for_through_investments() { + 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 invest_amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + 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, + false, + ); + enable_liquidity_pool_transferability::(currency_id); + let investment_currency_id: CurrencyId = default_investment_id::().into(); + + assert!( + !pallet_investments::Pallet::::investment_requires_collect( + &investor, + default_investment_id::() + ) + ); + + // 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(), + } + )); + + // Pre collect assertions + assert!( + pallet_investments::Pallet::::investment_requires_collect( + &investor, + 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 + assert_ok!(pallet_investments::Pallet::::collect_investments_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + 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!( + orml_tokens::Pallet::::balance(investment_currency_id, &investor), + 0 + ); + assert_eq!( + orml_tokens::Pallet::::balance( + investment_currency_id, + &sending_domain_locator + ), + invest_amount * 2 + ); + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_investments::Event::::InvestOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![0], + who: investor.clone(), + collection: InvestCollection:: { + payout_investment_invest: invest_amount * 2, + remaining_investment_invest: invest_amount / 2, + }, + outcome: CollectOutcome::FullyCollected, + } + .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::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + currency_payout: invest_amount / 2, + tranche_tokens_payout: invest_amount * 2, + remaining_invest_amount: invest_amount / 2, + }, + } + .into() + })); + + // Process rest of investment at 50% rate (1 pool currency = 2 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::one(), + price: Ratio::checked_from_rational(1, 2).unwrap(), + } + )); + // Order should have been cleared by fulfilling investment + assert_eq!( + pallet_investments::Pallet::::acc_active_invest_order( + default_investment_id::(), + ) + .amount, + 0 + ); + assert_eq!( + orml_tokens::Pallet::::total_issuance(investment_currency_id), + invest_amount * 3 + ); + + // Collect remainder through Investments + assert_ok!(pallet_investments::Pallet::::collect_investments_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + investor.clone(), + default_investment_id::() + )); + assert!( + !pallet_investments::Pallet::::investment_requires_collect( + &investor, + 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; + assert_eq!( + orml_tokens::Pallet::::total_issuance(investment_currency_id), + amount_tranche_tokens + ); + assert!( + orml_tokens::Pallet::::balance(investment_currency_id, &investor) + .is_zero() + ); + assert_eq!( + orml_tokens::Pallet::::balance( + investment_currency_id, + &sending_domain_locator + ), + amount_tranche_tokens + ); + assert!(!frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_investments::Event::::InvestCollectedForNonClearedOrderId { + investment_id: default_investment_id::(), + who: investor.clone(), + } + .into() + })); + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_investments::Event::::InvestOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![1], + who: investor.clone(), + collection: InvestCollection:: { + payout_investment_invest: invest_amount, + remaining_investment_invest: 0, + }, + outcome: CollectOutcome::FullyCollected, + } + .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::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + currency_payout: invest_amount / 2, + tranche_tokens_payout: invest_amount, + remaining_invest_amount: 0, + }, + } + .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 + let msg = LiquidityPoolMessage::CollectInvest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + ), + pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + ); + }); + } + + fn increase_redeem_order() { + 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 amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + + // Create new pool + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + + // Set permissions and execute initial redemption + do_initial_increase_redemption::( + pool_id, + amount, + investor.clone(), + currency_id, + ); + + // Verify amount was noted in the corresponding order + assert_eq!( + pallet_investments::Pallet::::acc_active_redeem_order( + default_investment_id::(), + ) + .amount, + amount + ); + + // Increasing again should just bump redeeming amount + assert_ok!(orml_tokens::Pallet::::mint_into( + default_investment_id::().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + amount + )); + let msg = LiquidityPoolMessage::IncreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount, + }; + 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 * 2, + } + ); + }); + } + + fn decrease_redeem_order() { + 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 redeem_amount = 10 * dollar(12); + let decrease_amount = redeem_amount / 3; + let final_amount = redeem_amount - decrease_amount; + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let sending_domain_locator = + Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); + + // Create new pool + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + + // Set permissions and execute initial redemption + do_initial_increase_redemption::( + pool_id, + redeem_amount, + investor.clone(), + currency_id, + ); + + // Verify the corresponding redemption order id is 0 + assert_eq!( + pallet_investments::Pallet::::invest_order_id(investment_id::( + pool_id, + default_tranche_id::(pool_id) + )), + 0 + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::DecreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: decrease_amount, + }; + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify investment was decreased into investment account + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &default_investment_account::(), + ), + final_amount + ); + // Tokens should have been transferred from investor's wallet to domain's + // sovereign account + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &investor + ), + 0 + ); + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &sending_domain_locator + ), + 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 { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: final_amount + } + .into())); + assert_eq!( + pallet_investments::Pallet::::acc_active_redeem_order( + default_investment_id::(), + ) + .amount, + final_amount + ); + + 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::ExecutedDecreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + tranche_tokens_payout: decrease_amount, + remaining_redeem_amount: final_amount, + }, + } + .into() + })); + }); + } + + fn cancel_redeem_order() { + 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 redeem_amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let sending_domain_locator = + Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); + + // Create new pool + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + + // Set permissions and execute initial redemption + do_initial_increase_redemption::( + pool_id, + redeem_amount, + investor.clone(), + currency_id, + ); + + // Verify the corresponding redemption order id is 0 + assert_eq!( + pallet_investments::Pallet::::invest_order_id(investment_id::( + pool_id, + default_tranche_id::(pool_id) + )), + 0 + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::CancelRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + }; + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify investment was decreased into investment account + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &default_investment_account::(), + ), + 0 + ); + // Tokens should have been transferred from investor's wallet to domain's + // sovereign account + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &investor + ), + 0 + ); + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &sending_domain_locator + ), + 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 + == pallet_investments::Event::::RedeemOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: 0 + } + .into())); + assert_eq!( + pallet_investments::Pallet::::acc_active_redeem_order( + default_investment_id::(), + ) + .amount, + 0 + ); + }); + } + + fn fully_collect_redeem_order() { + 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 amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } + .into_account_truncating(); + + // Create new pool + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + + // Set permissions and execute initial investment + do_initial_increase_redemption::( + pool_id, + amount, + investor.clone(), + currency_id, + ); + let events_before_collect = frame_system::Pallet::::events(); + + // Fund the pool account with sufficient pool currency, else redemption cannot + // swap tranche tokens against pool currency + assert_ok!(orml_tokens::Pallet::::mint_into( + currency_id, + &pool_account, + amount + )); + + // Process and fulfill order + // NOTE: Without this step, the order id is not cleared and + // `Event::RedeemCollectedForNonClearedOrderId` be dispatched + 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::one(), + price: Ratio::one(), + } + )); + + // Enable liquidity pool transferability + enable_liquidity_pool_transferability::(currency_id); + + // Mock collection message msg + let msg = LiquidityPoolMessage::CollectRedeem { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + }; + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Remove events before collect execution + let events_since_collect: Vec<_> = frame_system::Pallet::::events() + .into_iter() + .filter(|e| !events_before_collect.contains(e)) + .collect(); + + // Verify collected redemption was burned from investor + assert_eq!(orml_tokens::Pallet::::balance(currency_id, &investor), 0); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == orml_tokens::Event::::Withdrawn { + currency_id, + who: investor.clone(), + amount + } + .into())); + + // Order should have been cleared by fulfilling redemption + assert_eq!( + pallet_investments::Pallet::::acc_active_redeem_order( + default_investment_id::(), + ) + .amount, + 0 + ); + assert!(!events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::RedeemCollectedForNonClearedOrderId { + investment_id: default_investment_id::(), + who: investor.clone(), + } + .into() + })); + + // Order should not have been updated since everything is collected + assert!(!events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::RedeemOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: 0, + } + .into() + })); + + // Order should have been fully collected + assert!(events_since_collect.iter().any(|e| { + e.event + == pallet_investments::Event::::RedeemOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![0], + who: investor.clone(), + collection: RedeemCollection:: { + payout_investment_redeem: amount, + remaining_investment_redeem: 0, + }, + outcome: CollectOutcome::FullyCollected, + } + .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 { + 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::(currency_id), + currency_payout: amount, + tranche_tokens_payout: amount, + remaining_redeem_amount: 0, + }, + } + .into() + })); + }); + } + + fn partially_collect_redemption_for_through_investments() { + 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 redeem_amount = 10 * dollar(12); + let investor: AccountId = AccountConverter::::convert( + (DOMAIN_MOONBEAM, Keyring::Bob.into()), + ); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } + .into_account_truncating(); + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + do_initial_increase_redemption::( + pool_id, + redeem_amount, + investor.clone(), + currency_id, + ); + enable_liquidity_pool_transferability::(currency_id); + + // Fund the pool account with sufficient pool currency, else redemption cannot + // swap tranche tokens against pool currency + assert_ok!(orml_tokens::Pallet::::mint_into( + currency_id, + &pool_account, + redeem_amount + )); + assert!( + !pallet_investments::Pallet::::redemption_requires_collect( + &investor, + default_investment_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(), + } + )); + + // Pre collect assertions + assert!( + pallet_investments::Pallet::::redemption_requires_collect( + &investor, + 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( + RawOrigin::Signed(Keyring::Alice.into()).into(), + investor.clone(), + default_investment_id::() + )); + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_investments::Event::::RedeemOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![0], + who: investor.clone(), + collection: RedeemCollection:: { + payout_investment_redeem: redeem_amount / 8, + remaining_investment_redeem: redeem_amount / 2, + }, + outcome: CollectOutcome::FullyCollected, + } + .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::(currency_id), + currency_payout: redeem_amount / 8, + tranche_tokens_payout: redeem_amount / 2, + remaining_redeem_amount: redeem_amount / 2, + }, + } + .into() + })); + assert!( + !pallet_investments::Pallet::::redemption_requires_collect( + &investor, + default_investment_id::() + ) + ); + // 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, + who: investor.clone(), + amount: redeem_amount / 8 + } + .into())); + + // Process rest of redemption at 50% rate + 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::one(), + price: Ratio::checked_from_rational(1, 2).unwrap(), + } + )); + // Order should have been cleared by fulfilling redemption + assert_eq!( + pallet_investments::Pallet::::acc_active_redeem_order( + default_investment_id::(), + ) + .amount, + 0 + ); + + // Collect remainder through Investments + assert_ok!(pallet_investments::Pallet::::collect_redemptions_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + investor.clone(), + default_investment_id::() + )); + assert!( + !pallet_investments::Pallet::::redemption_requires_collect( + &investor, + 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 { + investment_id: default_investment_id::(), + who: investor.clone(), + } + .into() + })); + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_investments::Event::::RedeemOrdersCollected { + investment_id: default_investment_id::(), + processed_orders: vec![1], + who: investor.clone(), + collection: RedeemCollection:: { + payout_investment_redeem: redeem_amount / 4, + remaining_investment_redeem: 0, + }, + outcome: CollectOutcome::FullyCollected, + } + .into() + })); + // Verify collected redemption was burned from investor + assert_eq!(orml_tokens::Pallet::::balance(currency_id, &investor), 0); + assert!(frame_system::Pallet::::events().iter().any(|e| e.event + == orml_tokens::Event::::Withdrawn { + currency_id, + who: investor.clone(), + amount: redeem_amount / 4 + } + .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 { + 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::(currency_id), + currency_payout: redeem_amount / 4, + tranche_tokens_payout: redeem_amount / 2, + remaining_redeem_amount: 0, + }, + } + .into() + })); + }); + } + + crate::test_for_runtimes!([development], increase_invest_order); + crate::test_for_runtimes!([development], decrease_invest_order); + crate::test_for_runtimes!([development], cancel_invest_order); + crate::test_for_runtimes!([development], collect_invest_order); + crate::test_for_runtimes!( + [development], + partially_collect_investment_for_through_investments + ); + crate::test_for_runtimes!([development], increase_redeem_order); + crate::test_for_runtimes!([development], decrease_redeem_order); + crate::test_for_runtimes!([development], cancel_redeem_order); + crate::test_for_runtimes!([development], fully_collect_redeem_order); + crate::test_for_runtimes!( + [development], + partially_collect_redemption_for_through_investments + ); + + mod should_fail { + use super::*; + + mod decrease_should_underflow { + use super::*; + + fn invest_decrease_underflow() { + 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 invest_amount: u128 = 10 * dollar(12); + let decrease_amount = invest_amount + 1; + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + Keyring::Bob.into(), + )); + 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, + invest_amount, + investor.clone(), + currency_id, + false, + ); + enable_liquidity_pool_transferability::(currency_id); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: decrease_amount, + }; + + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + ), + pallet_foreign_investments::Error::::InvestError( + InvestError::DecreaseAmountOverflow + ) + ); + }); + } + + fn redeem_decrease_underflow() { + 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 redeem_amount: u128 = 10 * dollar(12); + let decrease_amount = redeem_amount + 1; + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + Keyring::Bob.into(), + )); + 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_redemption::( + pool_id, + redeem_amount, + investor.clone(), + currency_id, + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::DecreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: decrease_amount, + }; + + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + ), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::DecreaseTransition + ) + ); + }); + } + + crate::test_for_runtimes!([development], invest_decrease_underflow); + crate::test_for_runtimes!([development], redeem_decrease_underflow); + } + + mod should_throw_requires_collect { + use super::*; + + fn invest_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 amount: u128 = 10 * dollar(12); + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + Keyring::Bob.into(), + )); + 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, + false, + ); + enable_liquidity_pool_transferability::(currency_id); + + // Prepare collection + let pool_account = + pallet_pool_system::pool_types::PoolLocator { pool_id } + .into_account_truncating(); + assert_ok!(orml_tokens::Pallet::::mint_into( + currency_id, + &pool_account, + amount + )); + 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::one(), + price: Ratio::one(), + } + )); + + // Should fail to increase + let increase_msg = LiquidityPoolMessage::IncreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: AUSD_ED, + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + increase_msg + ), + pallet_foreign_investments::Error::::InvestError( + InvestError::CollectRequired + ) + ); + + // Should fail to decrease + let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: 1, + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_foreign_investments::Error::::InvestError( + InvestError::CollectRequired + ) + ); + }); + } + + fn redeem_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 amount: u128 = 10 * dollar(12); + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + Keyring::Bob.into(), + )); + 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_redemption::( + pool_id, + amount, + investor.clone(), + currency_id, + ); + enable_liquidity_pool_transferability::(currency_id); + + // Mint more into DomainLocator required for subsequent invest attempt + assert_ok!(orml_tokens::Pallet::::mint_into( + default_investment_id::().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + 1, + )); + + // Prepare collection + let pool_account = + pallet_pool_system::pool_types::PoolLocator { pool_id } + .into_account_truncating(); + assert_ok!(orml_tokens::Pallet::::mint_into( + currency_id, + &pool_account, + amount + )); + 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::one(), + price: Ratio::one(), + } + )); + + // Should fail to increase + let increase_msg = LiquidityPoolMessage::IncreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: 1, + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + increase_msg + ), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::CollectRequired + ) + ); + + // Should fail to decrease + let decrease_msg = LiquidityPoolMessage::DecreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(currency_id), + amount: 1, + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::CollectRequired + ) + ); + }); + } + + crate::test_for_runtimes!([development], invest_requires_collect); + crate::test_for_runtimes!([development], redeem_requires_collect); + } + + mod payment_payout_currency { + use super::*; + + fn invalid_invest_payment_currency() { + 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 * dollar(18); + + 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, 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: AUSD_ED, + }; + assert_noop!( + pallet_liquidity_pools::Pallet::::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!( + pallet_liquidity_pools::Pallet::::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!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + collect_msg + ), + pallet_foreign_investments::Error::::InvestError( + InvestError::InvalidPaymentCurrency + ) + ); + }); + } + + fn invalid_redeem_payout_currency() { + 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 * 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!(orml_tokens::Pallet::::mint_into( + default_investment_id::().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + amount, + )); + + // 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!( + pallet_liquidity_pools::Pallet::::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!( + pallet_liquidity_pools::Pallet::::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!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + collect_msg + ), + pallet_foreign_investments::Error::::RedeemError( + RedeemError::InvalidPayoutCurrency + ) + ); + }); + } + + fn invest_payment_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 * 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!( + pallet_liquidity_pools::Pallet::::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!( + pallet_liquidity_pools::Pallet::::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, collect_msg), + pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound + ); + }); + } + + 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 * 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!(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); + } + } + } + + mod mismatching_currencies { + use super::*; + + fn collect_foreign_investment_for() { + 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: 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: invest_amount_foreign_denominated, + }; + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Process 100% of investment at 50% rate (1 pool currency = 2 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::one(), + price: Ratio::checked_from_rational(1, 2).unwrap(), + } + )); + assert_ok!(pallet_investments::Pallet::::collect_investments_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + 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 + ) + .is_zero()); + assert_eq!( + orml_tokens::Pallet::::balance( + default_investment_id::().into(), + &sending_domain_locator + ), + invest_amount_pool_denominated * 2 + ); + 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::ExecutedCollectInvest { + 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, + tranche_tokens_payout: invest_amount_pool_denominated * 2, + remaining_invest_amount: 0, + }, + } + .into() + })); + + // 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. + fn invest_increase_decrease() { + 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: 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!( + 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 to decrease in the swapping foreign currency + enable_liquidity_pool_transferability::(foreign_currency); + let decrease_msg_pool_swap_amount = 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, + 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); + let decrease_msg_partial_invest_amount = + 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, + 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() + })); + }); + } + + /// Propagate swaps only via OrderBook fulfillments. + /// + /// Flow: Increase, fulfill, decrease, fulfill + fn invest_swaps_happy_path() { + 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 * dollar(18); + 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( + 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(), + ); - pub fn setup_usdc_xcm(env: &mut FudgeEnv) { - env.parachain_state_mut(|| { - // Set the XCM version used when sending XCM messages to USDC parachain. - assert_ok!(pallet_xcm::Pallet::::force_xcm_version( - ::RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(1000)), - )), - XCM_VERSION, - )); - }); + 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 * 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, + ); + 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(), + ); - env.relay_state_mut(|| { - assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< - FudgeRelayRuntime, - >::force_open_hrmp_channel( - as frame_system::Config>::RuntimeOrigin::root(), - Id::from(T::FudgeHandle::PARA_ID), - Id::from(1000), - 10, - 1024, - )); + 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 * 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, + || {}, + ); + 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(), + ); - assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< - FudgeRelayRuntime, - >::force_process_hrmp_open( - as frame_system::Config>::RuntimeOrigin::root(), - 0, - )); - }); + 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 * 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, + ); + 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(), + ); - env.pass(Blocks::ByNumber(1)); + 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 * 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, + ); + 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 * 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, + ); + 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() + })); + }); + } + + 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 + ); + } } } -type FudgeRelayRuntime = <::FudgeHandle as FudgeHandle>::RelayRuntime; - -use utils::*; - mod altair { use altair_runtime::{CurrencyIdConvert, PoolPalletIndex}; @@ -157,32 +6075,6 @@ mod altair { mod utils { use super::*; - pub fn register_ausd() { - let meta: AssetMetadata = AssetMetadata { - decimals: 12, - name: "Acala Dollar".into(), - symbol: "AUSD".into(), - existential_deposit: 1_000_000_000, - location: Some(VersionedMultiLocation::V3(MultiLocation::new( - 1, - X2( - Parachain(T::FudgeHandle::SIBLING_ID), - general_key(parachains::kusama::karura::AUSD_KEY), - ), - ))), - additional: CustomMetadata { - transferability: CrossChainTransferability::Xcm(Default::default()), - ..CustomMetadata::default() - }, - }; - - assert_ok!(orml_asset_registry::Pallet::::register_asset( - ::RuntimeOrigin::root(), - meta, - Some(AUSD_CURRENCY_ID) - )); - } - pub fn register_air() { let meta: AssetMetadata = AssetMetadata { decimals: 18, @@ -233,10 +6125,6 @@ mod altair { amount * dollar(currency_decimals::NATIVE) } - pub fn ausd(amount: Balance) -> Balance { - amount * dollar(currency_decimals::AUSD) - } - pub fn ksm(amount: Balance) -> Balance { amount * dollar(currency_decimals::KSM) } @@ -245,34 +6133,14 @@ mod altair { amount * dollar(decimals) } - pub fn dollar(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) - } - pub fn air_fee() -> Balance { fee(currency_decimals::NATIVE) } - pub fn ausd_fee() -> Balance { - fee(currency_decimals::AUSD) - } - - pub fn fee(decimals: u32) -> Balance { - calc_fee(default_per_second(decimals)) - } - // The fee associated with transferring KSM tokens pub fn ksm_fee() -> Balance { calc_fee(ksm_per_second()) } - - pub fn calc_fee(fee_per_second: Balance) -> Balance { - // We divide the fee to align its unit and multiply by 4 as that seems to be the - // unit of time the tests take. - // NOTE: it is possible that in different machines this value may differ. We - // shall see. - fee_per_second.div_euclid(10_000) * 8 - } } use utils::*; @@ -1179,7 +7047,6 @@ mod altair { mod centrifuge { use centrifuge_runtime::CurrencyIdConvert; - use sp_core::Get; use super::*; @@ -1275,34 +7142,6 @@ mod centrifuge { )); } - /// Register AUSD in the asset registry. - /// It should be executed within an externalities environment. - pub fn register_ausd() { - let meta: AssetMetadata = AssetMetadata { - decimals: 12, - name: "Acala Dollar".into(), - symbol: "AUSD".into(), - existential_deposit: 1_000_000_000_000, - location: Some(VersionedMultiLocation::V3(MultiLocation::new( - 1, - X2( - Parachain(T::FudgeHandle::SIBLING_ID), - general_key(parachains::polkadot::acala::AUSD_KEY), - ), - ))), - additional: CustomMetadata { - transferability: CrossChainTransferability::Xcm(Default::default()), - ..CustomMetadata::default() - }, - }; - - assert_ok!(orml_asset_registry::Pallet::::register_asset( - ::RuntimeOrigin::root(), - meta, - Some(AUSD_CURRENCY_ID) - )); - } - /// Register CFG in the asset registry. /// It should be executed within an externalities environment. pub fn register_cfg() { @@ -1391,14 +7230,6 @@ mod centrifuge { fee(currency_decimals::NATIVE) } - pub fn ausd_fee() -> Balance { - fee(currency_decimals::AUSD) - } - - pub fn fee(decimals: u32) -> Balance { - calc_fee(default_per_second(decimals)) - } - // The fee associated with transferring DOT tokens pub fn dot_fee() -> Balance { fee(10) @@ -1412,26 +7243,6 @@ mod centrifuge { fee(6) } - pub fn calc_fee(fee_per_second: Balance) -> Balance { - // We divide the fee to align its unit and multiply by 4 as that seems to be the - // unit of time the tests take. - // NOTE: it is possible that in different machines this value may differ. We - // shall see. - fee_per_second.div_euclid(10_000) * 8 - } - - pub fn cfg(amount: Balance) -> Balance { - amount * dollar(currency_decimals::NATIVE) - } - - pub fn dollar(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) - } - - pub fn ausd(amount: Balance) -> Balance { - amount * dollar(currency_decimals::AUSD) - } - pub fn dot(amount: Balance) -> Balance { amount * dollar(10) } @@ -1708,34 +7519,6 @@ mod centrifuge { }); } - fn convert_ausd() { - let mut env = FudgeEnv::::default(); - - assert_eq!(parachains::polkadot::acala::AUSD_KEY, &[0, 1]); - - let ausd_location: MultiLocation = MultiLocation::new( - 1, - X2( - Parachain(T::FudgeHandle::SIBLING_ID), - general_key(parachains::polkadot::acala::AUSD_KEY), - ), - ); - - env.parachain_state_mut(|| { - register_ausd::(); - - assert_eq!( - >::convert(ausd_location), - Ok(AUSD_CURRENCY_ID), - ); - - assert_eq!( - >::convert(AUSD_CURRENCY_ID), - Some(ausd_location) - ) - }); - } - fn convert_dot() { let mut env = FudgeEnv::::default(); @@ -1789,7 +7572,6 @@ mod centrifuge { crate::test_for_runtimes!([centrifuge], convert_cfg); crate::test_for_runtimes!([centrifuge], convert_cfg_xcm_v2); crate::test_for_runtimes!([centrifuge], convert_no_xcm_token); - crate::test_for_runtimes!([centrifuge], convert_ausd); crate::test_for_runtimes!([centrifuge], convert_dot); crate::test_for_runtimes!([centrifuge], convert_unknown_multilocation); crate::test_for_runtimes!([centrifuge], convert_unsupported_currency); diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index 974fd590e4..d3f13c7e8b 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -6,7 +6,8 @@ use cfg_primitives::{ }; use cfg_traits::Millis; use cfg_types::{ - fixed_point::{Quantity, Rate}, + domain_address::Domain, + fixed_point::{Quantity, Rate, Ratio}, investments::InvestmentPortfolio, locations::Location, oracles::OracleKey, @@ -21,6 +22,7 @@ use frame_support::{ Parameter, }; use liquidity_pools_gateway_routers::DomainRouter; +use pallet_liquidity_pools::Message; use pallet_transaction_payment::CurrencyAdapter; use runtime_common::{ apis, @@ -67,8 +69,11 @@ pub trait Runtime: ModifyPool = pallet_pool_system::Pallet, ModifyWriteOffPolicy = pallet_loans::Pallet, > + pallet_permissions::Config> - + pallet_investments::Config - + pallet_loans::Config< + + pallet_investments::Config< + InvestmentId = TrancheCurrency, + Amount = Balance, + BalanceRatio = Ratio, + > + pallet_loans::Config< Balance = Balance, PoolId = PoolId, LoanId = LoanId, @@ -102,11 +107,30 @@ pub trait Runtime: + pallet_restricted_tokens::Config + pallet_restricted_xtokens::Config + pallet_transfer_allowlist::Config - + pallet_liquidity_pools::Config - + pallet_liquidity_pools_gateway::Config> - + pallet_xcm_transactor::Config + + pallet_liquidity_pools::Config< + CurrencyId = CurrencyId, + Balance = Balance, + PoolId = PoolId, + TrancheId = TrancheId, + TrancheCurrency = TrancheCurrency, + BalanceRatio = Ratio, + > + pallet_liquidity_pools_gateway::Config< + Router = DomainRouter, + Message = Message, + > + pallet_xcm_transactor::Config + pallet_ethereum::Config + pallet_ethereum_transaction::Config + + pallet_order_book::Config< + Balance = Balance, + AssetCurrencyId = CurrencyId, + OrderIdNonce = u64, + SellRatio = Ratio, + > + pallet_foreign_investments::Config< + Balance = Balance, + InvestmentId = TrancheCurrency, + CurrencyId = CurrencyId, + TokenSwapOrderId = u64, + > { /// Just the RuntimeCall type, but redefined with extra bounds. /// You can add `From` bounds in order to convert pallet calls to @@ -130,7 +154,6 @@ pub trait Runtime: /// events to RuntimeEvent in tests. type RuntimeEventExt: Parameter + Member - + From> + Debug + IsType<::RuntimeEvent> + TryInto> @@ -138,13 +161,19 @@ pub trait Runtime: + TryInto> + TryInto> + TryInto> + + TryInto> + From> + From> + From> + From> + From> + From> - + From>; + + From> + + From> + + From> + + From> + + From> + + From>; type RuntimeOriginExt: Into, ::RuntimeOrigin>> + From> 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 deleted file mode 100644 index 8d5cb3b9f4..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/add_allow_upgrade.rs +++ /dev/null @@ -1,836 +0,0 @@ -// Copyright 2021 Centrifuge GmbH (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. - -// Copyright 2021 Centrifuge GmbH (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::{currency_decimals, parachains, AccountId, Balance, PoolId, TrancheId, CFG}; -use cfg_traits::{ - investments::{OrderManager, TrancheCurrency as TrancheCurrencyT}, - liquidity_pools::InboundQueue, - Permissions as _, -}; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - permissions::{PermissionScope, PoolRole, Role}, - tokens::{ - CrossChainTransferability, CurrencyId, CurrencyId::ForeignAsset, CustomMetadata, - ForeignAssetId, - }, -}; -use codec::Encode; -use frame_support::{assert_noop, assert_ok, traits::fungibles::Mutate}; -use fudge::primitives::{Chain, PoolState}; -use orml_traits::{asset_registry::AssetMetadata, FixedConversionRateProvider, MultiCurrency}; -use polkadot_parachain::primitives::Id; -use runtime_common::account_conversion::AccountConverter; -use sp_runtime::{ - traits::{BadOrigin, Convert, One, Zero}, - BoundedVec, DispatchError, Storage, -}; -use tokio::runtime::Handle; -use xcm::{ - latest::MultiLocation, - prelude::XCM_VERSION, - v3::{Junction, Junctions}, - VersionedMultiLocation, -}; - -use crate::{ - chain::{ - centrifuge::{ - LiquidityPools, LocationToAccountId, OrderBook, OrmlAssetRegistry, OrmlTokens, - Permissions, Runtime as DevelopmentRuntime, RuntimeCall, RuntimeEvent, RuntimeOrigin, - System, TreasuryAccount, XTokens, XcmTransactor, PARA_ID, - }, - relay::{Runtime as RelayRuntime, RuntimeOrigin as RelayRuntimeOrigin}, - }, - liquidity_pools::pallet::development::{ - setup::dollar, - tests::liquidity_pools::setup::{ - asset_metadata, create_ausd_pool, create_currency_pool, - enable_liquidity_pool_transferability, get_default_moonbeam_native_token_location, - investments::default_tranche_id, liquidity_pools_transferable_multilocation, - setup_pre_requirements, setup_test_env, DEFAULT_BALANCE_GLMR, DEFAULT_POOL_ID, - DEFAULT_SIBLING_LOCATION, DEFAULT_VALIDITY, - }, - }, - utils::{ - accounts::Keyring, - env, - env::{ChainState, EventRange, PARA_ID_SIBLING}, - genesis, AUSD_CURRENCY_ID, GLMR_CURRENCY_ID, MOONBEAM_EVM_CHAIN_ID, - }, -}; - -/// NOTE: We can't actually verify that the messages hits the -/// LiquidityPoolsXcmRouter contract on Moonbeam since that would require a -/// rather heavy e2e setup to emulate, involving depending on Moonbeam's -/// runtime, having said contract deployed to their evm environment, and be able -/// to query the evm side. Instead, these tests verify that - given all -/// pre-requirements are set up correctly - we succeed to send the message from -/// the Centrifuge chain pov. We have other unit tests verifying the -/// LiquidityPools' messages encoding and the encoding of the remote EVM call to -/// be executed on Moonbeam. -/// Verify that `LiquidityPools::add_pool` succeeds when called with all the -/// necessary requirements. -#[tokio::test] -async fn add_pool() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - - // Verify that the pool must exist before we can call LiquidityPools::add_pool - assert_noop!( - LiquidityPools::add_pool( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::PoolNotFound - ); - - // Now create the pool - create_ausd_pool(pool_id); - - // Verify ALICE can't call `add_pool` given she is not the `PoolAdmin` - assert_noop!( - LiquidityPools::add_pool( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::NotPoolAdmin - ); - - // Verify that it works if it's BOB calling it (the pool admin) - assert_ok!(LiquidityPools::add_pool( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - )); - }); -} - -/// Verify that `LiquidityPools::add_tranche` succeeds when called with all the -/// necessary requirements. We can't actually verify that the call hits the -/// LiquidityPoolsXcmRouter contract on Moonbeam since that would require a very -/// heavy e2e setup to emulate. Instead, here we test that we can send the -/// extrinsic and we have other unit tests verifying the encoding of the remote -/// EVM call to be executed on Moonbeam. -#[tokio::test] -async fn add_tranche() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // Now create the pool - let pool_id = DEFAULT_POOL_ID; - create_ausd_pool(pool_id); - - // Verify we can't call LiquidityPools::add_tranche with a non-existing - // tranche_id - let nonexistent_tranche = [71u8; 16]; - assert_noop!( - LiquidityPools::add_tranche( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - nonexistent_tranche, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::TrancheNotFound - ); - let tranche_id = default_tranche_id(pool_id); - - // Verify ALICE can't call `add_tranche` given she is not the `PoolAdmin` - assert_noop!( - LiquidityPools::add_tranche( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::NotPoolAdmin - ); - - // Finally, verify we can call LiquidityPools::add_tranche successfully - // when called by the PoolAdmin with the right pool + tranche id pair. - assert_ok!(LiquidityPools::add_tranche( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - )); - - // Edge case: Should throw if tranche exists but metadata does not exist - let tranche_currency_id = CurrencyId::Tranche(pool_id, tranche_id); - orml_asset_registry::Metadata::::remove(tranche_currency_id); - assert_noop!( - LiquidityPools::update_tranche_token_metadata( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::TrancheMetadataNotFound - ); - }); -} - -#[tokio::test] -async fn update_member() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // Now create the pool - let pool_id = DEFAULT_POOL_ID; - - create_ausd_pool(pool_id); - - let tranche_id = default_tranche_id(pool_id); - - // Finally, verify we can call LiquidityPools::add_tranche successfully - // when given a valid pool + tranche id pair. - let new_member = DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [3; 20]); - let valid_until = DEFAULT_VALIDITY; - - // Make ALICE the MembersListAdmin of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - Keyring::Alice.into(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - - // Verify it fails if the destination is not whitelisted yet - assert_noop!( - LiquidityPools::update_member( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - tranche_id, - new_member.clone(), - valid_until, - ), - pallet_liquidity_pools::Error::::InvestorDomainAddressNotAMember, - ); - - // Whitelist destination as TrancheInvestor of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::signed(Keyring::Alice.into()), - Role::PoolRole(PoolRole::InvestorAdmin), - AccountConverter::::convert( - new_member.clone() - ), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id(pool_id), - valid_until - )), - )); - - // Verify the Investor role was set as expected in Permissions - assert!(Permissions::has( - PermissionScope::Pool(pool_id), - AccountConverter::::convert( - new_member.clone() - ), - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, valid_until)), - )); - - // Verify it now works - assert_ok!(LiquidityPools::update_member( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - tranche_id, - new_member, - valid_until, - )); - - // Verify it cannot be called for another member without whitelisting the domain - // beforehand - assert_noop!( - LiquidityPools::update_member( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - tranche_id, - DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [9; 20]), - valid_until, - ), - pallet_liquidity_pools::Error::::InvestorDomainAddressNotAMember, - ); - }); -} - -#[tokio::test] -async fn update_token_price() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let currency_id = AUSD_CURRENCY_ID; - let pool_id = DEFAULT_POOL_ID; - - enable_liquidity_pool_transferability(currency_id); - - create_ausd_pool(pool_id); - - assert_ok!(LiquidityPools::update_token_price( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - currency_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - )); - }); -} - -#[tokio::test] -async fn add_currency() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_state(Chain::Para(PARA_ID), || { - let gateway_sender = - ::Sender::get(); - - let currency_id = AUSD_CURRENCY_ID; - - enable_liquidity_pool_transferability(currency_id); - - assert_eq!( - OrmlTokens::free_balance(GLMR_CURRENCY_ID, &gateway_sender), - DEFAULT_BALANCE_GLMR - ); - - assert_ok!(LiquidityPools::add_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - currency_id, - )); - - assert_eq!( - OrmlTokens::free_balance(GLMR_CURRENCY_ID, &gateway_sender), - /// Ensure it only charged the 0.2 GLMR of fee - DEFAULT_BALANCE_GLMR - - dollar(18).saturating_div(5) - ); - }) - .unwrap(); -} - -#[tokio::test] -async fn add_currency_should_fail() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - assert_noop!( - LiquidityPools::add_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - CurrencyId::ForeignAsset(42) - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - assert_noop!( - LiquidityPools::add_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - CurrencyId::Native - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - assert_noop!( - LiquidityPools::add_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards) - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - assert_noop!( - LiquidityPools::add_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards) - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - - // Should fail to add currency_id which is missing a registered - // MultiLocation - let currency_id = CurrencyId::ForeignAsset(100); - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - asset_metadata( - "Test".into(), - "TEST".into(), - 12, - false, - 1_000_000, - None, - CrossChainTransferability::LiquidityPools, - ), - Some(currency_id) - )); - assert_noop!( - LiquidityPools::add_currency(RuntimeOrigin::signed(Keyring::Bob.into()), currency_id), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken - ); - - // Add convertable MultiLocation to metadata but remove transferability - assert_ok!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - // Changed: Add multilocation to metadata for some random EVM chain id for which no - // instance is registered - Some(Some(liquidity_pools_transferable_multilocation( - u64::MAX, - [1u8; 20], - ))), - Some(CustomMetadata { - // Changed: Disallow liquidityPools transferability - transferability: CrossChainTransferability::Xcm(Default::default()), - mintable: Default::default(), - permissioned: Default::default(), - pool_currency: Default::default(), - }), - )); - assert_noop!( - LiquidityPools::add_currency(RuntimeOrigin::signed(Keyring::Bob.into()), currency_id), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable - ); - - // Switch transferability from XCM to None - assert_ok!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - None, - Some(CustomMetadata { - // Changed: Disallow cross chain transferability entirely - transferability: CrossChainTransferability::None, - mintable: Default::default(), - permissioned: Default::default(), - pool_currency: Default::default(), - }) - )); - assert_noop!( - LiquidityPools::add_currency(RuntimeOrigin::signed(Keyring::Bob.into()), currency_id), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable - ); - }); -} - -#[tokio::test] -async fn allow_investment_currency() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let currency_id = AUSD_CURRENCY_ID; - let pool_id = DEFAULT_POOL_ID; - let evm_chain_id: u64 = MOONBEAM_EVM_CHAIN_ID; - let evm_address = [1u8; 20]; - - // Create an AUSD pool - create_ausd_pool(pool_id); - - enable_liquidity_pool_transferability(currency_id); - - // Enable LiquidityPools transferability - assert_ok!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - // Changed: Add location which can be converted to LiquidityPoolsWrappedToken - Some(Some(liquidity_pools_transferable_multilocation( - evm_chain_id, - evm_address, - ))), - Some(CustomMetadata { - // Changed: Allow liquidity_pools transferability - transferability: CrossChainTransferability::LiquidityPools, - mintable: Default::default(), - permissioned: Default::default(), - pool_currency: true, - }) - )); - - assert_ok!(LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - currency_id, - )); - }); -} - -#[tokio::test] -async fn allow_pool_should_fail() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let currency_id = CurrencyId::ForeignAsset(42); - let ausd_currency_id = AUSD_CURRENCY_ID; - - // Should fail if pool does not exist - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - // Tranche id is arbitrary in this case as pool does not exist - [0u8; 16], - currency_id, - ), - pallet_liquidity_pools::Error::::PoolNotFound - ); - - // Register currency_id with pool_currency set to true - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - asset_metadata( - "Test".into(), - "TEST".into(), - 12, - true, - 1_000_000, - None, - Default::default(), - ), - Some(currency_id) - )); - - // Create pool - create_currency_pool(pool_id, currency_id, 10_000 * dollar(12)); - - // Should fail if asset is not payment currency - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - ausd_currency_id, - ), - 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(Keyring::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(), - currency_id, - None, - None, - None, - None, - None, - Some(CustomMetadata { - // Disallow any cross chain transferability - transferability: CrossChainTransferability::None, - mintable: Default::default(), - permissioned: Default::default(), - // Changed: Allow to be usable as pool currency - pool_currency: true, - }), - )); - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - currency_id, - ), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable - ); - - // Should fail if currency does not have any MultiLocation in metadata - assert_ok!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - None, - Some(CustomMetadata { - // Changed: Allow liquidityPools transferability - transferability: CrossChainTransferability::LiquidityPools, - mintable: Default::default(), - permissioned: Default::default(), - // Still allow to be pool currency - pool_currency: true, - }), - )); - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - currency_id, - ), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken - ); - - // Should fail if currency does not have LiquidityPoolsWrappedToken location in - // metadata - assert_ok!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - // Changed: Add some location which cannot be converted to LiquidityPoolsWrappedToken - Some(Some(VersionedMultiLocation::V3(Default::default()))), - // No change for transferability required as it is already allowed for LiquidityPools - None, - )); - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - default_tranche_id(pool_id), - currency_id, - ), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken - ); - - // Create new pool for non foreign asset - // NOTE: Can be removed after merging https://github.com/centrifuge/centrifuge-chain/pull/1343 - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - asset_metadata( - "Acala Dollar".into(), - "AUSD".into(), - 12, - true, - 1_000_000, - None, - Default::default() - ), - Some(CurrencyId::AUSD) - )); - create_currency_pool(pool_id + 1, CurrencyId::AUSD, 10_000 * dollar(12)); - // Should fail if currency is not foreign asset - assert_noop!( - LiquidityPools::allow_investment_currency( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id + 1, - // Tranche id is arbitrary in this case, so we don't need to check for the exact - // pool_id - default_tranche_id(pool_id + 1), - CurrencyId::AUSD, - ), - DispatchError::Token(sp_runtime::TokenError::Unsupported) - ); - }); -} - -#[tokio::test] -async fn schedule_upgrade() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // Only Root can call `schedule_upgrade` - assert_noop!( - LiquidityPools::schedule_upgrade( - RuntimeOrigin::signed(Keyring::Bob.into()), - MOONBEAM_EVM_CHAIN_ID, - [7; 20] - ), - BadOrigin - ); - - // Now it finally works - assert_ok!(LiquidityPools::schedule_upgrade( - RuntimeOrigin::root(), - MOONBEAM_EVM_CHAIN_ID, - [7; 20] - )); - }); -} - -#[tokio::test] -async fn cancel_upgrade_upgrade() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // Only Root can call `cancel_upgrade` - assert_noop!( - LiquidityPools::cancel_upgrade( - RuntimeOrigin::signed(Keyring::Bob.into()), - MOONBEAM_EVM_CHAIN_ID, - [7; 20] - ), - BadOrigin - ); - - // Now it finally works - assert_ok!(LiquidityPools::cancel_upgrade( - RuntimeOrigin::root(), - MOONBEAM_EVM_CHAIN_ID, - [7; 20] - )); - }); -} - -#[tokio::test] -async fn update_tranche_token_metadata() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - // NOTE: Default pool admin is BOB - create_ausd_pool(pool_id); - - // Missing tranche token should throw - let nonexistent_tranche = [71u8; 16]; - assert_noop!( - LiquidityPools::update_tranche_token_metadata( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - nonexistent_tranche, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::TrancheNotFound - ); - let tranche_id = default_tranche_id(pool_id); - - // Should throw if called by anything but `PoolAdmin` - assert_noop!( - LiquidityPools::update_tranche_token_metadata( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::NotPoolAdmin - ); - - assert_ok!(LiquidityPools::update_tranche_token_metadata( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - )); - - // Edge case: Should throw if tranche exists but metadata does not exist - let tranche_currency_id = CurrencyId::Tranche(pool_id, tranche_id); - orml_asset_registry::Metadata::::remove(tranche_currency_id); - assert_noop!( - LiquidityPools::update_tranche_token_metadata( - RuntimeOrigin::signed(Keyring::Bob.into()), - pool_id, - tranche_id, - Domain::EVM(MOONBEAM_EVM_CHAIN_ID), - ), - pallet_liquidity_pools::Error::::TrancheMetadataNotFound - ); - }); -} 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 deleted file mode 100644 index e0b27cdfa5..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs +++ /dev/null @@ -1,4314 +0,0 @@ -// Copyright 2021 Centrifuge GmbH (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. - -// Copyright 2021 Centrifuge GmbH (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::{currency_decimals, parachains, AccountId, Balance, PoolId, TrancheId, CFG}; -use cfg_traits::{ - investments::{Investment, OrderManager, TrancheCurrency as TrancheCurrencyT}, - liquidity_pools::InboundQueue, - IdentityCurrencyConversion, PoolInspect, -}; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - fixed_point::Ratio, - investments::{CollectedAmount, InvestCollection, InvestmentAccount, RedeemCollection, Swap}, - orders::FulfillmentWithPrice, - permissions::{PermissionScope, PoolRole, Role, UNION}, - pools::TrancheMetadata, - tokens::{ - CrossChainTransferability, CurrencyId, CurrencyId::ForeignAsset, CustomMetadata, - ForeignAssetId, TrancheCurrency, - }, -}; -use frame_support::{ - assert_noop, assert_ok, - traits::{ - fungibles::{Inspect, Mutate}, - Get, PalletInfo, - }, -}; -use fudge::primitives::Chain; -use orml_traits::{asset_registry::AssetMetadata, FixedConversionRateProvider, MultiCurrency}; -use pallet_foreign_investments::{ - types::{InvestState, RedeemState}, - CollectedInvestment, CollectedRedemption, InvestmentPaymentCurrency, InvestmentState, - RedemptionPayoutCurrency, RedemptionState, -}; -use pallet_investments::CollectOutcome; -use runtime_common::{ - account_conversion::AccountConverter, foreign_investments::IdentityPoolCurrencyConverter, -}; -use sp_runtime::{ - traits::{AccountIdConversion, BadOrigin, ConstU32, Convert, EnsureAdd, One, Zero}, - BoundedVec, DispatchError, FixedPointNumber, Perquintill, SaturatedConversion, Storage, - WeakBoundedVec, -}; -use tokio::runtime::Handle; - -use crate::{ - chain::centrifuge::{ - Balances, ForeignInvestments, Investments, LiquidityPools, LocationToAccountId, - MinFulfillmentAmountNative, OrmlAssetRegistry, Permissions, PoolSystem, - Runtime as DevelopmentRuntime, RuntimeOrigin, System, Tokens, TreasuryAccount, PARA_ID, - }, - liquidity_pools::pallet::development::{ - setup::dollar, - tests::liquidity_pools::{ - foreign_investments::setup::{ - do_initial_increase_investment, do_initial_increase_redemption, - ensure_executed_collect_redeem_not_dispatched, min_fulfillment_amount, - }, - setup::{ - asset_metadata, create_ausd_pool, create_currency_pool, - enable_liquidity_pool_transferability, - investments::{ - default_investment_account, default_investment_id, default_tranche_id, - general_currency_index, investment_id, - }, - setup_test_env, LiquidityPoolMessage, DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - DEFAULT_POOL_ID, DEFAULT_VALIDITY, DOMAIN_MOONBEAM, - }, - }, - }, - utils::{accounts::Keyring, env, genesis, AUSD_CURRENCY_ID, AUSD_ED}, -}; - -mod same_currencies { - use pallet_foreign_investments::errors::InvestError; - - use super::*; - - #[tokio::test] - async fn increase_invest_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - - // Create new pool - 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, false); - - // Verify the order was updated to the amount - assert_eq!( - pallet_investments::Pallet::::acc_active_invest_order( - default_investment_id(), - ) - .amount, - amount - ); - - // Increasing again should just bump invest_amount - let msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount, - }; - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - assert_eq!( - InvestmentState::::get(&investor, default_investment_id()), - InvestState::InvestmentOngoing { - invest_amount: amount * 2 - } - ); - }); - } - - #[tokio::test] - async fn decrease_invest_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let invest_amount: u128 = 10 * dollar(12); - let decrease_amount = invest_amount / 3; - let final_amount = invest_amount - decrease_amount; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id: CurrencyId = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - - // Create new pool - 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, - false, - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: decrease_amount, - }; - - // 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 - ); - enable_liquidity_pool_transferability(currency_id); - - // Execute byte message - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Verify investment was decreased into investment account - assert_eq!( - Tokens::balance(currency_id, &default_investment_account()), - final_amount - ); - // Since the investment was done in the pool currency, the decrement happens - // synchronously and thus it must be burned from investor's holdings - assert_eq!(Tokens::balance(currency_id, &investor), 0); - assert!(System::events().iter().any(|e| e.event - == pallet_investments::Event::::InvestOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: final_amount - } - .into())); - assert!(System::events().iter().any(|e| e.event - == orml_tokens::Event::::Withdrawn { - currency_id, - who: investor.clone(), - amount: decrease_amount - } - .into())); - assert_eq!( - pallet_investments::Pallet::::acc_active_invest_order( - default_investment_id(), - ) - .amount, - final_amount - ); - }); - } - - #[tokio::test] - async fn cancel_invest_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let invest_amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - - // Create new pool - 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, - false, - ); - - // Verify investment account holds funds before cancelling - assert_eq!( - Tokens::balance(currency_id, &default_investment_account()), - invest_amount - ); - - // Mock incoming cancel message - let msg = LiquidityPoolMessage::CancelInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - }; - - // 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 - ); - enable_liquidity_pool_transferability(currency_id); - - // Execute byte message - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Foreign InvestmentState should be cleared - assert!(!pallet_foreign_investments::InvestmentState::< - DevelopmentRuntime, - >::contains_key(&investor, default_investment_id())); - assert!(System::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!( - Tokens::balance(currency_id, &default_investment_account()), - 0 - ); - // Since the investment was done in the pool currency, the decrement happens - // synchronously and thus it must be burned from investor's holdings - assert_eq!(Tokens::balance(currency_id, &investor), 0); - assert!(System::events().iter().any(|e| e.event - == pallet_investments::Event::::InvestOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: 0 - } - .into())); - assert!(System::events().iter().any(|e| e.event - == orml_tokens::Event::::Withdrawn { - currency_id, - who: investor.clone(), - amount: invest_amount - } - .into())); - assert_eq!( - pallet_investments::Pallet::::acc_active_invest_order( - default_investment_id(), - ) - .amount, - 0 - ); - }); - } - - #[tokio::test] - async fn collect_invest_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - 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, false); - let events_before_collect = System::events(); - - // Process and fulfill order - // NOTE: Without this step, the order id is not cleared and - // `Event::InvestCollectedForNonClearedOrderId` be dispatched - assert_ok!(Investments::process_invest_orders(default_investment_id())); - - // Tranche tokens will be minted upon fulfillment - assert_eq!(Tokens::total_issuance(investment_currency_id), 0); - assert_ok!(Investments::invest_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::one(), - price: Ratio::one(), - } - )); - assert_eq!(Tokens::total_issuance(investment_currency_id), amount); - - // Mock collection message msg - 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)); - - // Remove events before collect execution - let events_since_collect: Vec<_> = System::events() - .into_iter() - .filter(|e| !events_before_collect.contains(e)) - .collect(); - - // Verify investment was transferred to the domain locator - assert_eq!( - Tokens::balance(default_investment_id().into(), &sending_domain_locator), - amount - ); - - // Order should have been cleared by fulfilling investment - assert_eq!( - pallet_investments::Pallet::::acc_active_invest_order( - default_investment_id(), - ) - .amount, - 0 - ); - assert!(!events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::InvestCollectedForNonClearedOrderId { - investment_id: default_investment_id(), - who: investor.clone(), - } - .into() - })); - - // Order should not have been updated since everything is collected - assert!(!events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::InvestOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: 0, - } - .into() - })); - - // Order should have been fully collected - assert!(events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::InvestOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![0], - who: investor.clone(), - collection: InvestCollection:: { - payout_investment_invest: amount, - remaining_investment_invest: 0, - }, - outcome: CollectOutcome::FullyCollected, - } - .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!(System::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::ExecutedCollectInvest { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - currency_payout: amount, - tranche_tokens_payout: amount, - remaining_invest_amount: 0, - }, - } - .into() - })); - }); - } - - #[tokio::test] - async fn partially_collect_investment_for_through_investments() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let invest_amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - 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, - false, - ); - enable_liquidity_pool_transferability(currency_id); - let investment_currency_id: CurrencyId = default_investment_id().into(); - - assert!(!Investments::investment_requires_collect( - &investor, - default_investment_id() - )); - - // Process 50% of investment at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(Investments::process_invest_orders(default_investment_id())); - assert_ok!(Investments::invest_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - - // Pre collect assertions - assert!(Investments::investment_requires_collect( - &investor, - 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 - assert_ok!(Investments::collect_investments_for( - RuntimeOrigin::signed(Keyring::Alice.into()), - investor.clone(), - 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 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!(Tokens::balance(investment_currency_id, &investor), 0); - assert_eq!( - Tokens::balance(investment_currency_id, &sending_domain_locator), - invest_amount * 2 - ); - assert!(System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::InvestOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![0], - who: investor.clone(), - collection: InvestCollection:: { - payout_investment_invest: invest_amount * 2, - remaining_investment_invest: invest_amount / 2, - }, - outcome: CollectOutcome::FullyCollected, - } - .into() - })); - assert!(System::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::ExecutedCollectInvest { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - currency_payout: invest_amount / 2, - tranche_tokens_payout: invest_amount * 2, - remaining_invest_amount: invest_amount / 2, - }, - } - .into() - })); - - // Process rest 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: Ratio::checked_from_rational(1, 2).unwrap(), - } - )); - // Order should have been cleared by fulfilling investment - assert_eq!( - pallet_investments::Pallet::::acc_active_invest_order( - default_investment_id(), - ) - .amount, - 0 - ); - assert_eq!( - Tokens::total_issuance(investment_currency_id), - invest_amount * 3 - ); - - // Collect remainder through Investments - assert_ok!(Investments::collect_investments_for( - RuntimeOrigin::signed(Keyring::Alice.into()), - investor.clone(), - default_investment_id() - )); - assert!(!Investments::investment_requires_collect( - &investor, - 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; - assert_eq!( - 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), - amount_tranche_tokens - ); - assert!(!System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::InvestCollectedForNonClearedOrderId { - investment_id: default_investment_id(), - who: investor.clone(), - } - .into() - })); - assert!(System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::InvestOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![1], - who: investor.clone(), - collection: InvestCollection:: { - payout_investment_invest: invest_amount, - remaining_investment_invest: 0, - }, - outcome: CollectOutcome::FullyCollected, - } - .into() - })); - assert!(System::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::ExecutedCollectInvest { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - currency_payout: invest_amount / 2, - tranche_tokens_payout: invest_amount, - remaining_invest_amount: 0, - }, - } - .into() - })); - // Clearing of foreign InvestState should have been dispatched exactly once - assert_eq!( - System::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 - let msg = LiquidityPoolMessage::CollectInvest { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg), - pallet_foreign_investments::Error::::InvestmentPaymentCurrencyNotFound - ); - }); - } - - #[tokio::test] - async fn increase_redeem_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - - // Create new pool - create_currency_pool(pool_id, currency_id, currency_decimals.into()); - - // Set permissions and execute initial redemption - do_initial_increase_redemption(pool_id, amount, investor.clone(), currency_id); - - // Verify amount was noted in the corresponding order - assert_eq!( - pallet_investments::Pallet::::acc_active_redeem_order( - default_investment_id(), - ) - .amount, - amount - ); - - // Increasing again should just bump redeeming amount - assert_ok!(Tokens::mint_into( - default_investment_id().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - amount - )); - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount, - }; - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - assert_eq!( - RedemptionState::::get(&investor, default_investment_id()), - RedeemState::Redeeming { - redeem_amount: amount * 2, - } - ); - }); - } - - #[tokio::test] - async fn decrease_redeem_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let redeem_amount = 10 * dollar(12); - let decrease_amount = redeem_amount / 3; - let final_amount = redeem_amount - decrease_amount; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); - - // Create new pool - create_currency_pool(pool_id, currency_id, currency_decimals.into()); - - // Set permissions and execute initial redemption - do_initial_increase_redemption(pool_id, redeem_amount, investor.clone(), currency_id); - - // Verify the corresponding redemption order id is 0 - assert_eq!( - pallet_investments::Pallet::::invest_order_id(investment_id( - pool_id, - default_tranche_id(pool_id) - )), - 0 - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::DecreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: decrease_amount, - }; - - // Execute byte message - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Verify investment was decreased into investment account - assert_eq!( - Tokens::balance( - default_investment_id().into(), - &default_investment_account(), - ), - final_amount - ); - // Tokens should have been transferred from investor's wallet to domain's - // sovereign account - assert_eq!( - Tokens::balance(default_investment_id().into(), &investor), - 0 - ); - assert_eq!( - Tokens::balance(default_investment_id().into(), &sending_domain_locator), - decrease_amount - ); - - // Foreign RedemptionState should be updated - assert!(System::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!(System::events().iter().any(|e| e.event - == pallet_investments::Event::::RedeemOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: final_amount - } - .into())); - assert_eq!( - pallet_investments::Pallet::::acc_active_redeem_order( - default_investment_id(), - ) - .amount, - final_amount - ); - - assert!(System::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::ExecutedDecreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - tranche_tokens_payout: decrease_amount, - remaining_redeem_amount: final_amount, - }, - } - .into() - })); - }); - } - - #[tokio::test] - async fn cancel_redeem_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let redeem_amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); - - // Create new pool - create_currency_pool(pool_id, currency_id, currency_decimals.into()); - - // Set permissions and execute initial redemption - do_initial_increase_redemption(pool_id, redeem_amount, investor.clone(), currency_id); - - // Verify the corresponding redemption order id is 0 - assert_eq!( - pallet_investments::Pallet::::invest_order_id(investment_id( - pool_id, - default_tranche_id(pool_id) - )), - 0 - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::CancelRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - }; - - // Execute byte message - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Verify investment was decreased into investment account - assert_eq!( - Tokens::balance( - default_investment_id().into(), - &default_investment_account(), - ), - 0 - ); - // Tokens should have been transferred from investor's wallet to domain's - // sovereign account - assert_eq!( - Tokens::balance(default_investment_id().into(), &investor), - 0 - ); - assert_eq!( - 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| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionCleared { - investor: investor.clone(), - investment_id: default_investment_id(), - } - .into() - })); - - // Order should have been updated - assert!(System::events().iter().any(|e| e.event - == pallet_investments::Event::::RedeemOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: 0 - } - .into())); - assert_eq!( - pallet_investments::Pallet::::acc_active_redeem_order( - default_investment_id(), - ) - .amount, - 0 - ); - }); - } - - #[tokio::test] - async fn fully_collect_redeem_order() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let pool_account = - pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); - - // Create new pool - create_currency_pool(pool_id, currency_id, currency_decimals.into()); - - // Set permissions and execute initial investment - do_initial_increase_redemption(pool_id, amount, investor.clone(), currency_id); - let events_before_collect = System::events(); - - // Fund the pool account with sufficient pool currency, else redemption cannot - // swap tranche tokens against pool currency - assert_ok!(Tokens::mint_into(currency_id, &pool_account, amount)); - - // Process and fulfill order - // NOTE: Without this step, the order id is not cleared and - // `Event::RedeemCollectedForNonClearedOrderId` be dispatched - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::one(), - price: Ratio::one(), - } - )); - - // Enable liquidity pool transferability - enable_liquidity_pool_transferability(currency_id); - - // Mock collection message msg - let msg = LiquidityPoolMessage::CollectRedeem { - pool_id, - tranche_id: default_tranche_id(pool_id), - 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 - let events_since_collect: Vec<_> = System::events() - .into_iter() - .filter(|e| !events_before_collect.contains(e)) - .collect(); - - // Verify collected redemption was burned from investor - assert_eq!(Tokens::balance(currency_id, &investor), 0); - assert!(System::events().iter().any(|e| e.event - == orml_tokens::Event::::Withdrawn { - currency_id, - who: investor.clone(), - amount - } - .into())); - - // Order should have been cleared by fulfilling redemption - assert_eq!( - pallet_investments::Pallet::::acc_active_redeem_order( - default_investment_id(), - ) - .amount, - 0 - ); - assert!(!events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemCollectedForNonClearedOrderId { - investment_id: default_investment_id(), - who: investor.clone(), - } - .into() - })); - - // Order should not have been updated since everything is collected - assert!(!events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: 0, - } - .into() - })); - - // Order should have been fully collected - assert!(events_since_collect.iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![0], - who: investor.clone(), - collection: RedeemCollection:: { - payout_investment_redeem: amount, - remaining_investment_redeem: 0, - }, - outcome: CollectOutcome::FullyCollected, - } - .into() - })); - - // Foreign CollectedRedemptionTrancheTokens should be killed - assert!(!pallet_foreign_investments::CollectedRedemption::< - DevelopmentRuntime, - >::contains_key(investor.clone(), default_investment_id(),)); - - // Foreign RedemptionState should be killed - assert!(!pallet_foreign_investments::RedemptionState::< - DevelopmentRuntime, - >::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!(System::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(currency_id), - currency_payout: amount, - tranche_tokens_payout: amount, - remaining_redeem_amount: 0, - }, - } - .into() - })); - }); - } - - #[tokio::test] - async fn partially_collect_redemption_for_through_investments() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let redeem_amount = 10 * dollar(12); - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.into(), - )); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let pool_account = - pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); - create_currency_pool(pool_id, currency_id, currency_decimals.into()); - do_initial_increase_redemption(pool_id, redeem_amount, investor.clone(), currency_id); - enable_liquidity_pool_transferability(currency_id); - - // Fund the pool account with sufficient pool currency, else redemption cannot - // swap tranche tokens against pool currency - assert_ok!(Tokens::mint_into(currency_id, &pool_account, redeem_amount)); - assert!(!Investments::redemption_requires_collect( - &investor, - default_investment_id() - )); - - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - - // Pre collect assertions - assert!(Investments::redemption_requires_collect( - &investor, - 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!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Alice.into()), - investor.clone(), - default_investment_id() - )); - assert!(System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![0], - who: investor.clone(), - collection: RedeemCollection:: { - payout_investment_redeem: redeem_amount / 8, - remaining_investment_redeem: redeem_amount / 2, - }, - outcome: CollectOutcome::FullyCollected, - } - .into() - })); - assert!(System::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(currency_id), - currency_payout: redeem_amount / 8, - tranche_tokens_payout: redeem_amount / 2, - remaining_redeem_amount: redeem_amount / 2, - }, - } - .into() - })); - assert!(!Investments::redemption_requires_collect( - &investor, - default_investment_id() - )); - // 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!(System::events().iter().any(|e| e.event - == orml_tokens::Event::::Withdrawn { - currency_id, - who: investor.clone(), - amount: redeem_amount / 8 - } - .into())); - - // Process rest of redemption at 50% rate - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::one(), - price: Ratio::checked_from_rational(1, 2).unwrap(), - } - )); - // Order should have been cleared by fulfilling redemption - assert_eq!( - pallet_investments::Pallet::::acc_active_redeem_order( - default_investment_id(), - ) - .amount, - 0 - ); - - // Collect remainder through Investments - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Alice.into()), - investor.clone(), - default_investment_id() - )); - assert!(!Investments::redemption_requires_collect( - &investor, - default_investment_id() - )); - assert!(!CollectedRedemption::::contains_key( - &investor, - default_investment_id() - )); - assert!(!RedemptionState::::contains_key( - &investor, - default_investment_id() - )); - assert!(!System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemCollectedForNonClearedOrderId { - investment_id: default_investment_id(), - who: investor.clone(), - } - .into() - })); - assert!(System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::RedeemOrdersCollected { - investment_id: default_investment_id(), - processed_orders: vec![1], - who: investor.clone(), - collection: RedeemCollection:: { - payout_investment_redeem: redeem_amount / 4, - remaining_investment_redeem: 0, - }, - outcome: CollectOutcome::FullyCollected, - } - .into() - })); - // Verify collected redemption was burned from investor - assert_eq!(Tokens::balance(currency_id, &investor), 0); - assert!(System::events().iter().any(|e| e.event - == orml_tokens::Event::::Withdrawn { - currency_id, - who: investor.clone(), - amount: redeem_amount / 4 - } - .into())); - // Clearing of foreign RedeemState should have been dispatched exactly once - assert_eq!( - System::events() - .iter() - .filter(|e| { - e.event - == pallet_foreign_investments::Event::::ForeignRedemptionCleared { - investor: investor.clone(), - investment_id: default_investment_id(), - } - .into() - }) - .count(), - 1 - ); - assert!(System::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(currency_id), - currency_payout: redeem_amount / 4, - tranche_tokens_payout: redeem_amount / 2, - remaining_redeem_amount: 0, - }, - } - .into() - })); - }); - } - - mod should_fail { - use pallet_foreign_investments::errors::{InvestError, RedeemError}; - - use super::*; - - mod decrease_should_underflow { - use super::*; - - #[tokio::test] - async fn invest_decrease_underflow() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let invest_amount: u128 = 10 * dollar(12); - let decrease_amount = invest_amount + 1; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::convert((DOMAIN_MOONBEAM, Keyring::Bob.into())); - 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, - invest_amount, - investor.clone(), - currency_id, - false, - ); - enable_liquidity_pool_transferability(currency_id); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: decrease_amount, - }; - - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg), - pallet_foreign_investments::Error::::InvestError( - InvestError::DecreaseAmountOverflow - ) - ); - }); - } - - #[tokio::test] - async fn redeem_decrease_underflow() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let redeem_amount: u128 = 10 * dollar(12); - let decrease_amount = redeem_amount + 1; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::convert((DOMAIN_MOONBEAM, Keyring::Bob.into())); - 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_redemption( - pool_id, - redeem_amount, - investor.clone(), - currency_id, - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::DecreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: decrease_amount, - }; - - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::DecreaseTransition - ) - ); - }); - } - } - - mod should_throw_requires_collect { - use super::*; - - #[tokio::test] - async fn invest_requires_collect() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount: u128 = 10 * dollar(12); - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::convert((DOMAIN_MOONBEAM, Keyring::Bob.into())); - 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, - false, - ); - enable_liquidity_pool_transferability(currency_id); - - // Prepare collection - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); - assert_ok!(Tokens::mint_into(currency_id, &pool_account, amount)); - assert_ok!(Investments::process_invest_orders(default_investment_id())); - assert_ok!(Investments::invest_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::one(), - price: Ratio::one(), - } - )); - - // Should fail to increase - let increase_msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: AUSD_ED, - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg), - pallet_foreign_investments::Error::::InvestError( - InvestError::CollectRequired - ) - ); - - // Should fail to decrease - let decrease_msg = LiquidityPoolMessage::DecreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: 1, - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), - pallet_foreign_investments::Error::::InvestError( - InvestError::CollectRequired - ) - ); - }); - } - - #[tokio::test] - async fn redeem_requires_collect() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let amount: u128 = 10 * dollar(12); - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::convert((DOMAIN_MOONBEAM, Keyring::Bob.into())); - 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_redemption(pool_id, amount, investor.clone(), currency_id); - enable_liquidity_pool_transferability(currency_id); - - // Mint more into DomainLocator required for subsequent invest attempt - assert_ok!(Tokens::mint_into( - default_investment_id().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - 1, - )); - - // Prepare collection - let pool_account = pallet_pool_system::pool_types::PoolLocator { pool_id } - .into_account_truncating(); - assert_ok!(Tokens::mint_into(currency_id, &pool_account, amount)); - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::one(), - price: Ratio::one(), - } - )); - - // Should fail to increase - let increase_msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: 1, - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, increase_msg), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::CollectRequired - ) - ); - - // Should fail to decrease - let decrease_msg = LiquidityPoolMessage::DecreaseRedeemOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount: 1, - }; - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, decrease_msg), - pallet_foreign_investments::Error::::RedeemError( - RedeemError::CollectRequired - ) - ); - }); - } - } - - 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, - }; - - #[tokio::test] - async fn invalid_invest_payment_currency() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::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 * dollar(18); - - 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, 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: AUSD_ED, - }; - 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 - ) - ); - }); - } - - #[tokio::test] - async fn invalid_redeem_payout_currency() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::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 * 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, - )); - - // 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 - ) - ); - }); - } - - #[tokio::test] - async fn invest_payment_currency_not_found() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::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 * 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 - ); - }); - } - - #[tokio::test] - async fn redeem_payout_currency_not_found() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = AccountConverter::< - DevelopmentRuntime, - LocationToAccountId, - >::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 * 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::OrderBook; - use pallet_foreign_investments::{types::TokenSwapReason, InvestmentState}; - - use super::*; - use crate::{ - liquidity_pools::pallet::development::tests::{ - liquidity_pools::foreign_investments::setup::enable_usdt_trading, register_usdt, - }, - utils::{GLMR_CURRENCY_ID, USDT_CURRENCY_ID}, - }; - - #[tokio::test] - async fn collect_foreign_investment_for() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.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 = 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: invest_amount_foreign_denominated, - }; - 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: Ratio::checked_from_rational(1, 2).unwrap(), - } - )); - assert_ok!(Investments::collect_investments_for( - RuntimeOrigin::signed(Keyring::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 - ); - assert!(System::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::ExecutedCollectInvest { - 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, - tranche_tokens_payout: invest_amount_pool_denominated * 2, - remaining_invest_amount: 0, - }, - } - .into() - })); - - // 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. - #[tokio::test] - async fn invest_increase_decrease() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - let pool_id = DEFAULT_POOL_ID; - let investor: AccountId = - AccountConverter::::convert(( - DOMAIN_MOONBEAM, - Keyring::Bob.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 = 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, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(foreign_currency), - amount: invest_amount_foreign_denominated, - }; - assert_ok!(LiquidityPools::submit( - 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!(System::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); - let decrease_msg_partial_invest_amount = 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!(LiquidityPools::submit( - 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!(System::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!(LiquidityPools::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!(System::events().iter().any(|e| { - e.event == - pallet_foreign_investments::Event::::ForeignInvestmentUpdated - { investor: investor.clone(), - investment_id: default_investment_id(), - state: expected_state.clone() - } - .into() - })); - }); - } - - /// Propagate swaps only via OrderBook fulfillments. - /// - /// Flow: Increase, fulfill, decrease, fulfill - #[tokio::test] - async fn invest_swaps_happy_path() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - 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 * dollar(18); - 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!(Tokens::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 = - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .expect("Swap order id created during increase"); - assert_eq!( - ForeignInvestments::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!(OrderBook::fill_order_full( - RuntimeOrigin::signed(trader.clone()), - swap_order_id - )); - assert!(System::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!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(ForeignInvestments::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!(LiquidityPools::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 = - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .expect("Swap order id created during decrease"); - assert_eq!( - ForeignInvestments::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!(OrderBook::fill_order_full( - RuntimeOrigin::signed(trader.clone()), - swap_order_id - )); - assert!(System::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!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - assert!(System::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::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 - #[tokio::test] - async fn concurrent_swap_orders_same_direction() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - 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 * 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, - ); - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = - pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); - assert_ok!(Tokens::mint_into( - pool_currency, - &pool_account, - invest_amount_pool_denominated - )); - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.into()), - investor.clone(), - default_investment_id() - )); - assert_eq!( - ForeignInvestments::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!(System::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!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.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!(System::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!(OrderBook::fill_order_full( - RuntimeOrigin::signed(trader.clone()), - swap_order_id - )); - assert!(System::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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - assert!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(System::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() - })); - }); - } - - /// 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 - #[tokio::test] - async fn concurrent_swap_orders_opposite_direction() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - 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 * 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, - || {}, - ); - assert_ok!(Tokens::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!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()), - Some(swap_order_id) - ); - - // Redeem setup: Increase and process - assert_ok!(Tokens::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!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - assert_eq!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - ForeignInvestments::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!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.into()), - investor.clone(), - default_investment_id() - )); - assert_eq!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_eq!( - ForeignInvestments::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!(System::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!(System::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!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.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!(System::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!(System::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: 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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - // Process remaining redemption at 200% rate, i.e. 1 tranche token = 2 pool - // currency - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(2, 1).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.into()), - investor.clone(), - default_investment_id() - )); - assert!(ForeignInvestments::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!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()), - Some(swap_order_id) - ); - assert_eq!( - ForeignInvestments::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!(OrderBook::fill_order_full( - RuntimeOrigin::signed(trader.clone()), - swap_order_id - )); - assert!(System::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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - assert!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(System::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 * 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 - #[tokio::test] - async fn fulfill_invest_swap_order_requires_collect() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - 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 * 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, - ); - assert_ok!(Tokens::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!(LiquidityPools::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!(Investments::process_invest_orders(default_investment_id())); - assert_ok!(Investments::invest_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert!(Investments::investment_requires_collect( - &investor, - default_investment_id() - )); - - // Fulfill swap order should implicitly collect, otherwise the unprocessed - // investment amount is unknown - assert_ok!(OrderBook::fill_order_full( - RuntimeOrigin::signed(trader.clone()), - swap_order_id - )); - assert!(!Investments::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 - #[tokio::test] - async fn fulfill_redeem_swap_order_requires_collect() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - 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 * 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, - ); - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = - pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); - assert_ok!(Tokens::mint_into( - pool_currency, - &pool_account, - invest_amount_pool_denominated - )); - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.into()), - investor.clone(), - default_investment_id() - )); - assert_eq!( - ForeignInvestments::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!(System::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!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.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!(System::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - swap_order_id, - invest_amount_foreign_denominated / 2 - )); - assert!(System::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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - assert!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(System::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 - #[tokio::test] - async fn partial_fulfillment_concurrent_swap_orders_same_direction() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - setup_test_env(&mut env); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // 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 * 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, - ); - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - - // Redeem setup: Increase and process - assert_ok!(Tokens::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!(LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - let pool_account = - pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); - assert_ok!(Tokens::mint_into( - pool_currency, - &pool_account, - invest_amount_pool_denominated - )); - assert_ok!(Investments::process_redeem_orders(default_investment_id())); - // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche - // tokens - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(50), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_eq!( - ForeignInvestments::foreign_investment_info(swap_order_id) - .unwrap() - .last_swap_reason - .unwrap(), - TokenSwapReason::Investment - ); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.into()), - investor.clone(), - default_investment_id() - )); - assert_eq!( - ForeignInvestments::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!(System::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!(Investments::process_redeem_orders(default_investment_id())); - assert_ok!(Investments::redeem_fulfillment( - default_investment_id(), - FulfillmentWithPrice { - of_amount: Perquintill::from_percent(100), - price: Ratio::checked_from_rational(1, 4).unwrap(), - } - )); - assert_ok!(Investments::collect_redemptions_for( - RuntimeOrigin::signed(Keyring::Charlie.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!(System::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - swap_order_id, - invest_amount_foreign_denominated / 2 - )); - assert!(System::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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); - assert!( - ForeignInvestments::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!(OrderBook::fill_order_partial( - RuntimeOrigin::signed(trader.clone()), - 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!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - assert!( - ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) - .is_none() - ); - assert!(System::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() - })); - }); - } -} - -mod setup { - use cfg_traits::{investments::ForeignInvestment, ConversionToAssetBalance}; - use development_runtime::OrderBook; - - use super::*; - 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 - /// `IncreaseInvestOrder`. - /// - /// Assumes `setup_pre_requirements` and - /// `investments::create_currency_pool` to have been called - /// beforehand - pub fn do_initial_increase_investment( - pool_id: u64, - amount: Balance, - investor: AccountId, - currency_id: CurrencyId, - clear_investment_payment_currency: bool, - ) { - let valid_until = DEFAULT_VALIDITY; - let pool_currency: CurrencyId = - PoolSystem::currency_for(pool_id).expect("Pool existence checked already"); - - // Mock incoming increase invest message - let msg = LiquidityPoolMessage::IncreaseInvestOrder { - pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount, - }; - - // Should fail if investor does not have investor role yet - // However, failure is async for foreign currencies as part of updating the - // investment after the swap was fulfilled - if currency_id == pool_currency { - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg.clone()), - DispatchError::Other("Account does not have the TrancheInvestor permission.") - ); - } - - // Make investor the MembersListAdmin of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - investor.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id(pool_id), - valid_until - )), - )); - - let amount_before = Tokens::balance(currency_id, &default_investment_account()); - let final_amount = amount_before - .ensure_add(amount) - .expect("Should not overflow when incrementing amount"); - - // 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!( - InvestmentState::::get(&investor, default_investment_id()), - InvestState::InvestmentOngoing { - invest_amount: amount - } - ); - // Verify investment was transferred into investment account - assert_eq!( - Tokens::balance(currency_id, &default_investment_account()), - final_amount - ); - assert!(System::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!(System::events().iter().any(|e| { - e.event - == pallet_investments::Event::::InvestOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor.clone(), - amount: final_amount, - } - .into() - })); - } else { - let amount_pool_denominated: u128 = - IdentityPoolCurrencyConverter::::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(), - ); - } - } - - /// Sets up required permissions for the investor and executes an - /// initial redemption via LiquidityPools by executing - /// `IncreaseRedeemOrder`. - /// - /// Assumes `setup_pre_requirements` and - /// `investments::create_currency_pool` to have been called - /// beforehand. - /// - /// NOTE: Mints exactly the redeeming amount of tranche tokens. - pub fn do_initial_increase_redemption( - pool_id: u64, - amount: Balance, - 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!(Tokens::mint_into( - default_investment_id().into(), - &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), - amount - )); - - // Verify redemption has not been made yet - assert_eq!( - Tokens::balance( - default_investment_id().into(), - &default_investment_account(), - ), - 0 - ); - assert_eq!( - Tokens::balance(default_investment_id().into(), &investor), - 0 - ); - - // Mock incoming increase invest message - let msg = LiquidityPoolMessage::IncreaseRedeemOrder { - pool_id: pool_id, - tranche_id: default_tranche_id(pool_id), - investor: investor.clone().into(), - currency: general_currency_index(currency_id), - amount, - }; - - // Should fail if investor does not have investor role yet - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg.clone()), - DispatchError::Other("Account does not have the TrancheInvestor permission.") - ); - - // Make investor the MembersListAdmin of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - investor.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id(pool_id), - valid_until - )), - )); - - assert_ok!(LiquidityPools::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!( - Tokens::balance( - default_investment_id().into(), - &default_investment_account(), - ), - amount - ); - assert_eq!( - Tokens::balance(default_investment_id().into(), &investor), - 0 - ); - assert_eq!( - Tokens::balance( - default_investment_id().into(), - &AccountConverter::::convert( - DEFAULT_OTHER_DOMAIN_ADDRESS - ) - ), - 0 - ); - assert_eq!( - System::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!( - System::events().iter().last().unwrap().event, - pallet_investments::Event::::RedeemOrderUpdated { - investment_id: default_investment_id(), - submitted_at: 0, - who: investor, - amount - } - .into() - ); - - // Verify order id is 0 - assert_eq!( - pallet_investments::Pallet::::redeem_order_id(investment_id( - pool_id, - default_tranche_id(pool_id) - )), - 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 - } - - pub(crate) fn ensure_executed_collect_redeem_not_dispatched() { - assert!(System::events().iter().any(|e| { - match &e.event { - development_runtime::RuntimeEvent::LiquidityPoolsGateway( - pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { - message, .. - }, - ) => match message { - pallet_liquidity_pools::Message::ExecutedCollectRedeem { .. } => false, - _ => true, - }, - _ => true, - } - })); - } - - pub(crate) fn min_fulfillment_amount(currency_id: CurrencyId) -> Balance { - runtime_common::foreign_investments::NativeBalanceDecimalConverter::::to_asset_balance( - MinFulfillmentAmountNative::get(), - currency_id, - ) - .expect("CurrencyId should be registered in AssetRegistry") - } -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/mod.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/mod.rs index 1bc730757d..a51147aadd 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/mod.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/mod.rs @@ -22,8 +22,6 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -mod add_allow_upgrade; -mod foreign_investments; pub(crate) mod setup; mod transfers;