diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 976a7287f5..e76e4507a9 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -776,7 +776,7 @@ impl pallet_democracy::Config for Runtime { /// Period in blocks where an external proposal may not be re-submitted /// after being vetoed. type CooloffPeriod = CooloffPeriod; - type Currency = Tokens; + type Currency = Balances; /// The minimum period of locking and the period between a proposal being /// approved and enacted. /// diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index b6832c6364..ea86acf24b 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -1,22 +1,71 @@ -use cfg_primitives::{currency_decimals, parachains, AccountId, Balance}; +use cfg_primitives::{ + currency_decimals, parachains, AccountId, Balance, CouncilCollective, PoolId, TrancheId, +}; +use cfg_traits::{ + investments::{ForeignInvestment, Investment, OrderManager, TrancheCurrency}, + liquidity_pools::{Codec, InboundQueue, OutboundQueue}, + ConversionToAssetBalance, IdentityCurrencyConversion, Permissions, PoolInspect, PoolMutate, + Seconds, +}; use cfg_types::{ + domain_address::{Domain, DomainAddress}, + fixed_point::{Quantity, Ratio}, + investments::{ + ForeignInvestmentInfo, InvestCollection, InvestmentAccount, RedeemCollection, Swap, + }, + locations::Location, + 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}, + pallet_prelude::GenesisBuild, + traits::{ + fungible::Mutate as FungibleMutate, + fungibles::{Inspect, Mutate as FungiblesMutate}, + OriginTrait, PalletInfo, + }, +}; +use liquidity_pools_gateway_routers::{ + AxelarEVMRouter, AxelarXCMRouter, DomainRouter, EVMDomain, EVMRouter, EthereumXCMRouter, + FeeValues, XCMRouter, XcmDomain, DEFAULT_PROOF_SIZE, MAX_AXELAR_EVM_CHAIN_SIZE, +}; use orml_traits::{asset_registry::AssetMetadata, MultiCurrency}; -use polkadot_parachain::primitives::Id; +use pallet_foreign_investments::{ + errors::{InvestError, RedeemError}, + types::{InvestState, RedeemState, TokenSwapReason}, + CollectedInvestment, CollectedRedemption, InvestmentPaymentCurrency, InvestmentState, + RedemptionPayoutCurrency, RedemptionState, +}; +use pallet_investments::CollectOutcome; +use pallet_liquidity_pools::Message; +use pallet_liquidity_pools_gateway::{Call as LiquidityPoolsGatewayCall, GatewayOrigin}; +use pallet_pool_system::tranches::{TrancheInput, TrancheLoc, TrancheType}; +use polkadot_core_primitives::BlakeTwo256; +use polkadot_parachain::primitives::{Id, ValidationCode}; +use polkadot_runtime_parachains::{ + paras, + paras::{ParaGenesisArgs, ParaKind}, +}; 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, U256}; use sp_runtime::{ - traits::{AccountIdConversion, BadOrigin, ConstU32, Convert as C2}, - WeakBoundedVec, + traits::{AccountIdConversion, BadOrigin, ConstU32, Convert as C2, EnsureAdd, Hash, One, Zero}, + BoundedVec, DispatchError, FixedPointNumber, Perquintill, SaturatedConversion, WeakBoundedVec, }; use xcm::{ + latest::NetworkId, prelude::XCM_VERSION, v3::{ AssetId, Fungibility, Junction, Junction::*, Junctions, Junctions::*, MultiAsset, @@ -26,126 +75,7098 @@ use xcm::{ }; use xcm_executor::traits::Convert as C1; -use crate::{ - generic::{ - config::Runtime, - env::{Blocks, Env}, - envs::fudge_env::{handle::FudgeHandle, FudgeEnv, FudgeSupport}, - utils::{genesis, genesis::Genesis}, - }, - utils::{accounts::Keyring, AUSD_CURRENCY_ID}, -}; +use crate::{ + generic::{ + config::Runtime, + env::{Blocks, Env}, + envs::fudge_env::{handle::FudgeHandle, FudgeEnv, FudgeSupport}, + utils::{democracy::execute_via_democracy, genesis, genesis::Genesis}, + }, + utils::{accounts::Keyring, AUSD_CURRENCY_ID, AUSD_ED, USDT_CURRENCY_ID, USDT_ED}, +}; + +mod utils { + use super::*; + + pub fn parachain_account(id: u32) -> AccountId { + polkadot_parachain::primitives::Sibling::from(id).into_account_truncating() + } + + pub fn xcm_metadata(transferability: CrossChainTransferability) -> Option { + match transferability { + CrossChainTransferability::Xcm(x) | CrossChainTransferability::All(x) => Some(x), + _ => None, + } + } + + pub fn setup_xcm(env: &mut FudgeEnv) { + env.parachain_state_mut(|| { + // Set the XCM version used when sending XCM messages to sibling. + assert_ok!(pallet_xcm::Pallet::::force_xcm_version( + ::RuntimeOrigin::root(), + Box::new(MultiLocation::new( + 1, + Junctions::X1(Junction::Parachain(T::FudgeHandle::SIBLING_ID)), + )), + XCM_VERSION, + )); + }); + + env.sibling_state_mut(|| { + // Set the XCM version used when sending XCM messages to parachain. + assert_ok!(pallet_xcm::Pallet::::force_xcm_version( + ::RuntimeOrigin::root(), + Box::new(MultiLocation::new( + 1, + Junctions::X1(Junction::Parachain(T::FudgeHandle::PARA_ID)), + )), + 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(T::FudgeHandle::SIBLING_ID), + 10, + 1024, + )); + + assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< + FudgeRelayRuntime, + >::force_open_hrmp_channel( + as frame_system::Config>::RuntimeOrigin::root(), + Id::from(T::FudgeHandle::SIBLING_ID), + Id::from(T::FudgeHandle::PARA_ID), + 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 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 cfg_fee() -> Balance { + fee(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 + } + + pub fn set_domain_router_call( + domain: Domain, + router: DomainRouter, + ) -> T::RuntimeCallExt { + LiquidityPoolsGatewayCall::set_domain_router { domain, router }.into() + } + + pub fn add_instance_call(instance: DomainAddress) -> T::RuntimeCallExt { + LiquidityPoolsGatewayCall::add_instance { instance }.into() + } + + pub fn remove_instance_call(instance: DomainAddress) -> T::RuntimeCallExt { + LiquidityPoolsGatewayCall::remove_instance { instance }.into() + } +} + +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") + } + + pub fn get_council_members() -> Vec { + vec![Keyring::Alice, Keyring::Bob, Keyring::Charlie] + } + } + + 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(), + ); + + 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(), + ); + + 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(), + ); + + 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(), + ); + + 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 + ); + } + } + + mod transfers { + use super::*; + + fn transfer_non_tranche_tokens_from_local() { + 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 initial_balance = 2 * AUSD_ED; + let amount = initial_balance / 2; + let dest_address = DEFAULT_DOMAIN_ADDRESS_MOONBEAM; + let currency_id = AUSD_CURRENCY_ID; + let source_account = Keyring::Charlie; + + // Mint sufficient balance + assert_eq!( + orml_tokens::Pallet::::free_balance(currency_id, &source_account.into()), + 0 + ); + assert_ok!(orml_tokens::Pallet::::mint_into( + currency_id, + &source_account.into(), + initial_balance + )); + assert_eq!( + orml_tokens::Pallet::::free_balance(currency_id, &source_account.into()), + initial_balance + ); + + // Only `ForeignAsset` can be transferred + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + CurrencyId::Tranche(42u64, [0u8; 16]), + dest_address.clone(), + amount, + ), + pallet_liquidity_pools::Error::::InvalidTransferCurrency + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards), + dest_address.clone(), + amount, + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + CurrencyId::Native, + dest_address.clone(), + amount, + ), + pallet_liquidity_pools::Error::::AssetNotFound + ); + + // Cannot transfer as long as cross chain transferability is disabled + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + currency_id, + dest_address.clone(), + initial_balance, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + + // Enable LiquidityPools transferability + enable_liquidity_pool_transferability::(currency_id); + + // Cannot transfer more than owned + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + currency_id, + dest_address.clone(), + initial_balance.saturating_add(1), + ), + orml_tokens::Error::::BalanceTooLow + ); + + assert_ok!(pallet_liquidity_pools::Pallet::::transfer( + RawOrigin::Signed(source_account.into()).into(), + currency_id, + dest_address.clone(), + amount, + )); + + // The account to which the currency should have been transferred + // to on Centrifuge for bookkeeping purposes. + let domain_account: AccountId = Domain::convert(dest_address.domain()); + // Verify that the correct amount of the token was transferred + // to the dest domain account on Centrifuge. + assert_eq!( + orml_tokens::Pallet::::free_balance(currency_id, &domain_account), + amount + ); + assert_eq!( + orml_tokens::Pallet::::free_balance(currency_id, &source_account.into()), + initial_balance - amount + ); + }); + } + + fn transfer_non_tranche_tokens_to_local() { + 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 amount = DEFAULT_BALANCE_GLMR / 2; + let currency_id = AUSD_CURRENCY_ID; + let receiver: AccountId = Keyring::Bob.into(); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::Transfer { + currency: general_currency_index::(currency_id), + // sender is irrelevant for other -> local + sender: Keyring::Alice.into(), + receiver: receiver.clone().into(), + amount, + }; + + assert_eq!(orml_tokens::Pallet::::total_issuance(currency_id), 0); + + // Finally, verify that we can now transfer the tranche to the destination + // address + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify that the correct amount was minted + assert_eq!( + orml_tokens::Pallet::::total_issuance(currency_id), + amount + ); + assert_eq!( + orml_tokens::Pallet::::free_balance(currency_id, &receiver), + amount + ); + + // Verify empty transfers throw + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + LiquidityPoolMessage::Transfer { + currency: general_currency_index::(currency_id), + sender: Keyring::Alice.into(), + receiver: receiver.into(), + amount: 0, + }, + ), + pallet_liquidity_pools::Error::::InvalidTransferAmount + ); + }); + } + + fn transfer_tranche_tokens_from_local() { + 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 = 100_000; + let dest_address: DomainAddress = DomainAddress::EVM(1284, [99; 20]); + let receiver = Keyring::Bob; + + // Create the pool + create_ausd_pool::(pool_id); + + let tranche_tokens: CurrencyId = cfg_types::tokens::TrancheCurrency::generate( + pool_id, + default_tranche_id::(pool_id), + ) + .into(); + + // Verify that we first need the destination address to be whitelisted + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer_tranche_tokens( + RawOrigin::Signed(Keyring::Alice.into()).into(), + pool_id, + default_tranche_id::(pool_id), + dest_address.clone(), + amount, + ), + pallet_liquidity_pools::Error::::UnauthorizedTransfer + ); + + // Make receiver the MembersListAdmin of this Pool + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + receiver.into(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::InvestorAdmin), + )); + + // Whitelist destination as TrancheInvestor of this Pool + let valid_until = u64::MAX; + assert_ok!(pallet_permissions::Pallet::::add( + RawOrigin::Signed(receiver.into()).into(), + Role::PoolRole(PoolRole::InvestorAdmin), + AccountConverter::::convert(dest_address.clone()), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + valid_until + )), + )); + + // Call the pallet_liquidity_pools::Pallet::::update_member which ensures the + // destination address is whitelisted. + assert_ok!(pallet_liquidity_pools::Pallet::::update_member( + RawOrigin::Signed(receiver.into()).into(), + pool_id, + default_tranche_id::(pool_id), + dest_address.clone(), + valid_until, + )); + + // Give receiver enough Tranche balance to be able to transfer it + assert_ok!(orml_tokens::Pallet::::deposit( + tranche_tokens, + &receiver.into(), + amount + )); + + // Finally, verify that we can now transfer the tranche to the destination + // address + assert_ok!( + pallet_liquidity_pools::Pallet::::transfer_tranche_tokens( + RawOrigin::Signed(receiver.into()).into(), + pool_id, + default_tranche_id::(pool_id), + dest_address.clone(), + amount, + ) + ); + + // The account to which the tranche should have been transferred + // to on Centrifuge for bookkeeping purposes. + let domain_account: AccountId = Domain::convert(dest_address.domain()); + + // Verify that the correct amount of the Tranche token was transferred + // to the dest domain account on Centrifuge. + assert_eq!( + orml_tokens::Pallet::::free_balance(tranche_tokens, &domain_account), + amount + ); + assert!( + orml_tokens::Pallet::::free_balance(tranche_tokens, &receiver.into()) + .is_zero() + ); + }); + } + + fn transfer_tranche_tokens_to_local() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + // Create new pool + let pool_id = DEFAULT_POOL_ID; + create_ausd_pool::(pool_id); + + let amount = 100_000_000; + let receiver: AccountId = Keyring::Bob.into(); + let sender: DomainAddress = DomainAddress::EVM(1284, [99; 20]); + let sending_domain_locator = + Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); + let tranche_id = default_tranche_id::(pool_id); + let tranche_tokens: CurrencyId = + cfg_types::tokens::TrancheCurrency::generate(pool_id, tranche_id).into(); + let valid_until = u64::MAX; + + // Fund `DomainLocator` account of origination domain tranche tokens are + // transferred from this account instead of minting + assert_ok!(orml_tokens::Pallet::::mint_into( + tranche_tokens, + &sending_domain_locator, + amount + )); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::TransferTrancheTokens { + pool_id, + tranche_id, + sender: sender.address(), + domain: Domain::Centrifuge, + receiver: receiver.clone().into(), + amount, + }; + + // Verify that we first need the receiver to be whitelisted + assert_noop!( + pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + pallet_liquidity_pools::Error::::UnauthorizedTransfer + ); + + // Make receiver the MembersListAdmin of this Pool + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + receiver.clone(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::InvestorAdmin), + )); + + // Whitelist destination as TrancheInvestor of this Pool + assert_ok!(pallet_permissions::Pallet::::add( + RawOrigin::Signed(receiver.clone()).into(), + Role::PoolRole(PoolRole::InvestorAdmin), + receiver.clone(), + PermissionScope::Pool(pool_id), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + valid_until + )), + )); + + // Finally, verify that we can now transfer the tranche to the destination + // address + assert_ok!(pallet_liquidity_pools::Pallet::::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify that the correct amount of the Tranche token was transferred + // to the dest domain account on Centrifuge. + assert_eq!( + orml_tokens::Pallet::::free_balance(tranche_tokens, &receiver), + amount + ); + assert!(orml_tokens::Pallet::::free_balance( + tranche_tokens, + &sending_domain_locator + ) + .is_zero()); + }); + } + + /// Try to transfer tranches for non-existing pools or invalid tranche + /// ids for existing pools. + fn transferring_invalid_tranche_tokens_should_fail() { + 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 dest_address: DomainAddress = DomainAddress::EVM(1284, [99; 20]); + + let valid_pool_id: u64 = 42; + create_ausd_pool::(valid_pool_id); + let valid_tranche_id = default_tranche_id::(valid_pool_id); + let valid_until = u64::MAX; + let transfer_amount = 42; + let invalid_pool_id = valid_pool_id + 1; + let invalid_tranche_id = valid_tranche_id.map(|i| i.saturating_add(1)); + assert!(pallet_pool_system::Pallet::::pool(invalid_pool_id).is_none()); + + // Make Keyring::Bob the MembersListAdmin of both pools + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + Keyring::Bob.into(), + PermissionScope::Pool(valid_pool_id), + Role::PoolRole(PoolRole::InvestorAdmin), + )); + assert_ok!(pallet_permissions::Pallet::::add( + ::RuntimeOrigin::root(), + Role::PoolRole(PoolRole::PoolAdmin), + Keyring::Bob.into(), + PermissionScope::Pool(invalid_pool_id), + Role::PoolRole(PoolRole::InvestorAdmin), + )); + + // Give Keyring::Bob investor role for (valid_pool_id, invalid_tranche_id) and + // (invalid_pool_id, valid_tranche_id) + assert_ok!(pallet_permissions::Pallet::::add( + RawOrigin::Signed(Keyring::Bob.into()).into(), + Role::PoolRole(PoolRole::InvestorAdmin), + AccountConverter::::convert(dest_address.clone()), + PermissionScope::Pool(invalid_pool_id), + Role::PoolRole(PoolRole::TrancheInvestor(valid_tranche_id, valid_until)), + )); + assert_ok!(pallet_permissions::Pallet::::add( + RawOrigin::Signed(Keyring::Bob.into()).into(), + Role::PoolRole(PoolRole::InvestorAdmin), + AccountConverter::::convert(dest_address.clone()), + PermissionScope::Pool(valid_pool_id), + Role::PoolRole(PoolRole::TrancheInvestor(invalid_tranche_id, valid_until)), + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer_tranche_tokens( + RawOrigin::Signed(Keyring::Bob.into()).into(), + invalid_pool_id, + valid_tranche_id, + dest_address.clone(), + transfer_amount + ), + pallet_liquidity_pools::Error::::PoolNotFound + ); + assert_noop!( + pallet_liquidity_pools::Pallet::::transfer_tranche_tokens( + RawOrigin::Signed(Keyring::Bob.into()).into(), + valid_pool_id, + invalid_tranche_id, + dest_address, + transfer_amount + ), + pallet_liquidity_pools::Error::::TrancheNotFound + ); + }); + } + + fn transfer_cfg_to_sibling(env: &mut FudgeEnv) { + let alice_initial_balance = cfg(1_000); + let transfer_amount = cfg(5); + let cfg_in_sibling = CurrencyId::ForeignAsset(12); + + // CFG Metadata + let meta: AssetMetadata = AssetMetadata { + decimals: 18, + name: "Development".into(), + symbol: "CFG".into(), + existential_deposit: 1_000_000_000_000, + location: Some(VersionedMultiLocation::V3(MultiLocation::new( + 1, + X2( + Parachain(T::FudgeHandle::PARA_ID), + general_key(parachains::polkadot::centrifuge::CFG_KEY), + ), + ))), + additional: CustomMetadata { + transferability: CrossChainTransferability::Xcm(Default::default()), + ..CustomMetadata::default() + }, + }; + + env.parachain_state_mut(|| { + assert_eq!( + pallet_balances::Pallet::::free_balance(&Keyring::Alice.into()), + alice_initial_balance + ); + assert_eq!( + pallet_balances::Pallet::::free_balance(¶chain_account( + T::FudgeHandle::SIBLING_ID + )), + 0 + ); + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + meta.clone(), + Some(CurrencyId::Native), + )); + }); + + env.sibling_state_mut(|| { + assert_eq!( + orml_tokens::Pallet::::free_balance(cfg_in_sibling, &Keyring::Bob.into()), + 0 + ); + + assert_ok!(orml_asset_registry::Pallet::::register_asset( + ::RuntimeOrigin::root(), + meta, + Some(cfg_in_sibling) + )); + }); + + env.parachain_state_mut(|| { + assert_ok!(pallet_restricted_xtokens::Pallet::::transfer( + RawOrigin::Signed(Keyring::Alice.into()).into(), + CurrencyId::Native, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(T::FudgeHandle::SIBLING_ID), + Junction::AccountId32 { + network: None, + id: Keyring::Bob.into(), + }, + ), + ) + .into() + ), + WeightLimit::Limited(8_000_000_000_000.into()), + )); + + // Confirm that Keyring::Alice's balance is initial balance - amount transferred + assert_eq!( + pallet_balances::Pallet::::free_balance(&Keyring::Alice.into()), + alice_initial_balance - transfer_amount + ); + + // Verify that the amount transferred is now part of the sibling account here + assert_eq!( + pallet_balances::Pallet::::free_balance(¶chain_account( + T::FudgeHandle::SIBLING_ID + )), + transfer_amount + ); + }); + + env.pass(Blocks::ByNumber(1)); + + env.sibling_state(|| { + let current_balance = + orml_tokens::Pallet::::free_balance(cfg_in_sibling, &Keyring::Bob.into()); + + // Verify that Keyring::Bob now has (amount transferred - fee) + assert_eq!(current_balance, transfer_amount - fee(18)); + + // Sanity check for the actual amount Keyring::Bob ends up with + assert_eq!(current_balance, 4992960800000000000); + }); + } + + fn transfer_cfg_to_and_from_sibling() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + // In order to be able to transfer CFG from Moonbeam to Development, we need to + // first send CFG from Development to Moonbeam, or else it fails since it'd be + // like Moonbeam had minted CFG on their side. + transfer_cfg_to_sibling::(&mut env); + + let para_to_sibling_transfer_amount = cfg(5); + + let alice_balance = cfg(1_000) - para_to_sibling_transfer_amount; + let bob_balance = para_to_sibling_transfer_amount - fee(18); + let charlie_balance = cfg(1_000); + + let sibling_to_para_transfer_amount = cfg(4); + // Note: This asset was registered in `transfer_cfg_to_sibling` + let cfg_in_sibling = CurrencyId::ForeignAsset(12); + + env.parachain_state(|| { + assert_eq!( + pallet_balances::Pallet::::free_balance(&Keyring::Alice.into()), + alice_balance + ); + }); + + env.sibling_state_mut(|| { + assert_eq!( + pallet_balances::Pallet::::free_balance(¶chain_account( + T::FudgeHandle::PARA_ID + )), + 0 + ); + + assert_eq!( + orml_tokens::Pallet::::free_balance(cfg_in_sibling, &Keyring::Bob.into()), + bob_balance + ); + }); + + env.sibling_state_mut(|| { + assert_ok!(pallet_restricted_xtokens::Pallet::::transfer( + RawOrigin::Signed(Keyring::Bob.into()).into(), + cfg_in_sibling, + sibling_to_para_transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(T::FudgeHandle::PARA_ID), + Junction::AccountId32 { + network: None, + id: Keyring::Charlie.into(), + } + ) + ) + .into() + ), + WeightLimit::Limited(8_000_000_000_000.into()), + )); + + // Confirm that Charlie's balance is initial balance - amount transferred + assert_eq!( + orml_tokens::Pallet::::free_balance(cfg_in_sibling, &Keyring::Bob.into()), + bob_balance - sibling_to_para_transfer_amount + ); + }); + + env.pass(Blocks::ByNumber(2)); + + env.parachain_state(|| { + // Verify that Charlie's balance equals the amount transferred - fee + assert_eq!( + pallet_balances::Pallet::::free_balance(&Into::::into( + Keyring::Charlie + )), + charlie_balance + sibling_to_para_transfer_amount - cfg_fee(), + ); + }); + } + + crate::test_for_runtimes!([development], transfer_non_tranche_tokens_from_local); + crate::test_for_runtimes!([development], transfer_non_tranche_tokens_to_local); + crate::test_for_runtimes!([development], transfer_tranche_tokens_from_local); + crate::test_for_runtimes!([development], transfer_tranche_tokens_to_local); + crate::test_for_runtimes!( + [development], + transferring_invalid_tranche_tokens_should_fail + ); + crate::test_for_runtimes!([development], transfer_cfg_to_and_from_sibling); + } + + mod routers { + use super::*; + + mod axelar_evm { + use super::*; + + mod utils { + use super::*; + + pub fn mint_balance_into_derived_account( + env: &mut impl Env, + address: H160, + balance: u128, + ) { + let chain_id = env.parachain_state(|| pallet_evm_chain_id::Pallet::::get()); + + let derived_account = AccountConverter::::convert_evm_address( + chain_id, + address.to_fixed_bytes(), + ); + + env.parachain_state_mut(|| { + pallet_balances::Pallet::::mint_into(&derived_account.into(), balance) + .unwrap() + }); + } + } + + use utils::*; + + fn test_via_outbound_queue() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::::default() + .add(genesis::balances::(cfg(1_000))) + .add::(genesis::council_members::( + get_council_members(), + )) + .storage(), + ); + + let test_domain = Domain::EVM(1); + + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code: Vec = vec![0, 0, 0]; + let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); + let liquidity_pools_contract_address = H160::from_low_u64_be(2); + + env.parachain_state_mut(|| { + pallet_evm::AccountCodes::::insert( + axelar_contract_address, + axelar_contract_code, + ) + }); + + let transaction_call_cost = env + .parachain_state(|| ::config().gas_transaction_call); + + let evm_domain = EVMDomain { + target_contract_address: axelar_contract_address, + target_contract_hash: axelar_contract_hash, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(transaction_call_cost + 10_000), + gas_price: U256::from(10), + }, + }; + + let axelar_evm_router = AxelarEVMRouter:: { + router: EVMRouter { + evm_domain, + _marker: Default::default(), + }, + evm_chain: BoundedVec::>::try_from( + "ethereum".as_bytes().to_vec(), + ) + .unwrap(), + _marker: Default::default(), + liquidity_pools_contract_address, + }; + + let test_router = DomainRouter::::AxelarEVM(axelar_evm_router); + + let set_domain_router_call = + set_domain_router_call(test_domain.clone(), test_router.clone()); + + let council_threshold = 2; + let voting_period = 3; + + execute_via_democracy::( + &mut env, + get_council_members(), + set_domain_router_call, + council_threshold, + voting_period, + 0, + 0, + ); + + let sender = Keyring::Alice.to_account_id(); + let gateway_sender = env.parachain_state(|| { + ::Sender::get() + }); + + let gateway_sender_h160: H160 = H160::from_slice( + &>::as_ref(&gateway_sender) + [0..20], + ); + + // Note how both the target address and the gateway sender need to have some + // balance. + mint_balance_into_derived_account::( + &mut env, + axelar_contract_address, + cfg(1_000_000_000), + ); + mint_balance_into_derived_account::( + &mut env, + gateway_sender_h160, + cfg(1_000_000), + ); + + let msg = LiquidityPoolMessage::Transfer { + currency: 0, + sender: Keyring::Alice.to_account_id().into(), + receiver: Keyring::Bob.to_account_id().into(), + amount: 1_000u128, + }; + + assert_ok!(env.parachain_state(|| { + as OutboundQueue>::submit( + sender, + test_domain, + msg, + ) + })); + } + + crate::test_for_runtimes!([development], test_via_outbound_queue); + } + + mod ethereum_xcm { + use super::*; + + mod utils { + use super::*; + + pub fn submit_test_fn( + router_creation_fn: RouterCreationFn, + ) { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); -mod utils { - use super::*; + env.parachain_state_mut(|| { + let domain_router = router_creation_fn( + MultiLocation { + parents: 1, + interior: X1(Parachain(T::FudgeHandle::SIBLING_ID)), + } + .into(), + crate::utils::GLMR_CURRENCY_ID, + ); + + assert_ok!( + pallet_liquidity_pools_gateway::Pallet::::set_domain_router( + ::RuntimeOrigin::root(), + TEST_DOMAIN, + domain_router, + ) + ); + + let msg = + Message::::Transfer { + currency: 0, + sender: Keyring::Alice.into(), + receiver: Keyring::Bob.into(), + amount: 1_000u128, + }; + + assert_ok!( + as OutboundQueue>::submit( + Keyring::Alice.into(), + TEST_DOMAIN, + msg.clone(), + ) + ); - pub fn parachain_account(id: u32) -> AccountId { - polkadot_parachain::primitives::Sibling::from(id).into_account_truncating() - } + assert_noop!( + as OutboundQueue>::submit( + Keyring::Alice.into(), + Domain::EVM(1285), + msg.clone(), + ), + pallet_liquidity_pools_gateway::Error::::RouterNotFound, + ); + }); + } - pub fn xcm_metadata(transferability: CrossChainTransferability) -> Option { - match transferability { - CrossChainTransferability::Xcm(x) | CrossChainTransferability::All(x) => Some(x), - _ => None, + type RouterCreationFn = + Box DomainRouter>; + + pub fn get_axelar_xcm_router_fn() -> RouterCreationFn + { + Box::new( + |location: VersionedMultiLocation, + currency_id: CurrencyId| + -> DomainRouter { + let router = AxelarXCMRouter:: { + router: XCMRouter { + xcm_domain: XcmDomain { + location: Box::new( + location.try_into().expect("Bad xcm domain location"), + ), + ethereum_xcm_transact_call_index: BoundedVec::truncate_from( + vec![38, 0], + ), + contract_address: H160::from_low_u64_be(11), + max_gas_limit: 700_000, + transact_required_weight_at_most: Default::default(), + overall_weight: Default::default(), + fee_currency: currency_id, + fee_amount: dollar(18).saturating_div(5), + }, + _marker: Default::default(), + }, + axelar_target_chain: BoundedVec::< + u8, + ConstU32, + >::try_from("ethereum".as_bytes().to_vec()) + .unwrap(), + axelar_target_contract: H160::from_low_u64_be(111), + _marker: Default::default(), + }; + + DomainRouter::AxelarXCM(router) + }, + ) + } + + pub fn get_ethereum_xcm_router_fn() -> RouterCreationFn + { + Box::new( + |location: VersionedMultiLocation, + currency_id: CurrencyId| + -> DomainRouter { + let router = EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: XcmDomain { + location: Box::new( + location.try_into().expect("Bad xcm domain location"), + ), + ethereum_xcm_transact_call_index: BoundedVec::truncate_from( + vec![38, 0], + ), + contract_address: H160::from_low_u64_be(11), + max_gas_limit: 700_000, + transact_required_weight_at_most: Default::default(), + overall_weight: Default::default(), + fee_currency: currency_id, + fee_amount: dollar(18).saturating_div(5), + }, + _marker: Default::default(), + }, + _marker: Default::default(), + }; + + DomainRouter::EthereumXCM(router) + }, + ) + } + } + + use utils::*; + + const TEST_DOMAIN: Domain = Domain::EVM(1); + + fn submit_ethereum_xcm() { + submit_test_fn::(get_ethereum_xcm_router_fn::()); + } + + fn submit_axelar_xcm() { + submit_test_fn::(get_axelar_xcm_router_fn::()); + } + + crate::test_for_runtimes!([development], submit_ethereum_xcm); + crate::test_for_runtimes!([development], submit_axelar_xcm); } } - pub fn setup_xcm(env: &mut FudgeEnv) { - env.parachain_state_mut(|| { - // Set the XCM version used when sending XCM messages to sibling. - assert_ok!(pallet_xcm::Pallet::::force_xcm_version( - ::RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(T::FudgeHandle::SIBLING_ID)), - )), - XCM_VERSION, - )); - }); + mod gateway { + use super::*; - env.sibling_state_mut(|| { - // Set the XCM version used when sending XCM messages to parachain. - assert_ok!(pallet_xcm::Pallet::::force_xcm_version( - ::RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(T::FudgeHandle::PARA_ID)), - )), - XCM_VERSION, - )); - }); + fn add_remove_instances() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::::default() + .add(genesis::balances::(cfg(1_000))) + .add::(genesis::council_members::( + get_council_members(), + )) + .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(T::FudgeHandle::SIBLING_ID), - 10, - 1024, - )); + let test_instance = DomainAddress::EVM(1, [0; 20]); - assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< - FudgeRelayRuntime, - >::force_open_hrmp_channel( - as frame_system::Config>::RuntimeOrigin::root(), - Id::from(T::FudgeHandle::SIBLING_ID), - Id::from(T::FudgeHandle::PARA_ID), - 10, - 1024, - )); + let add_instance_call = add_instance_call::(test_instance.clone()); - assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< - FudgeRelayRuntime, - >::force_process_hrmp_open( - as frame_system::Config>::RuntimeOrigin::root(), + let council_threshold = 2; + let voting_period = 3; + + let (prop_index, ref_index) = execute_via_democracy::( + &mut env, + get_council_members(), + add_instance_call, + council_threshold, + voting_period, 0, - )); - }); + 0, + ); - env.pass(Blocks::ByNumber(1)); - } + let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceAdded { + instance: test_instance.clone(), + }; - 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.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); - 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, - )); + env.check_event(expected_event); - assert_ok!(polkadot_runtime_parachains::hrmp::Pallet::< - FudgeRelayRuntime, - >::force_process_hrmp_open( - as frame_system::Config>::RuntimeOrigin::root(), + let remove_instance_call = remove_instance_call::(test_instance.clone()); + + execute_via_democracy::( + &mut env, + get_council_members(), + remove_instance_call, + council_threshold, + voting_period, + prop_index, + ref_index, + ); + + let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceRemoved { + instance: test_instance.clone(), + }; + + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); + + env.check_event(expected_event); + } + + fn process_msg() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::::default() + .add(genesis::balances::(cfg(1_000))) + .add::(genesis::council_members::( + get_council_members(), + )) + .storage(), + ); + + let test_instance = DomainAddress::EVM(1, [0; 20]); + + let add_instance_call = add_instance_call::(test_instance.clone()); + + let council_threshold = 2; + let voting_period = 3; + + execute_via_democracy::( + &mut env, + get_council_members(), + add_instance_call, + council_threshold, + voting_period, 0, - )); - }); + 0, + ); - env.pass(Blocks::ByNumber(1)); - } -} + let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceAdded { + instance: test_instance.clone(), + }; -type FudgeRelayRuntime = <::FudgeHandle as FudgeHandle>::RelayRuntime; + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); -use utils::*; + env.check_event(expected_event); + + let msg = LiquidityPoolMessage::AddPool { pool_id: 123 }; + + let encoded_msg = msg.serialize(); + + let gateway_msg = BoundedVec::< + u8, + ::MaxIncomingMessageSize, + >::try_from(encoded_msg) + .unwrap(); + + env.parachain_state_mut(|| { + assert_noop!( + pallet_liquidity_pools_gateway::Pallet::::process_msg( + GatewayOrigin::Domain(test_instance).into(), + gateway_msg, + ), + pallet_liquidity_pools::Error::::InvalidIncomingMessage, + ); + }); + } + + crate::test_for_runtimes!([development], add_remove_instances); + crate::test_for_runtimes!([development], process_msg); + } +} mod altair { use altair_runtime::{CurrencyIdConvert, PoolPalletIndex}; @@ -157,32 +7178,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 +7228,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 +7236,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 +8150,6 @@ mod altair { mod centrifuge { use centrifuge_runtime::CurrencyIdConvert; - use sp_core::Get; use super::*; @@ -1275,34 +8245,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() { @@ -1387,18 +8329,6 @@ mod centrifuge { )); } - pub fn cfg_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 DOT tokens pub fn dot_fee() -> Balance { fee(10) @@ -1412,26 +8342,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 +8618,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,29 +8671,12 @@ 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); } mod restricted_transfers { - use cfg_types::{ - domain_address::{Domain, DomainAddress}, - locations::Location, - }; - use frame_support::{pallet_prelude::GenesisBuild, traits::fungibles::Mutate, BoundedVec}; - use liquidity_pools_gateway_routers::{ - DomainRouter, EthereumXCMRouter, XCMRouter, XcmDomain, - }; - use polkadot_parachain::primitives::ValidationCode; - use polkadot_runtime_parachains::{ - paras, - paras::{ParaGenesisArgs, ParaKind}, - }; - use sp_core::{Hasher, H160}; - use sp_runtime::traits::BlakeTwo256; - use super::*; use crate::generic::envs::runtime_env::RuntimeEnv; diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index 974fd590e4..a303b81406 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -1,12 +1,13 @@ use std::fmt::Debug; use cfg_primitives::{ - AccountId, Address, AuraId, Balance, BlockNumber, CollectionId, Header, Index, ItemId, LoanId, - PoolId, Signature, TrancheId, + AccountId, Address, AuraId, Balance, BlockNumber, CollectionId, CouncilCollective, Header, + Index, ItemId, LoanId, PoolId, Signature, TrancheId, }; 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, @@ -51,6 +53,7 @@ pub trait Runtime: BlockNumber = BlockNumber, Lookup = AccountIdLookup, RuntimeOrigin = Self::RuntimeOriginExt, + Hash = H256, > + pallet_pool_system::Config< CurrencyId = CurrencyId, Balance = Balance, @@ -67,8 +70,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 +108,33 @@ 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, + > + pallet_preimage::Config + + pallet_collective::Config + + pallet_democracy::Config> + + pallet_evm_chain_id::Config { /// Just the RuntimeCall type, but redefined with extra bounds. /// You can add `From` bounds in order to convert pallet calls to @@ -123,14 +151,17 @@ pub trait Runtime: + From> + From> + From> - + From>; + + From> + + From> + + From> + + From> + + From>; /// Just the RuntimeEvent type, but redefined with extra bounds. /// You can add `TryInto` and `From` bounds in order to convert pallet /// events to RuntimeEvent in tests. type RuntimeEventExt: Parameter + Member - + From> + Debug + IsType<::RuntimeEvent> + TryInto> @@ -138,20 +169,30 @@ pub trait Runtime: + TryInto> + TryInto> + TryInto> + + TryInto> + From> + From> + From> + From> + From> + From> - + From>; + + From> + + From> + + From> + + From> + + From> + + From> + + From> + + From> + + From>; type RuntimeOriginExt: Into, ::RuntimeOrigin>> + From> + Clone + OriginTrait::RuntimeCall> + From - + Into::RuntimeOrigin>>; + + Into::RuntimeOrigin>> + + From; /// Block used by the runtime type Block: Block< diff --git a/runtime/integration-tests/src/generic/utils/democracy.rs b/runtime/integration-tests/src/generic/utils/democracy.rs new file mode 100644 index 0000000000..f95dd82757 --- /dev/null +++ b/runtime/integration-tests/src/generic/utils/democracy.rs @@ -0,0 +1,279 @@ +use std::ops::Add; + +use cfg_primitives::{Balance, BlockNumber, CouncilCollective}; +use codec::Encode; +use frame_support::{ + dispatch::{GetDispatchInfo, Weight}, + traits::Bounded, +}; +use pallet_collective::{Call as CouncilCall, MemberCount, ProposalIndex}; +use pallet_democracy::{ + AccountVote, Call as DemocracyCall, Conviction, PropIndex, ReferendumIndex, ReferendumInfo, + Vote, +}; +use pallet_preimage::Call as PreimageCall; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +use crate::{ + generic::{ + config::Runtime, + env::{Blocks, Env}, + envs::fudge_env::FudgeSupport, + }, + utils::accounts::Keyring, +}; + +pub fn note_preimage(call: T::RuntimeCallExt) -> T::RuntimeCallExt { + let encoded_call = call.encode(); + + PreimageCall::note_preimage { + bytes: encoded_call, + } + .into() +} + +pub fn external_propose_majority(call: T::RuntimeCallExt) -> T::RuntimeCallExt { + let hash = BlakeTwo256::hash_of(&call); + + DemocracyCall::external_propose_majority { + proposal: Bounded::Legacy { + hash, + dummy: Default::default(), + }, + } + .into() +} + +pub fn fast_track( + proposal_hash: H256, + voting_period: BlockNumber, + delay: BlockNumber, +) -> T::RuntimeCallExt { + DemocracyCall::fast_track { + proposal_hash, + voting_period, + delay, + } + .into() +} + +pub fn execute_via_democracy( + env: &mut impl Env, + council_members: Vec, + original_call: T::RuntimeCallExt, + council_threshold: MemberCount, + voting_period: BlockNumber, + starting_prop_index: PropIndex, + starting_ref_index: ReferendumIndex, +) -> (PropIndex, ReferendumIndex) { + let original_call_hash = BlakeTwo256::hash_of(&original_call); + + env.submit_later( + council_members[0].into(), + note_preimage::(original_call.clone()), + ) + .expect("Preimage noting is successful"); + + env.pass(Blocks::UntilEvent { + event: pallet_preimage::Event::::Noted { + hash: original_call_hash, + } + .into(), + limit: 3, + }); + + let external_propose_majority_call = external_propose_majority::(original_call); + + execute_collective_proposal::( + env, + &council_members, + external_propose_majority_call, + council_threshold, + starting_prop_index, + ); + + let fast_track_call = fast_track::(original_call_hash, voting_period, 0); + + execute_collective_proposal::( + env, + &council_members, + fast_track_call, + council_threshold, + starting_prop_index + 1, + ); + + let vote = AccountVote::::Standard { + vote: Vote { + aye: true, + conviction: Conviction::Locked2x, + }, + balance: 1_000_000u128, + }; + + execute_democracy_vote(env, &council_members, starting_ref_index, vote); + + (starting_prop_index + 2, starting_ref_index + 1) +} + +pub fn democracy_vote( + ref_index: ReferendumIndex, + vote: AccountVote, +) -> T::RuntimeCallExt { + DemocracyCall::vote { ref_index, vote }.into() +} + +fn execute_democracy_vote( + env: &mut impl Env, + voters: &Vec, + referendum_index: ReferendumIndex, + acc_vote: AccountVote, +) { + for acc in voters { + let ref_info = env.parachain_state(|| { + pallet_democracy::ReferendumInfoOf::::get(referendum_index).unwrap() + }); + + if let ReferendumInfo::Finished { .. } = ref_info { + // Referendum might be finished by the time all voters get to vote. + break; + } + + env.submit_later(*acc, democracy_vote::(referendum_index, acc_vote)) + .expect("Voting is successful"); + + env.pass(Blocks::UntilEvent { + event: pallet_democracy::Event::::Voted { + voter: acc.to_account_id(), + ref_index: referendum_index, + vote: acc_vote, + } + .into(), + limit: 3, + }); + } +} + +pub fn collective_propose( + proposal: T::RuntimeCallExt, + threshold: MemberCount, +) -> T::RuntimeCallExt { + let proposal_len = proposal.encode().len(); + + CouncilCall::propose { + threshold, + proposal: Box::new(proposal), + length_bound: proposal_len as u32, + } + .into() +} + +pub fn collective_vote( + proposal: H256, + index: ProposalIndex, + approve: bool, +) -> T::RuntimeCallExt { + CouncilCall::vote { + proposal, + index, + approve, + } + .into() +} + +pub fn collective_close( + proposal_hash: H256, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, +) -> T::RuntimeCallExt { + CouncilCall::close { + proposal_hash, + index, + proposal_weight_bound, + length_bound, + } + .into() +} + +fn execute_collective_proposal( + env: &mut impl Env, + council_members: &Vec, + proposal: T::RuntimeCallExt, + council_threshold: MemberCount, + prop_index: PropIndex, +) { + let prop_hash = BlakeTwo256::hash_of(&proposal); + + env.submit_later( + council_members[0].into(), + collective_propose::(proposal.clone(), council_threshold), + ) + .expect("Collective proposal is successful"); + + env.pass(Blocks::UntilEvent { + event: pallet_collective::Event::::Proposed { + account: council_members[0].into(), + proposal_index: prop_index, + proposal_hash: prop_hash, + threshold: council_threshold, + } + .into(), + limit: 3, + }); + + for (index, acc) in council_members.iter().enumerate() { + env.submit_later(*acc, collective_vote::(prop_hash, prop_index, true)) + .expect("Collective voting is successful"); + + env.pass(Blocks::UntilEvent { + event: pallet_collective::Event::::Voted { + account: council_members[0].into(), + proposal_hash: prop_hash, + voted: true, + yes: (index + 1) as u32, + no: 0, + } + .into(), + limit: 3, + }); + } + + let proposal_weight = env.parachain_state(|| { + let external_proposal = + pallet_collective::ProposalOf::::get(prop_hash).unwrap(); + + external_proposal.get_dispatch_info().weight + }); + + env.submit_later( + council_members[0].into(), + collective_close::( + prop_hash, + prop_index, + proposal_weight.add(1.into()), + (proposal.encoded_size() + 1) as u32, + ), + ) + .expect("Collective close is successful"); + + env.pass(Blocks::UntilEvent { + event: pallet_collective::Event::::Closed { + proposal_hash: prop_hash, + yes: council_members.len() as u32, + no: 0, + } + .into(), + limit: 3, + }); + + env.check_event(pallet_collective::Event::::Approved { + proposal_hash: prop_hash, + }) + .expect("Approved event is present."); + env.check_event(pallet_collective::Event::::Executed { + proposal_hash: prop_hash, + result: Ok(()), + }) + .expect("Executed event is present."); +} diff --git a/runtime/integration-tests/src/generic/utils/genesis.rs b/runtime/integration-tests/src/generic/utils/genesis.rs index 97fae41c1c..be8f4e2110 100644 --- a/runtime/integration-tests/src/generic/utils/genesis.rs +++ b/runtime/integration-tests/src/generic/utils/genesis.rs @@ -7,11 +7,12 @@ use cfg_primitives::Balance; use cfg_types::tokens::CurrencyId; use codec::Encode; use frame_support::traits::GenesisBuild; +use sp_core::crypto::AccountId32; use sp_runtime::Storage; use crate::{ generic::{config::Runtime, utils::currency}, - utils::accounts::default_accounts, + utils::accounts::{default_accounts, Keyring}, }; pub struct Genesis { @@ -29,7 +30,7 @@ impl Default for Genesis { } impl Genesis { - pub fn add(mut self, builder: impl GenesisBuild) -> Self { + pub fn add(mut self, builder: impl GenesisBuild) -> Self { builder.assimilate_storage(&mut self.storage).unwrap(); self } @@ -75,3 +76,18 @@ pub fn assets(currency_ids: Vec) -> impl GenesisBuild last_asset_id: Default::default(), // It seems deprecated } } + +pub fn council_members(members: Vec) -> impl GenesisBuild +where + I: 'static, + T: pallet_collective::Config, + T::AccountId: From, +{ + pallet_collective::GenesisConfig:: { + phantom: Default::default(), + members: members + .into_iter() + .map(|acc| acc.to_account_id().into()) + .collect(), + } +} diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index eb7878f67b..907dc9f34b 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -9,6 +9,7 @@ //! Divide this utilities into files when it grows pub mod currency; +pub mod democracy; pub mod genesis; use cfg_primitives::{AccountId, Balance, CollectionId, ItemId, PoolId, TrancheId}; diff --git a/runtime/integration-tests/src/lib.rs b/runtime/integration-tests/src/lib.rs index bd4b09b5b6..d86123f6f0 100644 --- a/runtime/integration-tests/src/lib.rs +++ b/runtime/integration-tests/src/lib.rs @@ -16,7 +16,6 @@ mod evm; mod generic; -mod liquidity_pools; mod rewards; mod runtime_apis; mod utils; diff --git a/runtime/integration-tests/src/liquidity_pools/gateway.rs b/runtime/integration-tests/src/liquidity_pools/gateway.rs deleted file mode 100644 index 3b61179791..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/gateway.rs +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use cfg_primitives::{Balance, PoolId, TrancheId}; -use cfg_traits::liquidity_pools::Codec; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - fixed_point::Rate, - tokens::{CurrencyId, CustomMetadata}, -}; -use frame_support::{ - assert_noop, assert_ok, - dispatch::{GetDispatchInfo, Pays}, - weights::Weight, -}; -use fudge::primitives::Chain; -use liquidity_pools_gateway_routers::{ - axelar_evm::AxelarEVMRouter, ethereum_xcm::EthereumXCMRouter, DomainRouter, EVMDomain, - FeeValues, XCMRouter, XcmDomain, XcmTransactInfo, -}; -use orml_traits::asset_registry::AssetMetadata; -use pallet_democracy::{AccountVote, Conviction, ReferendumIndex, Vote, VoteThreshold}; -use pallet_liquidity_pools::Message; -use pallet_liquidity_pools_gateway::GatewayOrigin; -use sp_core::{bounded::BoundedVec, bounded_vec, H160, H256}; -use sp_runtime::{ - traits::{BlakeTwo256, Convert, Hash}, - Storage, -}; -use tokio::runtime::Handle; -use xcm::{ - latest::{Junction, Junctions, MultiLocation}, - VersionedMultiLocation, -}; - -use crate::{ - chain::centrifuge::{ - AccountId, CouncilCollective, FastTrackVotingPeriod, MinimumDeposit, Runtime, RuntimeCall, - RuntimeEvent, PARA_ID, - }, - utils::{ - accounts::Keyring, - collective::{collective_close, collective_propose, collective_vote}, - democracy::{democracy_vote, execute_via_democracy, external_propose_majority, fast_track}, - env::{ChainState, EventRange}, - liquidity_pools_gateway::{ - add_instance_call, remove_instance_call, set_domain_router_call, - }, - preimage::note_preimage, - *, - }, -}; - -pub(crate) fn get_council_members() -> Vec { - vec![Keyring::Alice, Keyring::Bob, Keyring::Charlie] -} - -#[tokio::test] -async fn set_router() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - genesis::council_members::(get_council_members(), &mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - let test_domain = Domain::EVM(1); - - let xcm_domain_location = MultiLocation { - parents: 0, - interior: Junctions::X1(Junction::Parachain(456)), - }; - - let currency_id = CurrencyId::ForeignAsset(1); - let currency_location = MultiLocation { - parents: 0, - interior: Junctions::X1(Junction::Parachain(123)), - }; - - let currency_meta = AssetMetadata:: { - decimals: 18, - name: "Test".into(), - symbol: "TST".into(), - existential_deposit: 1_000_000, - location: Some(VersionedMultiLocation::V3(currency_location)), - additional: Default::default(), - }; - - let xcm_domain = XcmDomain { - location: Box::new(xcm_domain_location.clone().into_versioned()), - ethereum_xcm_transact_call_index: bounded_vec![0], - contract_address: H160::from_low_u64_be(3), - max_gas_limit: 10, - transact_required_weight_at_most: Default::default(), - overall_weight: Default::default(), - fee_currency: currency_id, - fee_amount: 0, - }; - - let ethereum_xcm_router = EthereumXCMRouter:: { - router: XCMRouter { - xcm_domain, - _marker: Default::default(), - }, - _marker: Default::default(), - }; - - let test_router = DomainRouter::::EthereumXCM(ethereum_xcm_router); - - let set_domain_router_call = set_domain_router_call(test_domain.clone(), test_router.clone()); - - let council_threshold = 2; - let voting_period = 3; - - execute_via_democracy( - &mut env, - get_council_members(), - set_domain_router_call, - council_threshold, - voting_period, - 0, - 0, - ); - - env::evolve_until_event_is_found!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - voting_period + 1, - RuntimeEvent::LiquidityPoolsGateway(pallet_liquidity_pools_gateway::Event::DomainRouterSet { - domain, - router, - }) if [*domain == test_domain && *router == test_router], - ); -} - -#[tokio::test] -async fn add_remove_instances() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - genesis::council_members::(get_council_members(), &mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - let test_instance = DomainAddress::EVM(1, [0; 20]); - - let add_instance_call = add_instance_call(test_instance.clone()); - - let council_threshold = 2; - let voting_period = 3; - - let (prop_index, ref_index) = execute_via_democracy( - &mut env, - get_council_members(), - add_instance_call, - council_threshold, - voting_period, - 0, - 0, - ); - - env::evolve_until_event_is_found!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - voting_period + 1, - RuntimeEvent::LiquidityPoolsGateway(pallet_liquidity_pools_gateway::Event::InstanceAdded { - instance, - }) if [*instance == test_instance], - ); - - let remove_instance_call = remove_instance_call(test_instance.clone()); - - execute_via_democracy( - &mut env, - get_council_members(), - remove_instance_call, - council_threshold, - voting_period, - prop_index, - ref_index, - ); - - env::evolve_until_event_is_found!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - voting_period + 1, - RuntimeEvent::LiquidityPoolsGateway(pallet_liquidity_pools_gateway::Event::InstanceRemoved { - instance, - }) if [*instance == test_instance], - ); -} - -#[tokio::test] -async fn process_msg() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - genesis::council_members::(get_council_members(), &mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - let test_instance = DomainAddress::EVM(1, [0; 20]); - - let add_instance_call = add_instance_call(test_instance.clone()); - - let council_threshold = 2; - let voting_period = 3; - - let (prop_index, ref_index) = execute_via_democracy( - &mut env, - get_council_members(), - add_instance_call, - council_threshold, - voting_period, - 0, - 0, - ); - - env::evolve_until_event_is_found!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - voting_period + 1, - RuntimeEvent::LiquidityPoolsGateway(pallet_liquidity_pools_gateway::Event::InstanceAdded { - instance, - }) if [*instance == test_instance], - ); - - let msg = Message::::AddPool { pool_id: 123 }; - - let encoded_msg = msg.serialize(); - - let gateway_msg = BoundedVec::< - u8, - ::MaxIncomingMessageSize, - >::try_from(encoded_msg) - .unwrap(); - - env.with_state(Chain::Para(PARA_ID), || { - assert_noop!( - pallet_liquidity_pools_gateway::Pallet::::process_msg( - GatewayOrigin::Domain(test_instance).into(), - gateway_msg, - ), - pallet_liquidity_pools::Error::::InvalidIncomingMessage, - ); - }) - .unwrap(); -} diff --git a/runtime/integration-tests/src/liquidity_pools/mod.rs b/runtime/integration-tests/src/liquidity_pools/mod.rs deleted file mode 100644 index 70a5d1fbd2..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -mod gateway; -mod pallet; diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/mod.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/mod.rs deleted file mode 100644 index e42269b27a..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod setup; -mod tests; -mod transfers; diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/setup.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/setup.rs deleted file mode 100644 index 10e3c66dd8..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/setup.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -pub use altair_runtime::{AccountId, CurrencyId, Runtime, RuntimeOrigin, System}; -use cfg_primitives::{currency_decimals, parachains, Balance}; -use cfg_types::{domain_address::Domain, tokens::CustomMetadata}; -use frame_support::traits::GenesisBuild; -use orml_traits::asset_registry::AssetMetadata; - -use crate::{chain::centrifuge::PARA_ID, utils::env::PARA_ID_SIBLING}; - -pub fn cfg(amount: Balance) -> Balance { - amount * dollar(currency_decimals::NATIVE) -} - -pub fn dollar(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) -} - -pub fn centrifuge_account() -> AccountId { - parachain_account(PARA_ID) -} - -pub fn sibling_account() -> AccountId { - parachain_account(PARA_ID_SIBLING) -} - -fn parachain_account(id: u32) -> AccountId { - use sp_runtime::traits::AccountIdConversion; - - polkadot_parachain::primitives::Sibling::from(id).into_account_truncating() -} 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 deleted file mode 100644 index 1bc730757d..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/mod.rs +++ /dev/null @@ -1,58 +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. - -mod add_allow_upgrade; -mod foreign_investments; -pub(crate) mod setup; -mod transfers; - -#[test] -fn test_vec_to_fixed_array() { - let src = "TrNcH".as_bytes().to_vec(); - let symbol: [u8; 32] = cfg_utils::vec_to_fixed_array(src); - - assert!(symbol.starts_with("TrNcH".as_bytes())); - - assert_eq!( - symbol, - [ - 84, 114, 78, 99, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0 - ] - ); -} - -// Verify that the max tranche token symbol and name lengths are what the -// LiquidityPools pallet expects. -#[test] -fn verify_tranche_fields_sizes() { - assert_eq!( - cfg_types::consts::pools::MaxTrancheNameLengthBytes::get(), - pallet_liquidity_pools::TOKEN_NAME_SIZE as u32 - ); - assert_eq!( - cfg_types::consts::pools::MaxTrancheSymbolLengthBytes::get(), - pallet_liquidity_pools::TOKEN_SYMBOL_SIZE as u32 - ); -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/setup.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/setup.rs deleted file mode 100644 index ca329c7c4b..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/setup.rs +++ /dev/null @@ -1,424 +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, Balance, PoolId, TrancheId}; -use cfg_traits::{investments::InvestmentAccountant, PoolMutate, Seconds}; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - fixed_point::{Quantity, Rate}, - pools::TrancheMetadata, - tokens::{CrossChainTransferability, CurrencyId, CustomMetadata}, -}; -use cumulus_primitives_core::Junction::GlobalConsensus; -use frame_support::{ - assert_ok, - traits::{ - fungible::Mutate as _, - fungibles::{Balanced, Mutate}, - Get, PalletInfo, - }, - weights::Weight, -}; -use fudge::primitives::Chain; -use liquidity_pools_gateway_routers::{ - ethereum_xcm::EthereumXCMRouter, DomainRouter, XCMRouter, XcmDomain as GatewayXcmDomain, - XcmTransactInfo, DEFAULT_PROOF_SIZE, -}; -use orml_asset_registry::{AssetMetadata, Metadata}; -use orml_traits::MultiCurrency; -use pallet_liquidity_pools::Message; -use pallet_pool_system::tranches::{TrancheInput, TrancheType}; -use polkadot_parachain::primitives::Id; -use runtime_common::{ - account_conversion::AccountConverter, xcm::general_key, xcm_fees::default_per_second, -}; -use sp_core::H160; -use sp_runtime::{ - traits::{AccountIdConversion, BadOrigin, ConstU32, Convert, EnsureAdd, One, Zero}, - BoundedVec, DispatchError, Perquintill, SaturatedConversion, WeakBoundedVec, -}; -use xcm::{ - prelude::{Parachain, X1, X2, X3, XCM_VERSION}, - v3::{Junction, Junction::*, Junctions, MultiLocation, NetworkId}, - VersionedMultiLocation, -}; - -use crate::{ - chain::{ - centrifuge::{ - LiquidityPools, LiquidityPoolsGateway, OrmlAssetRegistry, OrmlTokens, PolkadotXcm, - PoolSystem, Runtime as DevelopmentRuntime, RuntimeOrigin, Tokens, TreasuryPalletId, - PARA_ID, - }, - relay::{Hrmp as RelayHrmp, RuntimeOrigin as RelayRuntimeOrigin}, - }, - liquidity_pools::pallet::development::{setup::dollar, tests::register_ausd}, - utils::{ - accounts::Keyring, - env::{TestEnv, PARA_ID_SIBLING}, - AUSD_CURRENCY_ID, GLMR_CURRENCY_ID, GLMR_ED, MOONBEAM_EVM_CHAIN_ID, - }, -}; - -// 10 GLMR (18 decimals) -pub const DEFAULT_BALANCE_GLMR: Balance = 10_000_000_000_000_000_000; -pub const DOMAIN_MOONBEAM: Domain = Domain::EVM(MOONBEAM_EVM_CHAIN_ID); -pub const DEFAULT_EVM_ADDRESS_MOONBEAM: [u8; 20] = [99; 20]; -pub const DEFAULT_DOMAIN_ADDRESS_MOONBEAM: DomainAddress = - DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, DEFAULT_EVM_ADDRESS_MOONBEAM); -pub const DEFAULT_VALIDITY: Seconds = 2555583502; -pub const DEFAULT_OTHER_DOMAIN_ADDRESS: DomainAddress = - DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [0; 20]); -pub const DEFAULT_POOL_ID: u64 = 42; -pub const DEFAULT_SIBLING_LOCATION: MultiLocation = MultiLocation { - parents: 1, - interior: X1(Parachain(PARA_ID_SIBLING)), -}; - -pub type LiquidityPoolMessage = Message; - -pub fn get_default_moonbeam_native_token_location() -> MultiLocation { - MultiLocation { - parents: 1, - interior: X2(Parachain(PARA_ID_SIBLING), general_key(&[0, 1])), - } -} - -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: GatewayXcmDomain { - 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!(LiquidityPoolsGateway::set_domain_router( - RuntimeOrigin::root(), - domain, - domain_router, - )); -} - -pub fn setup_test_env(env: &mut TestEnv) { - env.with_mut_state(Chain::Para(PARA_ID), || { - setup_pre_requirements(); - }) - .unwrap(); - - env.with_mut_state(Chain::Relay, || { - setup_hrmp_channel(); - }) - .unwrap(); - - env.evolve().unwrap(); -} - -/// Initializes universally required storage for liquidityPools tests: -/// * Set the EthereumXCM router which in turn sets: -/// * transact info and domain router for Moonbeam `MultiLocation`, -/// * fee for GLMR (`GLMR_CURRENCY_ID`), -/// * Register GLMR and AUSD in `OrmlAssetRegistry`, -/// * Mint 10 GLMR (`DEFAULT_BALANCE_GLMR`) for the LP Gateway Sender. -/// * Set the XCM version for the sibling parachain. -/// -/// NOTE: AUSD is the default pool currency in `create_pool`. -/// Neither AUSD nor GLMR are registered as a liquidityPools-transferable -/// currency! -pub fn setup_pre_requirements() { - /// Set the EthereumXCM router necessary for Moonbeam. - set_test_domain_router( - MOONBEAM_EVM_CHAIN_ID, - DEFAULT_SIBLING_LOCATION.into(), - GLMR_CURRENCY_ID, - ); - - /// Register Moonbeam's native token - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - asset_metadata( - "Glimmer".into(), - "GLMR".into(), - 18, - false, - GLMR_ED, - Some(VersionedMultiLocation::V3( - get_default_moonbeam_native_token_location() - )), - CrossChainTransferability::Xcm(Default::default()), - ), - Some(GLMR_CURRENCY_ID) - )); - - // Fund the gateway sender account with enough glimmer to pay for fees - assert_ok!(Tokens::set_balance( - RuntimeOrigin::root(), - ::Sender::get().into(), - GLMR_CURRENCY_ID, - DEFAULT_BALANCE_GLMR, - 0, - )); - - // Register AUSD in the asset registry which is the default pool currency in - // `create_pool` - register_ausd(); - - // Set the XCM version used when sending XCM messages to sibling. - assert_ok!(PolkadotXcm::force_xcm_version( - RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(PARA_ID_SIBLING)), - )), - XCM_VERSION, - )); -} - -/// Opens the required HRMP channel between parachain and sibling. -/// -/// NOTE - this is should be done on the relay chain. -pub fn setup_hrmp_channel() { - assert_ok!(RelayHrmp::force_open_hrmp_channel( - RelayRuntimeOrigin::root(), - Id::from(PARA_ID), - Id::from(PARA_ID_SIBLING), - 10, - 1024, - )); - - assert_ok!(RelayHrmp::force_process_hrmp_open( - RelayRuntimeOrigin::root(), - 0, - )); -} - -/// 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!(PoolSystem::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, - cfg_types::consts::pools::MaxTrancheNameLengthBytes, - >::try_from("A highly advanced tranche".as_bytes().to_vec()) - .expect(""), - token_symbol: BoundedVec::< - u8, - cfg_types::consts::pools::MaxTrancheSymbolLengthBytes, - >::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, - )); -} - -/// 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::() - .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 = - 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!(OrmlAssetRegistry::update_asset( - RuntimeOrigin::root(), - currency_id, - None, - None, - None, - None, - location, - Some(CustomMetadata { - // Changed: Allow liquidity_pools transferability - transferability: CrossChainTransferability::LiquidityPools, - ..metadata.additional - }) - )); -} - -/// Returns metadata for the given data with existential deposit of -/// 1_000_000. -pub fn asset_metadata( - name: Vec, - symbol: Vec, - decimals: u32, - is_pool_currency: bool, - existential_deposit: Balance, - location: Option, - transferability: CrossChainTransferability, -) -> AssetMetadata { - AssetMetadata { - name, - symbol, - decimals, - location, - existential_deposit, - additional: CustomMetadata { - transferability, - mintable: false, - permissioned: false, - pool_currency: is_pool_currency, - }, - } -} - -pub(crate) mod investments { - use cfg_primitives::AccountId; - use cfg_traits::investments::TrancheCurrency as TrancheCurrencyT; - use cfg_types::investments::InvestmentAccount; - use development_runtime::{OrderBook, PoolSystem}; - use pallet_pool_system::tranches::TrancheLoc; - - use super::*; - - /// 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() - } - - /// 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 tranche id at index 0 for the given pool id. - pub fn default_tranche_id(pool_id: u64) -> TrancheId { - let pool_details = PoolSystem::pool(pool_id).expect("Pool should exist"); - pool_details - .tranches - .tranche_id(TrancheLoc::Index(0)) - .expect("Tranche at index 0 exists") - } - - /// 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") - } -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs deleted file mode 100644 index d95e21ce2b..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs +++ /dev/null @@ -1,486 +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::{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 frame_support::{assert_noop, assert_ok, dispatch::Weight, traits::fungibles::Mutate}; -use fudge::primitives::Chain; -use orml_traits::{asset_registry::AssetMetadata, FixedConversionRateProvider, MultiCurrency}; -use runtime_common::account_conversion::AccountConverter; -use sp_runtime::{ - traits::{Convert, One, Zero}, - BoundedVec, DispatchError, Storage, -}; -use tokio::runtime::Handle; -use xcm::{latest::MultiLocation, VersionedMultiLocation}; - -use crate::{ - chain::centrifuge::{ - LiquidityPools, LocationToAccountId, OrmlTokens, Permissions, PoolSystem, - Runtime as DevelopmentRuntime, RuntimeOrigin, System, PARA_ID, - }, - liquidity_pools::pallet::development::{ - setup::dollar, - tests::liquidity_pools::setup::{ - asset_metadata, create_ausd_pool, create_currency_pool, - enable_liquidity_pool_transferability, - investments::{default_tranche_id, general_currency_index, investment_id}, - liquidity_pools_transferable_multilocation, setup_test_env, LiquidityPoolMessage, - DEFAULT_BALANCE_GLMR, DEFAULT_DOMAIN_ADDRESS_MOONBEAM, DEFAULT_POOL_ID, - }, - }, - utils::{accounts::Keyring, env, genesis, AUSD_CURRENCY_ID, AUSD_ED, MOONBEAM_EVM_CHAIN_ID}, -}; - -#[tokio::test] -async fn transfer_non_tranche_tokens_from_local() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_native_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 initial_balance = 2 * AUSD_ED; - let amount = initial_balance / 2; - let dest_address = DEFAULT_DOMAIN_ADDRESS_MOONBEAM; - let currency_id = AUSD_CURRENCY_ID; - let source_account = Keyring::Charlie; - - // Mint sufficient balance - assert_eq!( - OrmlTokens::free_balance(currency_id, &source_account.into()), - 0 - ); - assert_ok!(OrmlTokens::mint_into( - currency_id, - &source_account.into(), - initial_balance - )); - assert_eq!( - OrmlTokens::free_balance(currency_id, &source_account.into()), - initial_balance - ); - - // Only `ForeignAsset` can be transferred - assert_noop!( - LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - CurrencyId::Tranche(42u64, [0u8; 16]), - dest_address.clone(), - amount, - ), - pallet_liquidity_pools::Error::::InvalidTransferCurrency - ); - assert_noop!( - LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - CurrencyId::Staking(cfg_types::tokens::StakingCurrency::BlockRewards), - dest_address.clone(), - amount, - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - assert_noop!( - LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - CurrencyId::Native, - dest_address.clone(), - amount, - ), - pallet_liquidity_pools::Error::::AssetNotFound - ); - - // Cannot transfer as long as cross chain transferability is disabled - assert_noop!( - LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - currency_id, - dest_address.clone(), - initial_balance, - ), - pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable - ); - - // Enable LiquidityPools transferability - enable_liquidity_pool_transferability(currency_id); - - // Cannot transfer more than owned - assert_noop!( - LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - currency_id, - dest_address.clone(), - initial_balance.saturating_add(1), - ), - orml_tokens::Error::::BalanceTooLow - ); - - assert_ok!(LiquidityPools::transfer( - RuntimeOrigin::signed(source_account.into()), - currency_id, - dest_address.clone(), - amount, - )); - - // The account to which the currency should have been transferred - // to on Centrifuge for bookkeeping purposes. - let domain_account: AccountId = Domain::convert(dest_address.domain()); - // Verify that the correct amount of the token was transferred - // to the dest domain account on Centrifuge. - assert_eq!( - OrmlTokens::free_balance(currency_id, &domain_account), - amount - ); - assert_eq!( - OrmlTokens::free_balance(currency_id, &source_account.into()), - initial_balance - amount - ); - }); -} - -#[tokio::test] -async fn transfer_non_tranche_tokens_to_local() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_native_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 initial_balance = DEFAULT_BALANCE_GLMR; - let amount = DEFAULT_BALANCE_GLMR / 2; - let dest_address = DEFAULT_DOMAIN_ADDRESS_MOONBEAM; - let currency_id = AUSD_CURRENCY_ID; - let receiver: AccountId = Keyring::Bob.into(); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::Transfer { - currency: general_currency_index(currency_id), - // sender is irrelevant for other -> local - sender: Keyring::Alice.into(), - receiver: receiver.clone().into(), - amount, - }; - - assert_eq!(OrmlTokens::total_issuance(currency_id), 0); - - // Finally, verify that we can now transfer the tranche to the destination - // address - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Verify that the correct amount was minted - assert_eq!(OrmlTokens::total_issuance(currency_id), amount); - assert_eq!(OrmlTokens::free_balance(currency_id, &receiver), amount); - - // Verify empty transfers throw - assert_noop!( - LiquidityPools::submit( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - LiquidityPoolMessage::Transfer { - currency: general_currency_index(currency_id), - sender: Keyring::Alice.into(), - receiver: receiver.into(), - amount: 0, - }, - ), - pallet_liquidity_pools::Error::::InvalidTransferAmount - ); - }); -} - -#[tokio::test] -async fn transfer_tranche_tokens_from_local() { - 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 = 100_000; - let dest_address: DomainAddress = DomainAddress::EVM(1284, [99; 20]); - let receiver = Keyring::Bob; - - // Create the pool - create_ausd_pool(pool_id); - - let tranche_tokens: CurrencyId = - cfg_types::tokens::TrancheCurrency::generate(pool_id, default_tranche_id(pool_id)) - .into(); - - // Verify that we first need the destination address to be whitelisted - assert_noop!( - LiquidityPools::transfer_tranche_tokens( - RuntimeOrigin::signed(Keyring::Alice.into()), - pool_id, - default_tranche_id(pool_id), - dest_address.clone(), - amount, - ), - pallet_liquidity_pools::Error::::UnauthorizedTransfer - ); - - // Make receiver the MembersListAdmin of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - receiver.into(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - - // Whitelist destination as TrancheInvestor of this Pool - let valid_until = u64::MAX; - assert_ok!(Permissions::add( - RuntimeOrigin::signed(receiver.into()), - Role::PoolRole(PoolRole::InvestorAdmin), - AccountConverter::::convert( - dest_address.clone() - ), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id(pool_id), - valid_until - )), - )); - - // Call the LiquidityPools::update_member which ensures the destination address - // is whitelisted. - assert_ok!(LiquidityPools::update_member( - RuntimeOrigin::signed(receiver.into()), - pool_id, - default_tranche_id(pool_id), - dest_address.clone(), - valid_until, - )); - - // Give receiver enough Tranche balance to be able to transfer it - OrmlTokens::deposit(tranche_tokens, &receiver.into(), amount); - - // Finally, verify that we can now transfer the tranche to the destination - // address - assert_ok!(LiquidityPools::transfer_tranche_tokens( - RuntimeOrigin::signed(receiver.into()), - pool_id, - default_tranche_id(pool_id), - dest_address.clone(), - amount, - )); - - // The account to which the tranche should have been transferred - // to on Centrifuge for bookkeeping purposes. - let domain_account: AccountId = Domain::convert(dest_address.domain()); - - // Verify that the correct amount of the Tranche token was transferred - // to the dest domain account on Centrifuge. - assert_eq!( - OrmlTokens::free_balance(tranche_tokens, &domain_account), - amount - ); - assert!(OrmlTokens::free_balance(tranche_tokens, &receiver.into()).is_zero()); - }); -} - -#[tokio::test] -async fn transfer_tranche_tokens_to_local() { - 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), || { - // Create new pool - let pool_id = DEFAULT_POOL_ID; - create_ausd_pool(pool_id); - - let amount = 100_000_000; - let receiver: AccountId = Keyring::Bob.into(); - let sender: DomainAddress = DomainAddress::EVM(1284, [99; 20]); - let sending_domain_locator = Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()); - let tranche_id = default_tranche_id(pool_id); - let tranche_tokens: CurrencyId = - cfg_types::tokens::TrancheCurrency::generate(pool_id, tranche_id).into(); - let valid_until = u64::MAX; - - // Fund `DomainLocator` account of origination domain tranche tokens are - // transferred from this account instead of minting - assert_ok!(OrmlTokens::mint_into( - tranche_tokens, - &sending_domain_locator, - amount - )); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::TransferTrancheTokens { - pool_id, - tranche_id, - sender: sender.address(), - domain: Domain::Centrifuge, - receiver: receiver.clone().into(), - amount, - }; - - // Verify that we first need the receiver to be whitelisted - assert_noop!( - LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg.clone()), - pallet_liquidity_pools::Error::::UnauthorizedTransfer - ); - - // Make receiver the MembersListAdmin of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - receiver.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - - // Whitelist destination as TrancheInvestor of this Pool - assert_ok!(Permissions::add( - RuntimeOrigin::signed(receiver.clone()), - Role::PoolRole(PoolRole::InvestorAdmin), - receiver.clone(), - PermissionScope::Pool(pool_id), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id(pool_id), - valid_until - )), - )); - - // Finally, verify that we can now transfer the tranche to the destination - // address - assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); - - // Verify that the correct amount of the Tranche token was transferred - // to the dest domain account on Centrifuge. - assert_eq!(OrmlTokens::free_balance(tranche_tokens, &receiver), amount); - assert!(OrmlTokens::free_balance(tranche_tokens, &sending_domain_locator).is_zero()); - }); -} - -/// Try to transfer tranches for non-existing pools or invalid tranche ids for -/// existing pools. -#[tokio::test] -async fn transferring_invalid_tranche_tokens_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 dest_address: DomainAddress = DomainAddress::EVM(1284, [99; 20]); - - let valid_pool_id: u64 = 42; - create_ausd_pool(valid_pool_id); - let valid_tranche_id = default_tranche_id(valid_pool_id); - let valid_until = u64::MAX; - let transfer_amount = 42; - let invalid_pool_id = valid_pool_id + 1; - let invalid_tranche_id = valid_tranche_id.map(|i| i.saturating_add(1)); - assert!(PoolSystem::pool(invalid_pool_id).is_none()); - - // Make Keyring::Bob the MembersListAdmin of both pools - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - Keyring::Bob.into(), - PermissionScope::Pool(valid_pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - assert_ok!(Permissions::add( - RuntimeOrigin::root(), - Role::PoolRole(PoolRole::PoolAdmin), - Keyring::Bob.into(), - PermissionScope::Pool(invalid_pool_id), - Role::PoolRole(PoolRole::InvestorAdmin), - )); - - // Give Keyring::Bob investor role for (valid_pool_id, invalid_tranche_id) and - // (invalid_pool_id, valid_tranche_id) - assert_ok!(Permissions::add( - RuntimeOrigin::signed(Keyring::Bob.into()), - Role::PoolRole(PoolRole::InvestorAdmin), - AccountConverter::::convert( - dest_address.clone() - ), - PermissionScope::Pool(invalid_pool_id), - Role::PoolRole(PoolRole::TrancheInvestor(valid_tranche_id, valid_until)), - )); - assert_ok!(Permissions::add( - RuntimeOrigin::signed(Keyring::Bob.into()), - Role::PoolRole(PoolRole::InvestorAdmin), - AccountConverter::::convert( - dest_address.clone() - ), - PermissionScope::Pool(valid_pool_id), - Role::PoolRole(PoolRole::TrancheInvestor(invalid_tranche_id, valid_until)), - )); - assert_noop!( - LiquidityPools::transfer_tranche_tokens( - RuntimeOrigin::signed(Keyring::Bob.into()), - invalid_pool_id, - valid_tranche_id, - dest_address.clone(), - transfer_amount - ), - pallet_liquidity_pools::Error::::PoolNotFound - ); - assert_noop!( - LiquidityPools::transfer_tranche_tokens( - RuntimeOrigin::signed(Keyring::Bob.into()), - valid_pool_id, - invalid_tranche_id, - dest_address, - transfer_amount - ), - pallet_liquidity_pools::Error::::TrancheNotFound - ); - }); -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/mod.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/mod.rs deleted file mode 100644 index b39c5ccc28..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/mod.rs +++ /dev/null @@ -1,74 +0,0 @@ -use cfg_primitives::{parachains, Balance}; -use cfg_types::tokens::{CrossChainTransferability, CustomMetadata}; -use development_runtime::{OrmlAssetRegistry, RuntimeOrigin}; -use frame_support::assert_ok; -use orml_traits::asset_registry::AssetMetadata; -use runtime_common::xcm::general_key; -use xcm::{ - latest::MultiLocation, - prelude::{GeneralIndex, PalletInstance, Parachain, X2, X3}, - VersionedMultiLocation, -}; - -use crate::utils::{AUSD_CURRENCY_ID, AUSD_ED, USDT_CURRENCY_ID, USDT_ED}; - -mod liquidity_pools; -mod routers; - -/// Register AUSD in the asset registry. -/// -/// NOTE: 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: AUSD_ED, - location: Some(VersionedMultiLocation::V3(MultiLocation::new( - 1, - X2( - Parachain(parachains::kusama::karura::ID), - general_key(parachains::kusama::karura::AUSD_KEY), - ), - ))), - additional: CustomMetadata { - transferability: CrossChainTransferability::Xcm(Default::default()), - pool_currency: true, - ..CustomMetadata::default() - }, - }; - - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - meta, - Some(AUSD_CURRENCY_ID) - )); -} - -/// 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!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - meta, - Some(USDT_CURRENCY_ID) - )); -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/axelar_evm.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/axelar_evm.rs deleted file mode 100644 index 356cc62cfd..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/axelar_evm.rs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use cfg_primitives::{Balance, PoolId, TrancheId, CFG}; -use cfg_traits::liquidity_pools::OutboundQueue; -use cfg_types::{domain_address::Domain, fixed_point::Quantity}; -use frame_support::{assert_ok, dispatch::RawOrigin, traits::fungible::Mutate}; -use fudge::primitives::Chain; -use lazy_static::lazy_static; -use liquidity_pools_gateway_routers::{ - axelar_evm::AxelarEVMRouter, DomainRouter, EVMDomain, EVMRouter, FeeValues, - MAX_AXELAR_EVM_CHAIN_SIZE, -}; -use pallet_evm::FeeCalculator; -use pallet_liquidity_pools::Message; -use runtime_common::account_conversion::AccountConverter; -use sp_core::{ - bounded::BoundedVec, crypto::AccountId32, storage::Storage, ConstU32, Get, H160, U256, -}; -use sp_runtime::traits::{BlakeTwo256, Hash}; -use tokio::runtime::Handle; - -use crate::{ - chain::centrifuge::{ - Balances, CouncilCollective, LiquidityPoolsGateway, Runtime, RuntimeEvent, RuntimeOrigin, - PARA_ID, - }, - liquidity_pools::gateway::get_council_members, - utils::{ - accounts::Keyring, - democracy::execute_via_democracy, - env, - env::{ChainState, EventRange, TestEnv}, - evm::mint_balance_into_derived_account, - genesis, - liquidity_pools_gateway::set_domain_router_call, - }, -}; - -lazy_static! { - pub(crate) static ref TEST_EVM_CHAIN: BoundedVec> = - BoundedVec::>::try_from( - "ethereum".as_bytes().to_vec() - ) - .unwrap(); -} - -#[tokio::test] -async fn submit() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - genesis::council_members::(get_council_members(), &mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - let test_domain = Domain::EVM(1); - - let axelar_contract_address = H160::from_low_u64_be(1); - let axelar_contract_code: Vec = vec![0, 0, 0]; - let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); - let liquidity_pools_contract_address = H160::from_low_u64_be(2); - - env.with_mut_state(Chain::Para(PARA_ID), || { - pallet_evm::AccountCodes::::insert(axelar_contract_address, axelar_contract_code) - }) - .unwrap(); - - let transaction_call_cost = env - .with_state(Chain::Para(PARA_ID), || { - ::config().gas_transaction_call - }) - .unwrap(); - - let evm_domain = EVMDomain { - target_contract_address: axelar_contract_address, - target_contract_hash: axelar_contract_hash, - fee_values: FeeValues { - value: U256::from(10), - gas_limit: U256::from(transaction_call_cost + 10_000), - gas_price: U256::from(10), - }, - }; - - let axelar_evm_router = AxelarEVMRouter:: { - router: EVMRouter { - evm_domain, - _marker: Default::default(), - }, - evm_chain: TEST_EVM_CHAIN.clone(), - _marker: Default::default(), - liquidity_pools_contract_address, - }; - - let test_router = DomainRouter::::AxelarEVM(axelar_evm_router); - - let set_domain_router_call = set_domain_router_call(test_domain.clone(), test_router.clone()); - - let council_threshold = 2; - let voting_period = 3; - - execute_via_democracy( - &mut env, - get_council_members(), - set_domain_router_call, - council_threshold, - voting_period, - 0, - 0, - ); - - env::evolve_until_event_is_found!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - voting_period + 1, - RuntimeEvent::LiquidityPoolsGateway(pallet_liquidity_pools_gateway::Event::DomainRouterSet { - domain, - router, - }) if [*domain == test_domain && *router == test_router], - ); - - let sender = Keyring::Alice.to_account_id(); - let gateway_sender = env - .with_state(Chain::Para(PARA_ID), || { - ::Sender::get() - }) - .unwrap(); - - let gateway_sender_h160: H160 = - H160::from_slice(&>::as_ref(&gateway_sender)[0..20]); - - // Note how both the target address and the gateway sender need to have some - // balance. - mint_balance_into_derived_account(&mut env, axelar_contract_address, 1_000_000_000 * CFG); - mint_balance_into_derived_account(&mut env, gateway_sender_h160, 1_000_000 * CFG); - - let msg = Message::::Transfer { - currency: 0, - sender: Keyring::Alice.to_account_id().into(), - receiver: Keyring::Bob.to_account_id().into(), - amount: 1_000u128, - }; - - assert_ok!(env.with_state(Chain::Para(PARA_ID), || { - ::submit(sender, test_domain, msg).unwrap() - })); -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/ethereum_xcm.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/ethereum_xcm.rs deleted file mode 100644 index 99e1f58079..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/ethereum_xcm.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use ::xcm::{ - latest::{Junction, Junction::*, Junctions::*, MultiLocation, NetworkId}, - prelude::{Parachain, X1, X2}, - VersionedMultiLocation, -}; -use cfg_primitives::{PoolId, TrancheId}; -use cfg_traits::liquidity_pools::OutboundQueue; -use cfg_types::{ - domain_address::Domain, - fixed_point::Quantity, - tokens::{CrossChainTransferability, CurrencyId, CustomMetadata}, -}; -use frame_support::{assert_noop, assert_ok}; -use fudge::primitives::Chain; -use hex::FromHex; -use liquidity_pools_gateway_routers::{ - ethereum_xcm::EthereumXCMRouter, AxelarXCMRouter, DomainRouter, EVMDomain, FeeValues, - XCMRouter, XcmDomain, XcmTransactInfo, -}; -use orml_traits::{asset_registry::AssetMetadata, MultiCurrency}; -use pallet_liquidity_pools::Message; -use runtime_common::{xcm::general_key, xcm_fees::default_per_second}; -use sp_core::{bounded::BoundedVec, H160}; -use sp_runtime::Storage; -use tokio::runtime::Handle; - -use crate::{ - chain::centrifuge::{ - Balance, LiquidityPoolsGateway, OrmlAssetRegistry, OrmlTokens, Runtime, RuntimeOrigin, - PARA_ID, - }, - liquidity_pools::pallet::development::{ - setup::dollar, - tests::{ - liquidity_pools::setup::{setup_test_env, DEFAULT_SIBLING_LOCATION}, - routers::axelar_evm::TEST_EVM_CHAIN, - }, - }, - utils::{accounts::Keyring, env, env::PARA_ID_SIBLING, genesis, GLMR_CURRENCY_ID}, -}; - -const TEST_DOMAIN: Domain = Domain::EVM(1); - -#[tokio::test] -async fn submit_ethereum_xcm() { - submit_test_fn(get_ethereum_xcm_router_fn()); -} - -#[tokio::test] -async fn submit_axelar_xcm() { - submit_test_fn(get_axelar_xcm_router_fn()); -} - -fn submit_test_fn(router_creation_fn: RouterCreationFn) { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_native_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), || { - setup(router_creation_fn); - - let msg = Message::::Transfer { - currency: 0, - sender: Keyring::Alice.into(), - receiver: Keyring::Bob.into(), - amount: 1_000u128, - }; - - assert_ok!(::submit( - Keyring::Alice.into(), - TEST_DOMAIN, - msg.clone(), - )); - - assert_noop!( - ::submit( - Keyring::Alice.into(), - Domain::EVM(1285), - msg.clone(), - ), - pallet_liquidity_pools_gateway::Error::::RouterNotFound, - ); - }); -} - -type RouterCreationFn = Box DomainRouter>; - -fn get_axelar_xcm_router_fn() -> RouterCreationFn { - Box::new( - |location: VersionedMultiLocation, currency_id: CurrencyId| -> DomainRouter { - let router = AxelarXCMRouter:: { - router: XCMRouter { - xcm_domain: XcmDomain { - location: Box::new(location.try_into().expect("Bad xcm domain location")), - ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), - contract_address: H160::from_low_u64_be(11), - max_gas_limit: 700_000, - transact_required_weight_at_most: Default::default(), - overall_weight: Default::default(), - fee_currency: currency_id, - fee_amount: dollar(18).saturating_div(5), - }, - _marker: Default::default(), - }, - axelar_target_chain: TEST_EVM_CHAIN.clone(), - axelar_target_contract: H160::from_low_u64_be(111), - _marker: Default::default(), - }; - - DomainRouter::AxelarXCM(router) - }, - ) -} - -fn get_ethereum_xcm_router_fn() -> RouterCreationFn { - Box::new( - |location: VersionedMultiLocation, currency_id: CurrencyId| -> DomainRouter { - let router = EthereumXCMRouter:: { - router: XCMRouter { - xcm_domain: XcmDomain { - location: Box::new(location.try_into().expect("Bad xcm domain location")), - ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), - contract_address: H160::from_low_u64_be(11), - max_gas_limit: 700_000, - transact_required_weight_at_most: Default::default(), - overall_weight: Default::default(), - fee_currency: currency_id, - fee_amount: dollar(18).saturating_div(5), - }, - _marker: Default::default(), - }, - _marker: Default::default(), - }; - - DomainRouter::EthereumXCM(router) - }, - ) -} - -fn setup(router_creation_fn: RouterCreationFn) { - let domain_router = router_creation_fn(DEFAULT_SIBLING_LOCATION.into(), GLMR_CURRENCY_ID); - - assert_ok!(LiquidityPoolsGateway::set_domain_router( - RuntimeOrigin::root(), - TEST_DOMAIN, - domain_router, - )); -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/mod.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/mod.rs deleted file mode 100644 index a8be751d8f..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/routers/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -mod axelar_evm; -mod ethereum_xcm; diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/transfers.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/transfers.rs deleted file mode 100644 index 7d88b26392..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/transfers.rs +++ /dev/null @@ -1,356 +0,0 @@ -// Copyright 2021 Development GmbH (centrifuge.io). -// This file is part of Development chain project. -// -// Development 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). -// Development 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 Development GmbH (centrifuge.io). -// This file is part of Development chain project. -// -// Development 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). -// Development 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::{constants::currency_decimals, parachains, Balance}; -use cfg_types::{ - tokens::{CrossChainTransferability, CurrencyId, CustomMetadata}, - xcm::XcmMetadata, -}; -use frame_support::assert_ok; -use fudge::primitives::Chain; -use orml_traits::{asset_registry::AssetMetadata, FixedConversionRateProvider, MultiCurrency}; -use polkadot_parachain::primitives::Id; -use runtime_common::{ - xcm::general_key, - xcm_fees::{default_per_second, ksm_per_second}, -}; -use sp_runtime::{traits::BadOrigin, Storage}; -use tokio::runtime::Handle; -use xcm::{ - prelude::XCM_VERSION, - v3::{Junction, Junction::*, Junctions, Junctions::*, MultiLocation, NetworkId, WeightLimit}, - VersionedMultiLocation, -}; - -use crate::{ - chain::{ - centrifuge::{ - AccountId, Balances, OrmlAssetRegistry, OrmlTokens, PolkadotXcm, Runtime, - RuntimeOrigin, XTokens, PARA_ID, - }, - relay::{Hrmp as RelayHrmp, RuntimeOrigin as RelayRuntimeOrigin}, - }, - liquidity_pools::pallet::development::{ - setup::{centrifuge_account, cfg, sibling_account}, - tests::register_ausd, - }, - utils::{ - accounts::Keyring, - env, - env::{TestEnv, PARA_ID_SIBLING}, - genesis, - }, -}; - -/* - -NOTE: We hardcode the expected balances after an XCM operation given that the weights involved in -XCM execution often change slightly with each Polkadot update. We could simply test that the final -balance after some XCM operation is `initialBalance - amount - fee`, which would mean we would -never have to touch the tests again. However, by hard-coding these values we are forced to catch -an unexpectedly big change that would have a big impact on the weights and fees and thus balances, -which would go unnoticed and untreated otherwise. - - */ - -#[tokio::test] -async fn test_transfer_cfg_to_sibling() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_native_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - transfer_cfg_to_sibling(&mut env); -} - -fn transfer_cfg_to_sibling(env: &mut TestEnv) { - let alice_initial_balance = cfg(100_000); - let transfer_amount = cfg(5); - let cfg_in_sibling = CurrencyId::ForeignAsset(12); - - // CFG Metadata - let meta: AssetMetadata = AssetMetadata { - decimals: 18, - name: "Development".into(), - symbol: "CFG".into(), - existential_deposit: 1_000_000_000_000, - location: Some(VersionedMultiLocation::V3(MultiLocation::new( - 1, - X2( - Parachain(PARA_ID), - general_key(parachains::polkadot::centrifuge::CFG_KEY), - ), - ))), - additional: CustomMetadata { - transferability: CrossChainTransferability::Xcm(Default::default()), - ..CustomMetadata::default() - }, - }; - - env.with_mut_state(Chain::Para(PARA_ID), || { - assert_eq!( - Balances::free_balance(&Keyring::Alice.into()), - alice_initial_balance - ); - assert_eq!(Balances::free_balance(&sibling_account()), 0); - - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - meta.clone(), - Some(CurrencyId::Native), - )); - - assert_ok!(PolkadotXcm::force_xcm_version( - RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(PARA_ID_SIBLING)), - )), - XCM_VERSION, - )); - }); - - env.with_mut_state(Chain::Relay, || { - assert_ok!(RelayHrmp::force_open_hrmp_channel( - RelayRuntimeOrigin::root(), - Id::from(PARA_ID), - Id::from(PARA_ID_SIBLING), - 10, - 1024, - )); - - assert_ok!(RelayHrmp::force_process_hrmp_open( - RelayRuntimeOrigin::root(), - 0, - )); - }); - - env.evolve().unwrap(); - - env.with_mut_state(Chain::Para(PARA_ID_SIBLING), || { - assert_eq!( - OrmlTokens::free_balance(cfg_in_sibling, &Keyring::Bob.into()), - 0 - ); - - assert_ok!(OrmlAssetRegistry::register_asset( - RuntimeOrigin::root(), - meta, - Some(cfg_in_sibling) - )); - }); - - env.with_mut_state(Chain::Para(PARA_ID), || { - assert_ok!(XTokens::transfer( - RuntimeOrigin::signed(Keyring::Alice.into()), - CurrencyId::Native, - transfer_amount, - Box::new( - MultiLocation::new( - 1, - X2( - Parachain(PARA_ID_SIBLING), - Junction::AccountId32 { - network: None, - id: Keyring::Bob.into(), - }, - ), - ) - .into() - ), - WeightLimit::Limited(8_000_000_000_000.into()), - )); - - // Confirm that Keyring::Alice's balance is initial balance - amount transferred - assert_eq!( - Balances::free_balance(&Keyring::Alice.into()), - alice_initial_balance - transfer_amount - ); - - // Verify that the amount transferred is now part of the sibling account here - assert_eq!(Balances::free_balance(&sibling_account()), transfer_amount); - }); - - env.evolve().unwrap(); - - env.with_mut_state(Chain::Para(PARA_ID_SIBLING), || { - let current_balance = OrmlTokens::free_balance(cfg_in_sibling, &Keyring::Bob.into()); - - // Verify that Keyring::Bob now has (amount transferred - fee) - assert_eq!(current_balance, transfer_amount - fee(18)); - - // Sanity check for the actual amount Keyring::Bob ends up with - assert_eq!(current_balance, 4992960800000000000); - }); -} - -#[tokio::test] -async fn transfer_cfg_sibling_to_centrifuge() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_native_balances::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - // In order to be able to transfer CFG from Moonbeam to Development, we need to - // first send CFG from Development to Moonbeam, or else it fails since it'd be - // like Moonbeam had minted CFG on their side. - transfer_cfg_to_sibling(&mut env); - - let para_to_sibling_transfer_amount = cfg(5); - - let alice_balance = cfg(100_000) - para_to_sibling_transfer_amount; - let bob_balance = para_to_sibling_transfer_amount - fee(18); - let charlie_balance = cfg(100_000); - - let sibling_to_para_transfer_amount = cfg(4); - // Note: This asset was registered in `transfer_cfg_to_sibling` - let cfg_in_sibling = CurrencyId::ForeignAsset(12); - - env.with_mut_state(Chain::Para(PARA_ID), || { - assert_eq!( - Balances::free_balance(&Keyring::Alice.into()), - alice_balance - ); - }); - - env.with_mut_state(Chain::Para(PARA_ID_SIBLING), || { - assert_eq!(Balances::free_balance(¢rifuge_account()), 0); - - assert_eq!( - Balances::free_balance(&Keyring::Charlie.into()), - charlie_balance - ); - - assert_eq!( - OrmlTokens::free_balance(cfg_in_sibling, &Keyring::Bob.into()), - bob_balance - ); - - assert_ok!(PolkadotXcm::force_xcm_version( - RuntimeOrigin::root(), - Box::new(MultiLocation::new( - 1, - Junctions::X1(Junction::Parachain(PARA_ID)), - )), - XCM_VERSION, - )); - }); - - env.with_mut_state(Chain::Relay, || { - assert_ok!(RelayHrmp::force_open_hrmp_channel( - RelayRuntimeOrigin::root(), - Id::from(PARA_ID_SIBLING), - Id::from(PARA_ID), - 10, - 1024, - )); - - assert_ok!(RelayHrmp::force_process_hrmp_open( - RelayRuntimeOrigin::root(), - 0, - )); - }); - - env.evolve().unwrap(); - - env.with_mut_state(Chain::Para(PARA_ID_SIBLING), || { - assert_ok!(XTokens::transfer( - RuntimeOrigin::signed(Keyring::Bob.into()), - cfg_in_sibling, - sibling_to_para_transfer_amount, - Box::new( - MultiLocation::new( - 1, - X2( - Parachain(PARA_ID), - Junction::AccountId32 { - network: None, - id: Keyring::Charlie.into(), - } - ) - ) - .into() - ), - WeightLimit::Limited(8_000_000_000_000.into()), - )); - - // Confirm that Charlie's balance is initial balance - amount transferred - assert_eq!( - OrmlTokens::free_balance(cfg_in_sibling, &Keyring::Bob.into()), - bob_balance - sibling_to_para_transfer_amount - ); - }); - - env.evolve().unwrap(); - env.evolve().unwrap(); - - env.with_mut_state(Chain::Para(PARA_ID), || { - // Verify that Charlie's balance equals the amount transferred - fee - assert_eq!( - Balances::free_balance(&Into::::into(Keyring::Charlie)), - charlie_balance + sibling_to_para_transfer_amount - cfg_fee(), - ); - }); -} - -#[test] -fn test_total_fee() { - assert_eq!(cfg_fee(), 7039200000000000); -} - -fn cfg_fee() -> Balance { - fee(currency_decimals::NATIVE) -} - -fn ausd_fee() -> Balance { - fee(currency_decimals::AUSD) -} - -fn fee(decimals: u32) -> Balance { - calc_fee(default_per_second(decimals)) -} - -// The fee associated with transferring DOT tokens -fn dot_fee() -> Balance { - fee(10) -} - -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 -} - -/// Get the `XcmMetadata` for a given `CrossChainTransferability` value if -/// possible. -fn xcm_metadata(transferability: CrossChainTransferability) -> Option { - match transferability { - CrossChainTransferability::Xcm(x) | CrossChainTransferability::All(x) => Some(x), - _ => None, - } -} diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/mod.rs b/runtime/integration-tests/src/liquidity_pools/pallet/mod.rs deleted file mode 100644 index 40b4e588a2..0000000000 --- a/runtime/integration-tests/src/liquidity_pools/pallet/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use cfg_types::{tokens::CrossChainTransferability, xcm::XcmMetadata}; - -mod development; diff --git a/runtime/integration-tests/src/utils/collective.rs b/runtime/integration-tests/src/utils/collective.rs deleted file mode 100644 index b10a86c334..0000000000 --- a/runtime/integration-tests/src/utils/collective.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use codec::Encode; -use frame_support::{traits::Len, weights::Weight}; -use pallet_collective::{Call as CouncilCall, MemberCount, ProposalIndex}; -use sp_core::H256; -use sp_runtime::traits::{BlakeTwo256, Hash}; - -use crate::chain::centrifuge::{Runtime, RuntimeCall}; - -pub fn collective_propose(proposal: RuntimeCall, threshold: MemberCount) -> RuntimeCall { - let proposal_len = proposal.encode().len(); - let hash = BlakeTwo256::hash_of(&proposal); - - RuntimeCall::Council(CouncilCall::propose { - threshold, - proposal: Box::new(proposal), - length_bound: proposal_len as u32, - }) -} - -pub fn collective_vote(proposal: H256, index: ProposalIndex, approve: bool) -> RuntimeCall { - RuntimeCall::Council(CouncilCall::vote { - proposal, - index, - approve, - }) -} - -pub fn collective_close( - proposal_hash: H256, - index: ProposalIndex, - proposal_weight_bound: Weight, - length_bound: u32, -) -> RuntimeCall { - RuntimeCall::Council(CouncilCall::close { - proposal_hash, - index, - proposal_weight_bound, - length_bound, - }) -} diff --git a/runtime/integration-tests/src/utils/democracy.rs b/runtime/integration-tests/src/utils/democracy.rs deleted file mode 100644 index c7c9cb8a6e..0000000000 --- a/runtime/integration-tests/src/utils/democracy.rs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use std::ops::Add; - -use cfg_primitives::Balance; -use chain::centrifuge::{ - BlockNumber, CouncilCollective, Runtime, RuntimeCall, RuntimeEvent, PARA_ID, -}; -use codec::Encode; -use frame_support::{dispatch::GetDispatchInfo, traits::Bounded}; -use fudge::primitives::Chain; -use pallet_collective::MemberCount; -use pallet_democracy::{ - AccountVote, Call as DemocracyCall, Conviction, PropIndex, ReferendumIndex, ReferendumInfo, - Vote, -}; -use sp_core::{blake2_256, H256}; -use sp_runtime::traits::{BlakeTwo256, Hash}; - -use crate::{ - chain, - utils::{accounts::Keyring, collective::*, env, env::*, preimage::*}, -}; - -pub fn external_propose_majority(call: &RuntimeCall) -> RuntimeCall { - let hash = BlakeTwo256::hash_of(call); - - RuntimeCall::Democracy(DemocracyCall::external_propose_majority { - proposal: Bounded::Legacy { - hash, - dummy: Default::default(), - }, - }) -} - -pub fn fast_track( - proposal_hash: H256, - voting_period: BlockNumber, - delay: BlockNumber, -) -> RuntimeCall { - RuntimeCall::Democracy(DemocracyCall::fast_track { - proposal_hash, - voting_period, - delay, - }) -} - -pub fn democracy_vote(ref_index: ReferendumIndex, vote: AccountVote) -> RuntimeCall { - RuntimeCall::Democracy(DemocracyCall::vote { ref_index, vote }) -} - -pub fn execute_via_democracy( - test_env: &mut TestEnv, - council_members: Vec, - original_call: RuntimeCall, - council_threshold: MemberCount, - voting_period: BlockNumber, - starting_prop_index: PropIndex, - starting_ref_index: ReferendumIndex, -) -> (PropIndex, ReferendumIndex) { - let original_call_hash = BlakeTwo256::hash_of(&original_call); - - env::run!( - test_env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - council_members[0] => note_preimage(&original_call) - ); - - env::assert_events!( - test_env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::Preimage(pallet_preimage::Event::Noted{ hash }) if [*hash == original_call_hash], - ); - - let external_propose_majority_call = external_propose_majority(&original_call); - - execute_collective_proposal( - test_env, - &council_members, - external_propose_majority_call, - council_threshold, - starting_prop_index, - ); - - let fast_track_call = fast_track(original_call_hash, voting_period, 0); - - execute_collective_proposal( - test_env, - &council_members, - fast_track_call, - council_threshold, - starting_prop_index + 1, - ); - - let vote = AccountVote::::Standard { - vote: Vote { - aye: true, - conviction: Conviction::Locked2x, - }, - balance: 1_000_000u128, - }; - - execute_democracy_vote(test_env, &council_members, starting_ref_index, vote); - - (starting_prop_index + 2, starting_ref_index + 1) -} - -fn execute_democracy_vote( - test_env: &mut TestEnv, - voters: &Vec, - referendum_index: ReferendumIndex, - acc_vote: AccountVote, -) { - for acc in voters { - test_env.evolve().unwrap(); - - let ref_info = test_env - .with_state(Chain::Para(PARA_ID), || { - pallet_democracy::ReferendumInfoOf::::get(referendum_index).unwrap() - }) - .unwrap(); - - if let ReferendumInfo::Finished { .. } = ref_info { - // Referendum might be finished by the time all voters get to vote. - break; - } - - env::run!( - test_env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - *acc => democracy_vote(referendum_index, acc_vote) - ); - - env::assert_events!( - test_env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::Democracy(pallet_democracy::Event::Voted{ - voter, - ref_index, - vote, - }) if [ - *voter == acc.to_account_id() - && *ref_index == referendum_index - && *vote == acc_vote - ], - ) - } -} - -fn execute_collective_proposal( - test_env: &mut TestEnv, - council_members: &Vec, - proposal: RuntimeCall, - council_threshold: MemberCount, - prop_index: PropIndex, -) { - let prop_hash = BlakeTwo256::hash_of(&proposal); - - env::run!( - test_env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - council_members[0] => collective_propose(proposal.clone(), council_threshold) - ); - - env::assert_events!( - test_env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::Council(pallet_collective::Event::Proposed{ - account, - proposal_index, - proposal_hash, - threshold, - }) if [ - *account == council_members[0].to_account_id() - && *proposal_index == prop_index - && *proposal_hash == prop_hash - && *threshold == council_threshold - ], - ); - - for (index, acc) in council_members.iter().enumerate() { - env::run!( - test_env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - *acc => collective_vote(prop_hash, prop_index, true) - ); - - env::assert_events!( - test_env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::Council(pallet_collective::Event::Voted{ - account, - proposal_hash, - voted, - yes, - no, - }) if [ - *account == acc.to_account_id() - && *proposal_hash == prop_hash - && *voted == true - && *yes == (index + 1) as u32 - && *no == 0 - ], - ) - } - - let proposal_weight = test_env - .with_state(Chain::Para(PARA_ID), || { - let external_proposal = - pallet_collective::ProposalOf::::get(prop_hash) - .unwrap(); - - external_proposal.get_dispatch_info().weight - }) - .unwrap(); - - env::run!( - test_env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - council_members[0] => collective_close( - prop_hash, - prop_index, - proposal_weight.add(1.into()), - (proposal.encoded_size() + 1) as u32, - ) - ); - - env::assert_events!( - test_env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::Council(pallet_collective::Event::Closed { - proposal_hash, - yes, - no, - }) if [ - *proposal_hash == prop_hash - && *yes == council_members.len() as u32 - && *no == 0 - ], - RuntimeEvent::Council(pallet_collective::Event::Approved{ - proposal_hash - }) if [ *proposal_hash == prop_hash], - RuntimeEvent::Council(pallet_collective::Event::Executed{ - proposal_hash, - result, - }) if [ *proposal_hash == prop_hash && result.is_ok()], - ); -} diff --git a/runtime/integration-tests/src/utils/liquidity_pools_gateway.rs b/runtime/integration-tests/src/utils/liquidity_pools_gateway.rs deleted file mode 100644 index 5c9f9f009b..0000000000 --- a/runtime/integration-tests/src/utils/liquidity_pools_gateway.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; -use cfg_traits::liquidity_pools::Router; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - fixed_point::Rate, -}; -use development_runtime::liquidity_pools::MaxIncomingMessageSize; -use liquidity_pools_gateway_routers::DomainRouter; -use pallet_liquidity_pools::Message; -use pallet_liquidity_pools_gateway::Call as LiquidityPoolsGatewayCall; -use sp_core::bounded::BoundedVec; - -use crate::chain::centrifuge::{Runtime, RuntimeCall}; - -pub fn set_domain_router_call(domain: Domain, router: DomainRouter) -> RuntimeCall { - RuntimeCall::LiquidityPoolsGateway(LiquidityPoolsGatewayCall::set_domain_router { - domain, - router, - }) -} - -pub fn add_instance_call(instance: DomainAddress) -> RuntimeCall { - RuntimeCall::LiquidityPoolsGateway(LiquidityPoolsGatewayCall::add_instance { instance }) -} - -pub fn remove_instance_call(instance: DomainAddress) -> RuntimeCall { - RuntimeCall::LiquidityPoolsGateway(LiquidityPoolsGatewayCall::remove_instance { instance }) -} diff --git a/runtime/integration-tests/src/utils/mod.rs b/runtime/integration-tests/src/utils/mod.rs index 28f2fb5c68..a390d3dbbc 100644 --- a/runtime/integration-tests/src/utils/mod.rs +++ b/runtime/integration-tests/src/utils/mod.rs @@ -14,15 +14,11 @@ use cfg_primitives::Balance; use cfg_types::tokens::CurrencyId; pub mod accounts; -pub mod collective; -pub mod democracy; pub mod env; pub mod evm; pub mod extrinsics; pub mod genesis; -pub mod liquidity_pools_gateway; pub mod logs; -pub mod preimage; pub mod time; pub mod tokens; diff --git a/runtime/integration-tests/src/utils/preimage.rs b/runtime/integration-tests/src/utils/preimage.rs deleted file mode 100644 index 48e195f6a1..0000000000 --- a/runtime/integration-tests/src/utils/preimage.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2021 Centrifuge Foundation (centrifuge.io). -// -// This file is part of the Centrifuge chain project. -// Centrifuge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version (see http://www.gnu.org/licenses). -// Centrifuge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -use codec::Encode; -use pallet_preimage::Call as PreimageCall; - -use crate::chain::centrifuge::{Runtime, RuntimeCall}; - -pub fn note_preimage(call: &RuntimeCall) -> RuntimeCall { - let encoded_call = call.encode(); - - RuntimeCall::Preimage(PreimageCall::note_preimage { - bytes: encoded_call, - }) -}