diff --git a/Cargo.toml b/Cargo.toml index 7dfca2b3d6..bacabf99ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ "pallets/keystore", "pallets/liquidity-pools", "pallets/liquidity-pools-gateway", - "pallets/liquidity-pools-gateway/queue", + "pallets/liquidity-pools-gateway-queue", "pallets/liquidity-pools-forwarder", "pallets/liquidity-rewards", "pallets/loans", @@ -236,7 +236,7 @@ pallet-investments = { path = "pallets/investments", default-features = false } pallet-keystore = { path = "pallets/keystore", default-features = false } pallet-liquidity-pools = { path = "pallets/liquidity-pools", default-features = false } pallet-liquidity-pools-gateway = { path = "pallets/liquidity-pools-gateway", default-features = false } -pallet-liquidity-pools-gateway-queue = { path = "pallets/liquidity-pools-gateway/queue", default-features = false } +pallet-liquidity-pools-gateway-queue = { path = "pallets/liquidity-pools-gateway-queue", default-features = false } pallet-liquidity-pools-forwarder = { path = "pallets/liquidity-pools-forwarder", default-features = false } pallet-liquidity-rewards = { path = "pallets/liquidity-rewards", default-features = false } pallet-loans = { path = "pallets/loans", default-features = false } diff --git a/README.md b/README.md index edbfaea8ce..ebe6652a14 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,11 @@ Centrifuge is the infrastructure that facilitates the decentralized financing of On top of the [Substrate FRAME](https://docs.substrate.io/reference/frame-pallets/) framework, Centrifuge Chain is composed of custom pallets which can be found inside the `pallets` folder. The following list gives a brief overview, and links to the corresponding documentation: - [**anchors**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/anchors) ([docs](https://reference.centrifuge.io/pallet_anchors/index.html)): Storing hashes of documents on-chain. The documents are stored in the Private Off-chain Data (POD) node network. -- + - [**anchors-v2**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/anchors-v2) ([docs](https://reference.centrifuge.io/pallet_anchors_v2/index.html)): Second version of the pallet used to store document hashes on-chain. +- [**axelar-routers**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway/pallet-axelar-router) ([docs](https://reference.centrifuge.io/pallet-axelar-router/index.html)): Pallet that communicates with other chains through axelar. + - [**block-rewards**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/block-rewards) ([docs](https://reference.centrifuge.io/pallet_block_rewards/index.html)): Provides means of configuring and distributing block rewards to collators as well as the annual treasury inflation. - [**bridge**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/bridge) ([docs](https://reference.centrifuge.io/pallet_bridge/index.html)): Connecting [ChainBridge](https://github.com/centrifuge/chainbridge-substrate) to transfer tokens to and from Ethereum. @@ -54,12 +56,8 @@ On top of the [Substrate FRAME](https://docs.substrate.io/reference/frame-pallet - [**liquidity-pools**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools) ([docs](https://reference.centrifuge.io/pallet_liquidity_pools/index.html)): Provides the toolset to enable foreign investments on foreign domains. - [**liquidity-pools-gateway**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway) ([docs](https://reference.centrifuge.io/pallet_liquidity_pools_gateway/index.html)): The main handler of incoming and outgoing Liquidity Pools messages. -- -- [**liquidity-pools-gateway-queue**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway/queue) ([docs](https://reference.centrifuge.io/pallet_liquidity_pools_gateway_queue/index.html)): The queue used by the Liquidity Pools Gateway for processing inbound/outbound messages. -- [**liquidity-pools-gateway-routers**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway/routers) ([docs](https://reference.centrifuge.io/liquidity_pools_gateway_routers/index.html)): This crate contains the `DomainRouters` used by the Liquidity Pools Gateway pallet. - -- [**axelar-gateway-precompile**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway/axelar-gateway-precompile) ([docs](https://reference.centrifuge.io/axelar_gateway_precompile/index.html)): Pallet that serves as an EVM precompile for incoming Liquidity Pools messages from the Axelar network. +- [**liquidity-pools-gateway-queue**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-pools-gateway/queue) ([docs](https://reference.centrifuge.io/pallet_liquidity_pools_gateway_queue/index.html)): The queue used by the Liquidity Pools Gateway for processing inbound/outbound messages. - [**liquidity-rewards**](https://github.com/centrifuge/centrifuge-chain/tree/main/pallets/liquidity-rewards) ([docs](https://reference.centrifuge.io/pallet_liquidity_rewards/index.html)): Epoch based reward system. diff --git a/libs/mocks/src/lib.rs b/libs/mocks/src/lib.rs index ca9fb241bc..7aa09731a5 100644 --- a/libs/mocks/src/lib.rs +++ b/libs/mocks/src/lib.rs @@ -10,11 +10,11 @@ pub mod foreign_investment_hooks; pub mod investment; pub mod liquidity_pools; pub mod liquidity_pools_gateway; -pub mod liquidity_pools_gateway_queue; pub mod pay_fee; pub mod permissions; pub mod pools; pub mod pre_conditions; +pub mod queue; pub mod rewards; pub mod router_message; pub mod status_notification; @@ -30,7 +30,6 @@ pub use fees::pallet as pallet_mock_fees; pub use investment::pallet as pallet_mock_investment; pub use liquidity_pools::pallet as pallet_mock_liquidity_pools; pub use liquidity_pools_gateway::pallet as pallet_mock_liquidity_pools_gateway; -pub use liquidity_pools_gateway_queue::pallet as pallet_mock_liquidity_pools_gateway_queue; pub use pay_fee::pallet as pallet_mock_pay_fee; pub use permissions::pallet as pallet_mock_permissions; pub use pools::pallet as pallet_mock_pools; diff --git a/libs/mocks/src/liquidity_pools_gateway_queue.rs b/libs/mocks/src/queue.rs similarity index 80% rename from libs/mocks/src/liquidity_pools_gateway_queue.rs rename to libs/mocks/src/queue.rs index 85524378d2..a368399ee6 100644 --- a/libs/mocks/src/liquidity_pools_gateway_queue.rs +++ b/libs/mocks/src/queue.rs @@ -16,7 +16,7 @@ pub mod pallet { type CallIds = StorageMap<_, _, String, mock_builder::CallId>; impl Pallet { - pub fn mock_submit(f: impl Fn(T::Message) -> DispatchResult + 'static) -> CallHandler { + pub fn mock_queue(f: impl Fn(T::Message) -> DispatchResult + 'static) -> CallHandler { register_call!(f) } } @@ -24,7 +24,7 @@ pub mod pallet { impl MessageQueue for Pallet { type Message = T::Message; - fn submit(msg: Self::Message) -> DispatchResult { + fn queue(msg: Self::Message) -> DispatchResult { execute_call!(msg) } } diff --git a/libs/traits/src/lib.rs b/libs/traits/src/lib.rs index d7eb9042b0..5860bbfb98 100644 --- a/libs/traits/src/lib.rs +++ b/libs/traits/src/lib.rs @@ -30,25 +30,15 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_runtime::{traits::Member, DispatchError}; use sp_std::{fmt::Debug, marker::PhantomData, vec::Vec}; -/// Traits related to checked changes. pub mod changes; -/// Traits related to data registry and collection. pub mod data; -/// Traits related to Ethereum/EVM. pub mod ethereum; -/// Traits related to pool fees. pub mod fee; -/// Traits related to fees payment. pub mod fees; -/// Traits related to interest rates. pub mod interest; -/// Traits related to investments. pub mod investments; -/// Traits related to liquidity pools. pub mod liquidity_pools; -/// Traits related to rewards. pub mod rewards; -/// Traits related to swaps. pub mod swaps; #[cfg(feature = "runtime-benchmarks")] diff --git a/libs/traits/src/liquidity_pools.rs b/libs/traits/src/liquidity_pools.rs index 7c5dddd69d..a0d73d98a3 100644 --- a/libs/traits/src/liquidity_pools.rs +++ b/libs/traits/src/liquidity_pools.rs @@ -50,13 +50,13 @@ pub trait LpMessage: Sized { /// /// Hash - hash of the message that should be recovered. /// Router - the address of the recovery router. - fn initiate_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self; + fn initiate_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self; /// Creates a message used for disputing message recovery. /// /// Hash - hash of the message that should be disputed. /// Router - the address of the recovery router. - fn dispute_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self; + fn dispute_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self; /// Checks whether a message is a forwarded one. fn is_forwarded(&self) -> bool; @@ -123,10 +123,10 @@ pub trait MessageQueue { type Message; /// Submit a message to the queue. - fn submit(msg: Self::Message) -> DispatchResult; + fn queue(msg: Self::Message) -> DispatchResult; } -/// The trait required for processing queued messages. +/// The trait required for processing dequeued messages. pub trait MessageProcessor { /// The message type. type Message; @@ -134,7 +134,7 @@ pub trait MessageProcessor { /// Process a message. fn process(msg: Self::Message) -> (DispatchResult, Weight); - /// Process a message. + /// Max weight that processing a message can take fn max_processing_weight(msg: &Self::Message) -> Weight; } diff --git a/libs/types/src/domain_address.rs b/libs/types/src/domain_address.rs index 04f48b708c..8962a5b380 100644 --- a/libs/types/src/domain_address.rs +++ b/libs/types/src/domain_address.rs @@ -85,12 +85,6 @@ impl TypeId for DomainAddress { const TYPE_ID: [u8; 4] = crate::ids::DOMAIN_ADDRESS_ID; } -impl Default for DomainAddress { - fn default() -> Self { - DomainAddress::Centrifuge(AccountId32::new([0; 32])) - } -} - impl From for Domain { fn from(x: DomainAddress) -> Self { match x { diff --git a/libs/utils/src/lib.rs b/libs/utils/src/lib.rs index 189cfe8b7e..3e456628e5 100644 --- a/libs/utils/src/lib.rs +++ b/libs/utils/src/lib.rs @@ -70,42 +70,6 @@ pub fn set_block_number_timestamp( pallet_timestamp::Pallet::::set_timestamp(timestamp); } -pub fn decode_var_source( - source_address: &[u8], -) -> Option<[u8; EXPECTED_SOURCE_ADDRESS_SIZE]> { - const HEX_PREFIX: &str = "0x"; - - let mut address = [0u8; EXPECTED_SOURCE_ADDRESS_SIZE]; - - if source_address.len() == EXPECTED_SOURCE_ADDRESS_SIZE { - address.copy_from_slice(source_address); - return Some(address); - } - - let try_bytes = match sp_std::str::from_utf8(source_address) { - Ok(res) => res.as_bytes(), - Err(_) => source_address, - }; - - // Attempt to hex decode source address. - let bytes = match hex::decode(try_bytes) { - Ok(res) => Some(res), - Err(_) => { - // Strip 0x prefix. - let res = try_bytes.strip_prefix(HEX_PREFIX.as_bytes())?; - - hex::decode(res).ok() - } - }?; - - if bytes.len() == EXPECTED_SOURCE_ADDRESS_SIZE { - address.copy_from_slice(bytes.as_slice()); - Some(address) - } else { - None - } -} - pub mod math { use sp_arithmetic::{ traits::{BaseArithmetic, EnsureFixedPointNumber}, @@ -233,28 +197,6 @@ pub mod math { mod tests { use super::*; - mod get_source_address_bytes { - const EXPECTED: usize = 20; - use super::*; - - #[test] - fn get_source_address_bytes_works() { - let hash = [1u8; 20]; - - decode_var_source::(&hash).expect("address bytes from H160 works"); - - let str = String::from("d47ed02acbbb66ee8a3fe0275bd98add0aa607c3"); - - decode_var_source::(str.as_bytes()) - .expect("address bytes from un-prefixed hex works"); - - let str = String::from("0xd47ed02acbbb66ee8a3fe0275bd98add0aa607c3"); - - decode_var_source::(str.as_bytes()) - .expect("address bytes from prefixed hex works"); - } - } - mod vec_to_fixed_array { use super::*; diff --git a/pallets/axelar-router/src/lib.rs b/pallets/axelar-router/src/lib.rs index 23b1bed796..aee7a250cc 100644 --- a/pallets/axelar-router/src/lib.rs +++ b/pallets/axelar-router/src/lib.rs @@ -62,12 +62,6 @@ pub enum AxelarId { Evm(EVMChainId), } -impl Default for AxelarId { - fn default() -> Self { - Self::Evm(1) - } -} - /// Configuration for outbound messages though axelar #[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] pub struct AxelarConfig { @@ -242,9 +236,8 @@ pub mod pallet { match config.domain { DomainConfig::Evm(EvmConfig { chain_id, .. }) => { - let source_address_bytes = - cfg_utils::decode_var_source::(source_address) - .ok_or(Error::::InvalidSourceAddress)?; + let source_address_bytes = decode_var_source::(source_address) + .ok_or(Error::::InvalidSourceAddress)?; T::Receiver::receive( AxelarId::Evm(chain_id).into(), @@ -432,3 +425,64 @@ pub fn wrap_into_axelar_msg( Ok(encoded_axelar_contract) } + +/// Decodes the source address which can be: +/// - a 20 bytes array +/// - an hexadecimal character secuence (40 characters) +/// - an hexadecimal character secuence (40 characters) with 0x prefix +pub fn decode_var_source( + source_address: &[u8], +) -> Option<[u8; EXPECTED_SOURCE_ADDRESS_SIZE]> { + const HEX_PREFIX: &str = "0x"; + + let mut address = [0u8; EXPECTED_SOURCE_ADDRESS_SIZE]; + + if source_address.len() == EXPECTED_SOURCE_ADDRESS_SIZE { + address.copy_from_slice(source_address); + return Some(address); + } + + let try_bytes = match sp_std::str::from_utf8(source_address) { + Ok(res) => res.as_bytes(), + Err(_) => source_address, + }; + + // Attempt to hex decode source address. + let bytes = match hex::decode(try_bytes) { + Ok(res) => Some(res), + Err(_) => { + // Strip 0x prefix. + let res = try_bytes.strip_prefix(HEX_PREFIX.as_bytes())?; + + hex::decode(res).ok() + } + }?; + + if bytes.len() == EXPECTED_SOURCE_ADDRESS_SIZE { + address.copy_from_slice(bytes.as_slice()); + Some(address) + } else { + None + } +} + +#[cfg(test)] +mod test_decode_var_source { + const EXPECTED: usize = 20; + use super::*; + + #[test] + fn success() { + assert!(decode_var_source::(&[1; 20]).is_some()); + + assert!(decode_var_source::( + "d47ed02acbbb66ee8a3fe0275bd98add0aa607c3".as_bytes() + ) + .is_some()); + + assert!(decode_var_source::( + "0xd47ed02acbbb66ee8a3fe0275bd98add0aa607c3".as_bytes() + ) + .is_some()); + } +} diff --git a/pallets/liquidity-pools-gateway/queue/Cargo.toml b/pallets/liquidity-pools-gateway-queue/Cargo.toml similarity index 100% rename from pallets/liquidity-pools-gateway/queue/Cargo.toml rename to pallets/liquidity-pools-gateway-queue/Cargo.toml diff --git a/pallets/liquidity-pools-gateway/queue/src/lib.rs b/pallets/liquidity-pools-gateway-queue/src/lib.rs similarity index 99% rename from pallets/liquidity-pools-gateway/queue/src/lib.rs rename to pallets/liquidity-pools-gateway-queue/src/lib.rs index f473c20b42..ff3a45b7af 100644 --- a/pallets/liquidity-pools-gateway/queue/src/lib.rs +++ b/pallets/liquidity-pools-gateway-queue/src/lib.rs @@ -253,7 +253,7 @@ pub mod pallet { impl MessageQueueT for Pallet { type Message = T::Message; - fn submit(message: Self::Message) -> DispatchResult { + fn queue(message: Self::Message) -> DispatchResult { let nonce = >::try_mutate(|n| { n.ensure_add_assign(T::MessageNonce::one())?; Ok::(*n) diff --git a/pallets/liquidity-pools-gateway/queue/src/mock.rs b/pallets/liquidity-pools-gateway-queue/src/mock.rs similarity index 81% rename from pallets/liquidity-pools-gateway/queue/src/mock.rs rename to pallets/liquidity-pools-gateway-queue/src/mock.rs index f4a7af0895..2b0e4b99f9 100644 --- a/pallets/liquidity-pools-gateway/queue/src/mock.rs +++ b/pallets/liquidity-pools-gateway-queue/src/mock.rs @@ -12,17 +12,17 @@ // GNU General Public License for more details. use cfg_mocks::pallet_mock_liquidity_pools_gateway; -use cfg_primitives::LPGatewayQueueMessageNonce; -use cfg_types::domain_address::Domain; use frame_support::derive_impl; use crate::{self as pallet_liquidity_pools_gateway_queue, Config}; +type Nonce = u64; + frame_support::construct_runtime!( pub enum Runtime { System: frame_system, - LPGatewayMock: pallet_mock_liquidity_pools_gateway, - LPGatewayQueue: pallet_liquidity_pools_gateway_queue, + Processor: pallet_mock_liquidity_pools_gateway, + Queue: pallet_liquidity_pools_gateway_queue, } ); @@ -33,14 +33,14 @@ impl frame_system::Config for Runtime { } impl pallet_mock_liquidity_pools_gateway::Config for Runtime { - type Destination = Domain; + type Destination = (); type Message = u32; } impl Config for Runtime { type Message = u32; - type MessageNonce = LPGatewayQueueMessageNonce; - type MessageProcessor = LPGatewayMock; + type MessageNonce = Nonce; + type MessageProcessor = Processor; type RuntimeEvent = RuntimeEvent; } diff --git a/pallets/liquidity-pools-gateway/queue/src/tests.rs b/pallets/liquidity-pools-gateway-queue/src/tests.rs similarity index 67% rename from pallets/liquidity-pools-gateway/queue/src/tests.rs rename to pallets/liquidity-pools-gateway-queue/src/tests.rs index 6ee9e4e10b..1f89bac710 100644 --- a/pallets/liquidity-pools-gateway/queue/src/tests.rs +++ b/pallets/liquidity-pools-gateway-queue/src/tests.rs @@ -1,18 +1,11 @@ -use cfg_primitives::LPGatewayQueueMessageNonce; -use cfg_traits::liquidity_pools::MessageQueue as MessageQueueT; +use cfg_traits::liquidity_pools::MessageQueue as _; use frame_support::{ assert_noop, assert_ok, dispatch::RawOrigin, pallet_prelude::Hooks, weights::Weight, }; -use sp_runtime::{ - traits::{BadOrigin, One, Zero}, - DispatchError, -}; +use sp_runtime::{traits::BadOrigin, DispatchError}; use crate::{ - mock::{ - new_test_ext, LPGatewayMock, LPGatewayQueue, Runtime, RuntimeEvent as MockEvent, - RuntimeOrigin, - }, + mock::{new_test_ext, Processor, Queue, Runtime, RuntimeEvent as MockEvent, RuntimeOrigin}, Error, Event, FailedMessageQueue, MessageQueue, }; @@ -36,20 +29,17 @@ mod process_message { fn success() { new_test_ext().execute_with(|| { let message = 1; - let nonce = LPGatewayQueueMessageNonce::one(); + let nonce = 1; MessageQueue::::insert(nonce, message); - LPGatewayMock::mock_process(move |msg| { + Processor::mock_process(move |msg| { assert_eq!(msg, message); (Ok(()), Default::default()) }); - assert_ok!(LPGatewayQueue::process_message( - RuntimeOrigin::signed(1), - nonce - )); + assert_ok!(Queue::process_message(RuntimeOrigin::signed(1), nonce)); assert!(MessageQueue::::get(nonce).is_none()); @@ -60,13 +50,7 @@ mod process_message { #[test] fn failure_bad_origin() { new_test_ext().execute_with(|| { - assert_noop!( - LPGatewayQueue::process_message( - RawOrigin::None.into(), - LPGatewayQueueMessageNonce::zero(), - ), - BadOrigin, - ); + assert_noop!(Queue::process_message(RawOrigin::None.into(), 0), BadOrigin); }); } @@ -74,10 +58,7 @@ mod process_message { fn failure_message_not_found() { new_test_ext().execute_with(|| { assert_noop!( - LPGatewayQueue::process_message( - RuntimeOrigin::signed(1), - LPGatewayQueueMessageNonce::zero(), - ), + Queue::process_message(RuntimeOrigin::signed(1), 0,), Error::::MessageNotFound, ); }); @@ -87,22 +68,19 @@ mod process_message { fn failure_message_processor() { new_test_ext().execute_with(|| { let message = 1; - let nonce = LPGatewayQueueMessageNonce::one(); + let nonce = 1; MessageQueue::::insert(nonce, message); let error = DispatchError::Unavailable; - LPGatewayMock::mock_process(move |msg| { + Processor::mock_process(move |msg| { assert_eq!(msg, message); (Err(error), Default::default()) }); - assert_ok!(LPGatewayQueue::process_message( - RuntimeOrigin::signed(1), - nonce - )); + assert_ok!(Queue::process_message(RuntimeOrigin::signed(1), nonce)); assert_eq!( FailedMessageQueue::::get(nonce), @@ -125,18 +103,18 @@ mod process_failed_message { fn success() { new_test_ext().execute_with(|| { let message = 1; - let nonce = LPGatewayQueueMessageNonce::one(); + let nonce = 1; let error = DispatchError::Unavailable; FailedMessageQueue::::insert(nonce, (message, error)); - LPGatewayMock::mock_process(move |msg| { + Processor::mock_process(move |msg| { assert_eq!(msg, message); (Ok(()), Default::default()) }); - assert_ok!(LPGatewayQueue::process_failed_message( + assert_ok!(Queue::process_failed_message( RuntimeOrigin::signed(1), nonce )); @@ -151,10 +129,7 @@ mod process_failed_message { fn failure_bad_origin() { new_test_ext().execute_with(|| { assert_noop!( - LPGatewayQueue::process_failed_message( - RawOrigin::None.into(), - LPGatewayQueueMessageNonce::zero(), - ), + Queue::process_failed_message(RawOrigin::None.into(), 0,), BadOrigin, ); }); @@ -164,10 +139,7 @@ mod process_failed_message { fn failure_message_not_found() { new_test_ext().execute_with(|| { assert_noop!( - LPGatewayQueue::process_failed_message( - RuntimeOrigin::signed(1), - LPGatewayQueueMessageNonce::zero(), - ), + Queue::process_failed_message(RuntimeOrigin::signed(1), 0,), Error::::MessageNotFound, ); }); @@ -177,19 +149,19 @@ mod process_failed_message { fn failure_message_processor() { new_test_ext().execute_with(|| { let message = 1; - let nonce = LPGatewayQueueMessageNonce::one(); + let nonce = 1; let error = DispatchError::Unavailable; FailedMessageQueue::::insert(nonce, (message, error)); let error = DispatchError::Unavailable; - LPGatewayMock::mock_process(move |msg| { + Processor::mock_process(move |msg| { assert_eq!(msg, message); (Err(error), Default::default()) }); - assert_ok!(LPGatewayQueue::process_failed_message( + assert_ok!(Queue::process_failed_message( RuntimeOrigin::signed(1), nonce )); @@ -216,9 +188,9 @@ mod message_queue_impl { new_test_ext().execute_with(|| { let message = 1; - assert_ok!(LPGatewayQueue::submit(message)); + assert_ok!(Queue::queue(message)); - let nonce = LPGatewayQueueMessageNonce::one(); + let nonce = 1; assert_eq!(MessageQueue::::get(nonce), Some(message)); @@ -239,10 +211,10 @@ mod on_idle { new_test_ext().execute_with(|| { (1..=3).for_each(|i| MessageQueue::::insert(i as u64, i * 10)); - LPGatewayMock::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); - let handle = LPGatewayMock::mock_process(|_| (Ok(()), PROCESS_WEIGHT)); + Processor::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); + let handle = Processor::mock_process(|_| (Ok(()), PROCESS_WEIGHT)); - let weight = LPGatewayQueue::on_idle(0, TOTAL_WEIGHT); + let weight = Queue::on_idle(0, TOTAL_WEIGHT); assert_eq!(weight, PROCESS_WEIGHT * 3); assert_eq!(handle.times(), 3); @@ -256,10 +228,10 @@ mod on_idle { new_test_ext().execute_with(|| { (1..=5).for_each(|i| MessageQueue::::insert(i as u64, i * 10)); - LPGatewayMock::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); - let handle = LPGatewayMock::mock_process(|_| (Ok(()), PROCESS_WEIGHT)); + Processor::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); + let handle = Processor::mock_process(|_| (Ok(()), PROCESS_WEIGHT)); - let weight = LPGatewayQueue::on_idle(0, TOTAL_WEIGHT); + let weight = Queue::on_idle(0, TOTAL_WEIGHT); assert_eq!(weight, PROCESS_WEIGHT * 4); assert_eq!(handle.times(), 4); @@ -268,7 +240,7 @@ mod on_idle { // Next block - let weight = LPGatewayQueue::on_idle(0, TOTAL_WEIGHT); + let weight = Queue::on_idle(0, TOTAL_WEIGHT); assert_eq!(weight, PROCESS_WEIGHT); assert_eq!(handle.times(), 5); @@ -281,13 +253,13 @@ mod on_idle { new_test_ext().execute_with(|| { (1..=3).for_each(|i| MessageQueue::::insert(i as u64, i * 10)); - LPGatewayMock::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); - let handle = LPGatewayMock::mock_process(|msg| match msg { + Processor::mock_max_processing_weight(|_| PROCESS_LIMIT_WEIGHT); + let handle = Processor::mock_process(|msg| match msg { 20 => (Err(DispatchError::Unavailable), PROCESS_WEIGHT / 2), _ => (Ok(()), PROCESS_WEIGHT), }); - let weight = LPGatewayQueue::on_idle(0, TOTAL_WEIGHT); + let weight = Queue::on_idle(0, TOTAL_WEIGHT); assert_eq!(weight, PROCESS_WEIGHT * 2 + PROCESS_WEIGHT / 2); assert_eq!(handle.times(), 3); diff --git a/pallets/liquidity-pools-gateway/src/lib.rs b/pallets/liquidity-pools-gateway/src/lib.rs index 8e22be8714..8e6be6b4b8 100644 --- a/pallets/liquidity-pools-gateway/src/lib.rs +++ b/pallets/liquidity-pools-gateway/src/lib.rs @@ -114,7 +114,7 @@ pub mod pallet { >; /// An identification of a router - type RouterId: Parameter + MaxEncodedLen; + type RouterId: Parameter + MaxEncodedLen + Into; /// The type that provides the router available for a domain. type RouterProvider: RouterProvider; @@ -531,13 +531,14 @@ pub mod pallet { #[pallet::call_index(12)] pub fn initiate_message_recovery( origin: OriginFor, - domain: Domain, message_hash: MessageHash, recovery_router: [u8; 32], messaging_router: T::RouterId, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; + let domain = messaging_router.clone().into(); + let message = T::Message::initiate_recovery_message(message_hash, recovery_router); Self::send_recovery_message(domain, message, messaging_router.clone())?; @@ -560,13 +561,14 @@ pub mod pallet { #[pallet::call_index(13)] pub fn dispute_message_recovery( origin: OriginFor, - domain: Domain, message_hash: MessageHash, recovery_router: [u8; 32], messaging_router: T::RouterId, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; + let domain = messaging_router.clone().into(); + let message = T::Message::dispute_recovery_message(message_hash, recovery_router); Self::send_recovery_message(domain, message, messaging_router.clone())?; @@ -694,7 +696,7 @@ pub mod pallet { router_id, }; - T::MessageQueue::submit(gateway_message) + T::MessageQueue::queue(gateway_message) } } } diff --git a/pallets/liquidity-pools-gateway/src/message_processing.rs b/pallets/liquidity-pools-gateway/src/message_processing.rs index 720cd923e9..89230e18c6 100644 --- a/pallets/liquidity-pools-gateway/src/message_processing.rs +++ b/pallets/liquidity-pools-gateway/src/message_processing.rs @@ -487,7 +487,7 @@ impl Pallet { router_id, }; - T::MessageQueue::submit(gateway_message)?; + T::MessageQueue::queue(gateway_message)?; } Ok(()) diff --git a/pallets/liquidity-pools-gateway/src/mock.rs b/pallets/liquidity-pools-gateway/src/mock.rs index 8b86553d31..ced65ba8d8 100644 --- a/pallets/liquidity-pools-gateway/src/mock.rs +++ b/pallets/liquidity-pools-gateway/src/mock.rs @@ -1,6 +1,6 @@ use std::fmt::{Debug, Formatter}; -use cfg_mocks::{pallet_mock_liquidity_pools, pallet_mock_liquidity_pools_gateway_queue}; +use cfg_mocks::pallet_mock_liquidity_pools; use cfg_traits::liquidity_pools::{LpMessage, MessageHash, RouterProvider}; use cfg_types::{ domain_address::{Domain, DomainAddress}, @@ -119,11 +119,11 @@ impl LpMessage for Message { } } - fn initiate_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self { + fn initiate_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self { Self::InitiateMessageRecovery((hash, router)) } - fn dispute_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self { + fn dispute_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self { Self::DisputeMessageRecovery((hash, router)) } @@ -140,7 +140,7 @@ impl LpMessage for Message { } } -#[derive(Default, Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen, Hash)] +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen, Hash)] pub struct RouterId(pub u32); pub struct TestRouterProvider; @@ -156,11 +156,17 @@ impl RouterProvider for TestRouterProvider { } } +impl Into for RouterId { + fn into(self) -> Domain { + Domain::Evm(self.0.into()) + } +} + frame_support::construct_runtime!( pub enum Runtime { System: frame_system, MockLiquidityPools: pallet_mock_liquidity_pools, - MockLiquidityPoolsGatewayQueue: pallet_mock_liquidity_pools_gateway_queue, + MockLiquidityPoolsGatewayQueue: cfg_mocks::queue::pallet, MockMessageSender: cfg_mocks::router_message::pallet, LiquidityPoolsGateway: pallet_liquidity_pools_gateway, } @@ -179,7 +185,7 @@ impl pallet_mock_liquidity_pools::Config for Runtime { type Message = Message; } -impl pallet_mock_liquidity_pools_gateway_queue::Config for Runtime { +impl cfg_mocks::queue::pallet::Config for Runtime { type Message = GatewayMessage; } diff --git a/pallets/liquidity-pools-gateway/src/tests.rs b/pallets/liquidity-pools-gateway/src/tests.rs index 805e77c251..1a03f69a79 100644 --- a/pallets/liquidity-pools-gateway/src/tests.rs +++ b/pallets/liquidity-pools-gateway/src/tests.rs @@ -357,7 +357,7 @@ mod extrinsics { DOMAIN )); - let handler = MockLiquidityPoolsGatewayQueue::mock_submit(|_| Ok(())); + let handler = MockLiquidityPoolsGatewayQueue::mock_queue(|_| Ok(())); // Ok Batched assert_ok!(LiquidityPoolsGateway::handle(USER, DOMAIN, Message::Simple)); @@ -402,7 +402,7 @@ mod extrinsics { DOMAIN )); - let handler = MockLiquidityPoolsGatewayQueue::mock_submit(|_| Ok(())); + let handler = MockLiquidityPoolsGatewayQueue::mock_queue(|_| Ok(())); (0..MAX_PACKED_MESSAGES).for_each(|_| { assert_ok!(LiquidityPoolsGateway::handle(USER, DOMAIN, Message::Simple)); @@ -755,7 +755,6 @@ mod extrinsics { assert_ok!(LiquidityPoolsGateway::initiate_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -778,7 +777,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::initiate_message_recovery( RuntimeOrigin::signed(AccountId32::new([0u8; 32])), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -796,7 +794,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::initiate_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -827,7 +824,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::initiate_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, RouterId(4), @@ -860,7 +856,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::initiate_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -894,7 +889,6 @@ mod extrinsics { assert_ok!(LiquidityPoolsGateway::dispute_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -917,7 +911,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::dispute_message_recovery( RuntimeOrigin::signed(AccountId32::new([0u8; 32])), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -935,7 +928,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::dispute_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -966,7 +958,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::dispute_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, RouterId(4), @@ -999,7 +990,6 @@ mod extrinsics { assert_noop!( LiquidityPoolsGateway::dispute_message_recovery( RuntimeOrigin::root(), - TEST_DOMAIN, MESSAGE_HASH, recovery_router, ROUTER_ID_1, @@ -1030,7 +1020,7 @@ mod implementations { BoundedVec::try_from(vec![ROUTER_ID_1, ROUTER_ID_2, ROUTER_ID_3]).unwrap(), )); - let handler = MockLiquidityPoolsGatewayQueue::mock_submit(move |mock_msg| { + let handler = MockLiquidityPoolsGatewayQueue::mock_queue(move |mock_msg| { match mock_msg { GatewayMessage::Inbound { .. } => { assert!(false, "expected outbound message") @@ -1098,7 +1088,7 @@ mod implementations { let err = DispatchError::Unavailable; - let handler = MockLiquidityPoolsGatewayQueue::mock_submit(move |mock_msg| { + let handler = MockLiquidityPoolsGatewayQueue::mock_queue(move |mock_msg| { assert_eq!(mock_msg, gateway_message); Err(err) @@ -3375,7 +3365,7 @@ mod implementations { router_id: router_id.clone(), }; - let handler = MockLiquidityPoolsGatewayQueue::mock_submit(move |mock_message| { + let handler = MockLiquidityPoolsGatewayQueue::mock_queue(move |mock_message| { assert_eq!(mock_message, gateway_message); Ok(()) }); @@ -3439,7 +3429,7 @@ mod implementations { router_id: router_id.clone(), }; - MockLiquidityPoolsGatewayQueue::mock_submit(move |mock_message| { + MockLiquidityPoolsGatewayQueue::mock_queue(move |mock_message| { assert_eq!(mock_message, gateway_message); Err(err) }); diff --git a/pallets/liquidity-pools/src/message.rs b/pallets/liquidity-pools/src/message.rs index 8609a9eea7..f786600319 100644 --- a/pallets/liquidity-pools/src/message.rs +++ b/pallets/liquidity-pools/src/message.rs @@ -620,11 +620,11 @@ impl LpMessage for Message { } } - fn initiate_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self { + fn initiate_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self { Message::InitiateMessageRecovery { hash, router } } - fn dispute_recovery_message(hash: [u8; 32], router: [u8; 32]) -> Self { + fn dispute_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self { Message::DisputeMessageRecovery { hash, router } } diff --git a/runtime/integration-tests/src/cases.rs b/runtime/integration-tests/src/cases.rs index 38f6212f63..6a79cd5c84 100644 --- a/runtime/integration-tests/src/cases.rs +++ b/runtime/integration-tests/src/cases.rs @@ -4,13 +4,13 @@ mod block_rewards; mod currency_conversions; mod ethereum_transaction; mod example; +mod foreign_investments; mod investments; -mod liquidity_pools; -mod liquidity_pools_gateway_queue; mod loans; mod lp; mod oracles; mod proxy; +mod queue; mod restricted_transfers; mod routers; mod xcm_transfers; diff --git a/runtime/integration-tests/src/cases/assets.rs b/runtime/integration-tests/src/cases/assets.rs index 9ae8147d0e..6635caa759 100644 --- a/runtime/integration-tests/src/cases/assets.rs +++ b/runtime/integration-tests/src/cases/assets.rs @@ -1,5 +1,5 @@ use cfg_types::tokens::{default_metadata, CurrencyId}; -use frame_support::{assert_noop, assert_ok, dispatch::RawOrigin}; +use frame_support::{assert_err, assert_ok, dispatch::RawOrigin}; use sp_runtime::{DispatchError, DispatchError::BadOrigin}; use crate::{config::Runtime, env::Env, envs::runtime_env::RuntimeEnv, utils::orml_asset_registry}; @@ -21,7 +21,7 @@ fn authority_configured() { Some(CurrencyId::ForeignAsset(42)) )); - assert_noop!( + assert_err!( orml_asset_registry::Pallet::::register_asset( RawOrigin::Root.into(), default_metadata(), @@ -37,7 +37,7 @@ fn processor_configured() { let mut env = RuntimeEnv::::default(); env.parachain_state_mut(|| { - assert_noop!( + assert_err!( orml_asset_registry::Pallet::::register_asset( RawOrigin::Root.into(), default_metadata(), diff --git a/runtime/integration-tests/src/cases/example.rs b/runtime/integration-tests/src/cases/example.rs index 22d2915bbe..862d803ee8 100644 --- a/runtime/integration-tests/src/cases/example.rs +++ b/runtime/integration-tests/src/cases/example.rs @@ -68,7 +68,11 @@ fn transfer_balance() { } // Identical to `transfer_balance()` test but using fudge. -#[test_runtimes([development, altair, centrifuge], ignore = "uncomment to run the example")] +// +// NOTE: this test fails checking the events if compiled in debug +// (which implies using std in the runtimes), compiling this in release (which +// implies using no-std in the runtimes) works. TODO: investigate why. +#[test_runtimes([development, altair, centrifuge])] fn fudge_transfer_balance() { const TRANSFER: Balance = 1000 * CFG; const FOR_FEES: Balance = 1 * CFG; diff --git a/runtime/integration-tests/src/cases/foreign_investments.rs b/runtime/integration-tests/src/cases/foreign_investments.rs new file mode 100644 index 0000000000..1927efe523 --- /dev/null +++ b/runtime/integration-tests/src/cases/foreign_investments.rs @@ -0,0 +1,2155 @@ +use cfg_primitives::{ + currency_decimals, parachains, AccountId, Balance, InvestmentId, OrderId, PoolId, TrancheId, +}; +use cfg_traits::{ + investments::OrderManager, liquidity_pools::InboundMessageHandler, IdentityCurrencyConversion, + Permissions, PoolInspect, PoolMutate, Seconds, +}; +use cfg_types::{ + domain_address::{Domain, DomainAddress}, + fixed_point::Ratio, + investments::{InvestCollection, InvestmentAccount, RedeemCollection}, + orders::FulfillmentWithPrice, + permissions::{PermissionScope, PoolRole, Role}, + pools::TrancheMetadata, + tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}, +}; +use frame_support::{ + assert_noop, assert_ok, + dispatch::RawOrigin, + traits::{ + fungibles::{Inspect, Mutate as FungiblesMutate}, + OriginTrait, PalletInfo, + }, +}; +use pallet_axelar_router::AxelarId; +use pallet_foreign_investments::ForeignInvestmentInfo; +use pallet_investments::CollectOutcome; +use pallet_liquidity_pools::Message; +use pallet_liquidity_pools_gateway::message::GatewayMessage; +use pallet_liquidity_pools_gateway_queue::MessageNonceStore; +use pallet_pool_system::tranches::{TrancheInput, TrancheLoc, TrancheType}; +use runtime_common::{ + foreign_investments::IdentityPoolCurrencyConverter, routing::RouterId, xcm::general_key, +}; +use sp_core::{Get, H160}; +use sp_runtime::{ + traits::{AccountIdConversion, EnsureAdd, One, Zero}, + BoundedVec, DispatchError, FixedPointNumber, Perquintill, SaturatedConversion, +}; +use staging_xcm::{ + v4::{Junction::*, Location, NetworkId}, + VersionedLocation, +}; + +use crate::{ + config::Runtime, + env::Env, + envs::{fudge_env::handle::SIBLING_ID, runtime_env::RuntimeEnv}, + utils::{accounts::Keyring, genesis, genesis::Genesis, orml_asset_registry}, +}; + +// ------------------ +// NOTE +// This file only contains foreign investments tests, but the name must remain +// as it is until feature lpv2 is merged to avoid conflicts: +// (https://github.com/centrifuge/centrifuge-chain/pull/1909) +// ------------------ + +/// The AUSD asset id +pub const AUSD_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(3); +/// The USDT asset id +pub const USDT_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1); + +pub const AUSD_ED: Balance = 1_000_000_000; +pub const USDT_ED: Balance = 10_000; + +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 POOL_ADMIN: Keyring = Keyring::Bob; +pub const POOL_ID: PoolId = 42; +pub const CHAIN_ID: u64 = 1284; +pub const DEFAULT_VALIDITY: Seconds = 2555583502; +pub const DOMAIN_MOONBEAM: Domain = Domain::Evm(CHAIN_ID); +pub const DEFAULT_DOMAIN_ADDRESS_MOONBEAM: DomainAddress = + DomainAddress::Evm(CHAIN_ID, H160::repeat_byte(99)); +pub const DEFAULT_OTHER_DOMAIN_ADDRESS: DomainAddress = + DomainAddress::Evm(CHAIN_ID, H160::repeat_byte(0)); +pub const DEFAULT_ROUTER_ID: RouterId = RouterId::Axelar(AxelarId::Evm(CHAIN_ID)); + +pub type LiquidityPoolMessage = Message; + +pub mod utils { + use cfg_types::oracles::OracleKey; + use runtime_common::oracle::Feeder; + + 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, decimals(currency_decimals::AUSD)) + } + + pub fn register_ausd() { + let meta: AssetMetadata = AssetMetadata { + decimals: 12, + name: BoundedVec::default(), + symbol: BoundedVec::default(), + existential_deposit: 1_000_000_000, + location: Some(VersionedLocation::V4(Location::new( + 1, + [ + Parachain(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 cfg(amount: Balance) -> Balance { + amount * decimals(currency_decimals::NATIVE) + } + + pub fn decimals(decimals: u32) -> Balance { + 10u128.saturating_pow(decimals) + } + + /// 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( + POOL_ADMIN.into(), + POOL_ADMIN.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, + ::StringLimit, + >::try_from( + "A highly advanced tranche".as_bytes().to_vec() + ) + .expect("Can create BoundedVec for token name"), + token_symbol: BoundedVec::< + u8, + ::StringLimit, + >::try_from("TrNcH".as_bytes().to_vec()) + .expect("Can create BoundedVec for token symbol"), + } + }, + 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, + // No pool fees per default + vec![] + )); + } + + pub fn register_glmr() { + let meta: AssetMetadata = AssetMetadata { + decimals: 18, + name: BoundedVec::default(), + symbol: BoundedVec::default(), + existential_deposit: GLMR_ED, + location: Some(VersionedLocation::V4(Location::new( + 1, + [Parachain(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 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 `VersionedLocation` 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], + ) -> VersionedLocation { + VersionedLocation::V4(Location::new( + 0, + [ + 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 `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::( + 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 RuntimeEnv) { + env.parachain_state_mut(|| { + register_ausd::(); + register_glmr::(); + + assert_ok!(orml_tokens::Pallet::::set_balance( + ::RuntimeOrigin::root(), + T::Sender::get().account().into(), + GLMR_CURRENCY_ID, + DEFAULT_BALANCE_GLMR, + 0, + )); + + assert_ok!(pallet_liquidity_pools_gateway::Pallet::::set_routers( + ::RuntimeOrigin::root(), + BoundedVec::try_from(vec![DEFAULT_ROUTER_ID]).unwrap(), + )); + }); + } + + /// 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") + } + + pub fn default_investment_id() -> InvestmentId { + (POOL_ID, default_tranche_id::(POOL_ID)) + } + + pub fn default_order_id(investor: &AccountId) -> OrderId { + pallet_foreign_investments::Pallet::::order_id( + &investor, + default_investment_id::(), + pallet_foreign_investments::Action::Investment, + ) + .expect("Swap order exists; qed") + } + + /// 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() + } + + pub fn fulfill_swap_into_pool( + pool_id: u64, + swap_order_id: u64, + amount_pool: Balance, + amount_foreign: Balance, + trader: AccountId, + ) { + let pool_currency: CurrencyId = pallet_pool_system::Pallet::::currency_for(pool_id) + .expect("Pool existence checked already"); + assert_ok!(orml_tokens::Pallet::::mint_into( + pool_currency, + &trader, + amount_pool + )); + assert_ok!(pallet_order_book::Pallet::::fill_order( + RawOrigin::Signed(trader.clone()).into(), + swap_order_id, + amount_foreign + )); + } + + /// Sets up required permissions for the investor and executes an + /// initial investment via LiquidityPools by executing + /// `DepositRequest`. + /// + /// 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, + ) { + 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::DepositRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + DispatchError::Other("Account does not have the TrancheInvestor permission.") + ); + } + + // Make investor the MembersListAdmin of this Pool + if !pallet_permissions::Pallet::::has( + PermissionScope::Pool(pool_id), + investor.clone(), + Role::PoolRole(PoolRole::TrancheInvestor( + default_tranche_id::(pool_id), + DEFAULT_VALIDITY, + )), + ) { + crate::utils::pool::give_role::( + investor.clone(), + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); + } + + let amount_before = + orml_tokens::Pallet::::balance(currency_id, &default_investment_account::()); + let final_amount = amount_before + .ensure_add(amount) + .expect("Should not overflow when incrementing amount"); + + // Execute byte message + assert_ok!(pallet_liquidity_pools::Pallet::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + if currency_id == pool_currency { + // 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_investments::Event::::InvestOrderUpdated { + investment_id: default_investment_id::(), + submitted_at: 0, + who: investor.clone(), + amount: final_amount, + } + .into() + })); + } + } + + /// Sets up required permissions for the investor and executes an + /// initial redemption via LiquidityPools by executing + /// `RedeemRequest`. + /// + /// 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, + ) { + // 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(), + &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), + 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::RedeemRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + ), + DispatchError::Other("Account does not have the TrancheInvestor permission.") + ); + + // Make investor the MembersListAdmin of this Pool + crate::utils::pool::give_role::( + investor.clone(), + pool_id, + PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), + ); + + assert_ok!(pallet_liquidity_pools::Pallet::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // 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(), + &DEFAULT_OTHER_DOMAIN_ADDRESS.account(), + ), + 0 + ); + 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(( + 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: BoundedVec::default(), + symbol: BoundedVec::default(), + existential_deposit: USDT_ED, + location: Some(VersionedLocation::V4(Location::new( + 1, + [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 with + /// conversion ratio one 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, + ) -> 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); + } + + assert_ok!(pallet_order_book::Pallet::::set_market_feeder( + ::RuntimeOrigin::root(), + Feeder::root(), + )); + crate::utils::oracle::update_feeders::(POOL_ADMIN.id(), POOL_ID, [Feeder::root()]); + + if enable_foreign_to_pool_pair { + crate::utils::oracle::feed_from_root::( + OracleKey::ConversionRatio(foreign_currency, pool_currency), + Ratio::one(), + ); + } + if enable_pool_to_foreign_pair { + crate::utils::oracle::feed_from_root::( + OracleKey::ConversionRatio(pool_currency, foreign_currency), + Ratio::one(), + ); + } + + amount_foreign_denominated + } + + pub fn outbound_message_dispatched(f: impl Fn() -> ()) -> bool { + let events_before = frame_system::Pallet::::events(); + + f(); + + frame_system::Pallet::::events() + .into_iter() + .filter(|e1| !events_before.iter().any(|e2| e1 == e2)) + .any(|e| { + if let Ok(event) = e.event.clone().try_into() + as Result, _> + { + match event { + pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { + .. + } => true, + _ => false, + } + } else { + false + } + }) + } +} + +use utils::*; + +mod same_currencies { + use super::*; + + #[test_runtimes([development])] + fn increase_deposit_request() { + let mut env = RuntimeEnv::::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 = POOL_ID; + let amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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); + + // 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::DepositRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + }); + } + + #[test_runtimes([development])] + fn decrease_deposit_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let invest_amount: u128 = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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, + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::CancelDepositRequest { + 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 `FulfilledCancelDepositRequest` message. + assert_noop!( + pallet_liquidity_pools::Pallet::::handle( + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // Verify investment was decreased into 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 + ); + }); + } + + #[test_runtimes([development])] + fn cancel_deposit_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let invest_amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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, + ); + + // 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::CancelDepositRequest { + 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 `FulfilledCancelDepositRequest` message. + assert_noop!( + pallet_liquidity_pools::Pallet::::handle( + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + + // 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 + ); + }); + } + + #[test_runtimes([development])] + fn collect_deposit_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let sending_domain_locator = DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); + 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); + 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 + ); + + // Collect investment + assert_ok!(pallet_investments::Pallet::::collect_investments_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + investor.clone(), + default_investment_id::() + )); + + // 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() + })); + + let nonce = MessageNonceStore::::get(); + + // Clearing of foreign InvestState should be dispatched + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledDepositRequest { + 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, + }, + }, + } + .into() + })); + }); + } + + #[test_runtimes([development])] + fn collect_investment() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let invest_amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let sending_domain_locator = DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); + create_currency_pool::(pool_id, currency_id, currency_decimals.into()); + do_initial_increase_investment::( + pool_id, + invest_amount, + investor.clone(), + currency_id, + ); + enable_liquidity_pool_transferability::(currency_id); + let investment_currency_id: CurrencyId = default_investment_id::().into(); + + // 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(), + } + )); + + // 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::() + )); + + // 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() + })); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: Message::FulfilledDepositRequest { + 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, + }, + }, + } + .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::() + )); + + // 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() + })); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledDepositRequest { + 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, + }, + }, + } + .into() + })); + + // Collecting through investments should not mutate any state + let events_before = frame_system::Pallet::::events(); + let info_before = + ForeignInvestmentInfo::::get(&investor, default_investment_id::()); + assert_ok!(pallet_investments::Pallet::::collect_investments_for( + RawOrigin::Signed(Keyring::Alice.into()).into(), + investor.clone(), + default_investment_id::() + )); + assert!(!frame_system::Pallet::::events() + .into_iter() + .filter(|e1| !events_before.iter().any(|e2| e1 == e2)) + .any(|e| { + if let Ok(event) = e.event.clone().try_into() + as Result, _> + { + match event { + pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { + message: + GatewayMessage::Outbound { + router_id: event_router_id, + message: Message::FulfilledDepositRequest { .. }, + }, + .. + } => event_router_id == DEFAULT_ROUTER_ID, + _ => false, + } + } else { + false + } + })); + assert_eq!( + ForeignInvestmentInfo::::get(investor, default_investment_id::()), + info_before + ); + }); + } + + #[test_runtimes([development])] + fn increase_redeem_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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(), + &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), + amount + )); + let msg = LiquidityPoolMessage::RedeemRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg + )); + }); + } + + #[test_runtimes([development])] + fn cancel_redeem_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let redeem_amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let currency_id = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let sending_domain_locator = DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); + + // 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::::redeem_order_id(default_investment_id::()), + 0 + ); + + // Mock incoming decrease message + let msg = LiquidityPoolMessage::CancelRedeemRequest { + 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::::handle( + 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 + ); + + // 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 + ); + }); + } + + #[test_runtimes([development])] + fn collect_redeem_request() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let redeem_amount = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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 + )); + + // 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(), + } + )); + + // 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() + })); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledRedeemRequest { + 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, + }, + }, + } + .into() + })); + // Since foreign currency is pool currency, the swap is immediately fulfilled + // and FulfilledRedeemRequest dispatched + 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!(!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())); + + let nonce = MessageNonceStore::::get(); + + // Clearing of foreign RedeemState should have been dispatched exactly once + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledRedeemRequest { + 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, + }, + }, + } + .into() + })); + }); + } + + mod should_fail { + use super::*; + + mod should_throw_requires_collect { + use super::*; + + #[test_runtimes([development])] + fn invest_requires_collect() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let amount: u128 = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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, + ); + 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::DepositRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + increase_msg + ), + pallet_investments::Error::::CollectRequired + ); + + // Should fail to decrease + let decrease_msg = LiquidityPoolMessage::CancelDepositRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_investments::Error::::CollectRequired + ); + }); + } + + #[test_runtimes([development])] + fn redeem_requires_collect() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let amount: u128 = 10 * decimals(12); + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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(), + &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), + 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::RedeemRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + increase_msg + ), + pallet_investments::Error::::CollectRequired + ); + + // Should fail to decrease + let decrease_msg = LiquidityPoolMessage::CancelRedeemRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_investments::Error::::CollectRequired + ); + }); + } + } + + mod payment_payout_currency { + use super::*; + + #[test_runtimes([development])] + fn redeem_payout_currency_not_found() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let pool_currency = AUSD_CURRENCY_ID; + let currency_decimals = currency_decimals::AUSD; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let amount = 6 * decimals(18); + + create_currency_pool::(pool_id, pool_currency, currency_decimals.into()); + do_initial_increase_redemption::( + pool_id, + amount, + investor.clone(), + pool_currency, + ); + enable_usdt_trading::(pool_currency, amount, true, true, true); + assert_ok!(orml_tokens::Pallet::::mint_into( + default_investment_id::().into(), + &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), + amount, + )); + + // Should fail to decrease or collect for another + // foreign currency as long as `RedemptionState` + // exists + let decrease_msg = LiquidityPoolMessage::CancelRedeemRequest { + 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::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + decrease_msg + ), + pallet_foreign_investments::Error::::MismatchedForeignCurrency + ); + }); + } + } + } +} + +mod mismatching_currencies { + use pallet_liquidity_pools_gateway_queue::MessageNonceStore; + + use super::*; + + #[test_runtimes([development])] + fn collect_foreign_investment_for() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + 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 * decimals(18); + let sending_domain_locator = DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); + let trader: AccountId = Keyring::Alice.into(); + create_currency_pool::(pool_id, pool_currency, pool_currency_decimals.into()); + + // USDT investment preparations + let invest_amount_foreign_denominated = 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 first investment and fulfill swap order + do_initial_increase_investment::( + pool_id, + invest_amount_foreign_denominated, + investor.clone(), + foreign_currency, + ); + fulfill_swap_into_pool::( + pool_id, + default_order_id::(&investor), + invest_amount_pool_denominated, + invest_amount_foreign_denominated, + trader, + ); + + // Increase invest order to initialize ForeignInvestmentInfo + let msg = LiquidityPoolMessage::DepositRequest { + 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::::handle( + 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!(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 + ); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledDepositRequest { + 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: 2 * invest_amount_pool_denominated, + }, + }, + } + .into() + })); + }); + } + + /// Invest, fulfill swap foreign->pool, cancel, fulfill swap + /// pool->foreign + #[test_runtimes([development])] + fn cancel_unprocessed_investment() { + let mut env = RuntimeEnv::::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 = POOL_ID; + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let trader: AccountId = Keyring::Alice.into(); + let pool_currency: CurrencyId = AUSD_CURRENCY_ID; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let pool_currency_decimals = currency_decimals::AUSD; + let invest_amount_pool_denominated: u128 = 10 * decimals(18); + 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, + ); + + // Increase such that active swap into USDT is initialized + do_initial_increase_investment::( + pool_id, + invest_amount_foreign_denominated, + investor.clone(), + foreign_currency, + ); + + // Fulfilling order should propagate it from swapping to investing + let swap_order_id = default_order_id::(&investor); + fulfill_swap_into_pool::( + pool_id, + swap_order_id, + invest_amount_pool_denominated, + invest_amount_foreign_denominated, + trader.clone(), + ); + 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, + currency_in: pool_currency, + currency_out: foreign_currency, + ratio: Ratio::one(), + } + .into() + })); + + // Cancel investment + let msg = LiquidityPoolMessage::CancelDepositRequest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(foreign_currency), + }; + + // FulfilledCancel message dispatch blocked until pool currency is swapped back + // to foreign + assert!(!outbound_message_dispatched::(|| { + assert_ok!(pallet_liquidity_pools::Pallet::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + )); + })); + + assert!(!outbound_message_dispatched::(|| { + assert_ok!(pallet_order_book::Pallet::::fill_order( + RawOrigin::Signed(trader.clone()).into(), + default_order_id::(&investor), + invest_amount_pool_denominated / 4 + )); + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_order_book::Event::::OrderFulfillment { + order_id: default_order_id::(&investor), + placing_account: investor.clone(), + fulfilling_account: trader.clone(), + partial_fulfillment: true, + fulfillment_amount: invest_amount_pool_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency, + ratio: Ratio::one(), + } + .into() + })); + })); + + let swap_order_id = default_order_id::(&investor); + assert_ok!(pallet_order_book::Pallet::::fill_order( + RawOrigin::Signed(trader.clone()).into(), + swap_order_id, + invest_amount_pool_denominated / 4 * 3 + )); + 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 / 4 * 3, + currency_in: foreign_currency, + currency_out: pool_currency, + ratio: Ratio::one(), + } + .into() + })); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledCancelDepositRequest { + 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, + fulfilled_invest_amount: invest_amount_foreign_denominated, + }, + }, + } + .into() + })); + }); + } + + /// Invest, fulfill swap foreign->pool, process 50% of investment, + /// cancel, swap back pool->foreign of remaining unprocessed investment + #[test_runtimes([development])] + fn cancel_partially_processed_investment() { + let mut env = RuntimeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = POOL_ID; + let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); + let pool_currency: CurrencyId = AUSD_CURRENCY_ID; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let pool_currency_decimals = currency_decimals::AUSD; + let invest_amount_pool_denominated: u128 = 10 * decimals(18); + let trader: AccountId = Keyring::Alice.into(); + create_currency_pool::(pool_id, pool_currency, pool_currency_decimals.into()); + + // USDT investment preparations + let invest_amount_foreign_denominated = enable_usdt_trading::( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + ); + + // Do first investment and fulfill swap order + do_initial_increase_investment::( + pool_id, + invest_amount_foreign_denominated, + investor.clone(), + foreign_currency, + ); + fulfill_swap_into_pool::( + pool_id, + default_order_id::(&investor), + invest_amount_pool_denominated, + invest_amount_foreign_denominated, + trader.clone(), + ); + + // Process 50% 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::from_percent(50), + 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::() + )); + + // Cancel pending deposit request: FulfilledCancel message blocked until pool + // currency is fully swapped back to foreign one + assert!(!outbound_message_dispatched::(|| { + let cancel_msg = LiquidityPoolMessage::CancelDepositRequest { + pool_id, + tranche_id: default_tranche_id::(pool_id), + investor: investor.clone().into(), + currency: general_currency_index::(foreign_currency), + }; + + assert_ok!(pallet_liquidity_pools::Pallet::::handle( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + cancel_msg + )); + })); + + assert_ok!(pallet_order_book::Pallet::::fill_order( + RawOrigin::Signed(trader.clone()).into(), + default_order_id::(&investor), + invest_amount_pool_denominated / 2 + )); + + let nonce = MessageNonceStore::::get(); + + assert!(frame_system::Pallet::::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { + nonce, + message: GatewayMessage::Outbound { + router_id: DEFAULT_ROUTER_ID, + message: LiquidityPoolMessage::FulfilledCancelDepositRequest { + 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, + fulfilled_invest_amount: invest_amount_foreign_denominated / 2, + }, + }, + } + .into() + })); + }); + } +} diff --git a/runtime/integration-tests/src/cases/liquidity_pools.rs b/runtime/integration-tests/src/cases/liquidity_pools.rs deleted file mode 100644 index 328bb305e4..0000000000 --- a/runtime/integration-tests/src/cases/liquidity_pools.rs +++ /dev/null @@ -1,2208 +0,0 @@ -use cfg_primitives::{ - currency_decimals, parachains, AccountId, Balance, InvestmentId, OrderId, PoolId, TrancheId, -}; -use cfg_traits::{ - investments::OrderManager, liquidity_pools::InboundMessageHandler, IdentityCurrencyConversion, - Permissions, PoolInspect, PoolMutate, Seconds, -}; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - fixed_point::Ratio, - investments::{InvestCollection, InvestmentAccount, RedeemCollection}, - orders::FulfillmentWithPrice, - permissions::{PermissionScope, PoolRole, Role}, - pools::TrancheMetadata, - tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}, -}; -use frame_support::{ - assert_noop, assert_ok, - dispatch::RawOrigin, - traits::{ - fungibles::{Inspect, Mutate as FungiblesMutate}, - OriginTrait, PalletInfo, - }, -}; -use pallet_axelar_router::AxelarId; -use pallet_foreign_investments::ForeignInvestmentInfo; -use pallet_investments::CollectOutcome; -use pallet_liquidity_pools::Message; -use pallet_liquidity_pools_gateway::message::GatewayMessage; -use pallet_liquidity_pools_gateway_queue::MessageNonceStore; -use pallet_pool_system::tranches::{TrancheInput, TrancheLoc, TrancheType}; -use runtime_common::{ - foreign_investments::IdentityPoolCurrencyConverter, routing::RouterId, xcm::general_key, -}; -use sp_core::{Get, H160}; -use sp_runtime::{ - traits::{AccountIdConversion, EnsureAdd, One, Zero}, - BoundedVec, DispatchError, FixedPointNumber, Perquintill, SaturatedConversion, -}; -use staging_xcm::{ - v4::{Junction::*, Location, NetworkId}, - VersionedLocation, -}; - -use crate::{ - config::Runtime, - env::Env, - envs::{fudge_env::handle::SIBLING_ID, runtime_env::RuntimeEnv}, - utils::{accounts::Keyring, genesis, genesis::Genesis, orml_asset_registry}, -}; - -// ------------------ -// NOTE -// This file only contains foreign investments tests, but the name must remain -// as it is until feature lpv2 is merged to avoid conflicts: -// (https://github.com/centrifuge/centrifuge-chain/pull/1909) -// ------------------ - -/// The AUSD asset id -pub const AUSD_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(3); -/// The USDT asset id -pub const USDT_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1); - -pub const AUSD_ED: Balance = 1_000_000_000; -pub const USDT_ED: Balance = 10_000; - -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 POOL_ADMIN: Keyring = Keyring::Bob; -pub const POOL_ID: PoolId = 42; -pub const CHAIN_ID: u64 = 1284; -pub const DEFAULT_VALIDITY: Seconds = 2555583502; -pub const DOMAIN_MOONBEAM: Domain = Domain::Evm(CHAIN_ID); -pub const DEFAULT_DOMAIN_ADDRESS_MOONBEAM: DomainAddress = - DomainAddress::Evm(CHAIN_ID, H160::repeat_byte(99)); -pub const DEFAULT_OTHER_DOMAIN_ADDRESS: DomainAddress = - DomainAddress::Evm(CHAIN_ID, H160::repeat_byte(0)); -pub const DEFAULT_ROUTER_ID: RouterId = RouterId::Axelar(AxelarId::Evm(CHAIN_ID)); - -pub type LiquidityPoolMessage = Message; - -pub mod utils { - use cfg_types::oracles::OracleKey; - use runtime_common::oracle::Feeder; - - 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, decimals(currency_decimals::AUSD)) - } - - pub fn register_ausd() { - let meta: AssetMetadata = AssetMetadata { - decimals: 12, - name: BoundedVec::default(), - symbol: BoundedVec::default(), - existential_deposit: 1_000_000_000, - location: Some(VersionedLocation::V4(Location::new( - 1, - [ - Parachain(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 cfg(amount: Balance) -> Balance { - amount * decimals(currency_decimals::NATIVE) - } - - pub fn decimals(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) - } - - /// 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( - POOL_ADMIN.into(), - POOL_ADMIN.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, - ::StringLimit, - >::try_from( - "A highly advanced tranche".as_bytes().to_vec() - ) - .expect("Can create BoundedVec for token name"), - token_symbol: BoundedVec::< - u8, - ::StringLimit, - >::try_from("TrNcH".as_bytes().to_vec()) - .expect("Can create BoundedVec for token symbol"), - } - }, - 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, - // No pool fees per default - vec![] - )); - } - - pub fn register_glmr() { - let meta: AssetMetadata = AssetMetadata { - decimals: 18, - name: BoundedVec::default(), - symbol: BoundedVec::default(), - existential_deposit: GLMR_ED, - location: Some(VersionedLocation::V4(Location::new( - 1, - [Parachain(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 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 `VersionedLocation` 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], - ) -> VersionedLocation { - VersionedLocation::V4(Location::new( - 0, - [ - 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 `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::( - 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 RuntimeEnv) { - env.parachain_state_mut(|| { - register_ausd::(); - register_glmr::(); - - assert_ok!(orml_tokens::Pallet::::set_balance( - ::RuntimeOrigin::root(), - T::Sender::get().account().into(), - GLMR_CURRENCY_ID, - DEFAULT_BALANCE_GLMR, - 0, - )); - - assert_ok!(pallet_liquidity_pools_gateway::Pallet::::set_routers( - ::RuntimeOrigin::root(), - BoundedVec::try_from(vec![DEFAULT_ROUTER_ID]).unwrap(), - )); - }); - } - - /// 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") - } - - pub fn default_investment_id() -> InvestmentId { - (POOL_ID, default_tranche_id::(POOL_ID)) - } - - pub fn default_order_id(investor: &AccountId) -> OrderId { - pallet_foreign_investments::Pallet::::order_id( - &investor, - default_investment_id::(), - pallet_foreign_investments::Action::Investment, - ) - .expect("Swap order exists; qed") - } - - /// 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() - } - - pub fn fulfill_swap_into_pool( - pool_id: u64, - swap_order_id: u64, - amount_pool: Balance, - amount_foreign: Balance, - trader: AccountId, - ) { - let pool_currency: CurrencyId = pallet_pool_system::Pallet::::currency_for(pool_id) - .expect("Pool existence checked already"); - assert_ok!(orml_tokens::Pallet::::mint_into( - pool_currency, - &trader, - amount_pool - )); - assert_ok!(pallet_order_book::Pallet::::fill_order( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - amount_foreign - )); - } - - /// Sets up required permissions for the investor and executes an - /// initial investment via LiquidityPools by executing - /// `DepositRequest`. - /// - /// 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, - ) { - 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::DepositRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - ), - DispatchError::Other("Account does not have the TrancheInvestor permission.") - ); - } - - // Make investor the MembersListAdmin of this Pool - if !pallet_permissions::Pallet::::has( - PermissionScope::Pool(pool_id), - investor.clone(), - Role::PoolRole(PoolRole::TrancheInvestor( - default_tranche_id::(pool_id), - DEFAULT_VALIDITY, - )), - ) { - crate::utils::pool::give_role::( - investor.clone(), - pool_id, - PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), - ); - } - - let amount_before = - orml_tokens::Pallet::::balance(currency_id, &default_investment_account::()); - let final_amount = amount_before - .ensure_add(amount) - .expect("Should not overflow when incrementing amount"); - - // Execute byte message - assert_ok!(pallet_liquidity_pools::Pallet::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - - if currency_id == pool_currency { - // 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_investments::Event::::InvestOrderUpdated { - investment_id: default_investment_id::(), - submitted_at: 0, - who: investor.clone(), - amount: final_amount, - } - .into() - })); - } - } - - /// Sets up required permissions for the investor and executes an - /// initial redemption via LiquidityPools by executing - /// `RedeemRequest`. - /// - /// 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, - ) { - // 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(), - &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), - 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::RedeemRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - ), - DispatchError::Other("Account does not have the TrancheInvestor permission.") - ); - - // Make investor the MembersListAdmin of this Pool - crate::utils::pool::give_role::( - investor.clone(), - pool_id, - PoolRole::TrancheInvestor(default_tranche_id::(pool_id), DEFAULT_VALIDITY), - ); - - assert_ok!(pallet_liquidity_pools::Pallet::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - - // 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(), - &DEFAULT_OTHER_DOMAIN_ADDRESS.account(), - ), - 0 - ); - 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(( - 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: BoundedVec::default(), - symbol: BoundedVec::default(), - existential_deposit: USDT_ED, - location: Some(VersionedLocation::V4(Location::new( - 1, - [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 with - /// conversion ratio one 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, - ) -> 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); - } - - assert_ok!(pallet_order_book::Pallet::::set_market_feeder( - ::RuntimeOrigin::root(), - Feeder::root(), - )); - crate::utils::oracle::update_feeders::(POOL_ADMIN.id(), POOL_ID, [Feeder::root()]); - - if enable_foreign_to_pool_pair { - crate::utils::oracle::feed_from_root::( - OracleKey::ConversionRatio(foreign_currency, pool_currency), - Ratio::one(), - ); - } - if enable_pool_to_foreign_pair { - crate::utils::oracle::feed_from_root::( - OracleKey::ConversionRatio(pool_currency, foreign_currency), - Ratio::one(), - ); - } - - amount_foreign_denominated - } - - /// Adds bidirectional trading pairs with conversion ratio one. - pub fn enable_symmetric_trading_pair( - currency_1: CurrencyId, - currency_2: CurrencyId, - pool_admin: AccountId, - pool_id: PoolId, - ) { - assert_ok!(pallet_order_book::Pallet::::set_market_feeder( - ::RuntimeOrigin::root(), - Feeder::root(), - )); - crate::utils::oracle::update_feeders::(pool_admin, pool_id, [Feeder::root()]); - crate::utils::oracle::feed_from_root::( - OracleKey::ConversionRatio(currency_1, currency_2), - Ratio::one(), - ); - crate::utils::oracle::feed_from_root::( - OracleKey::ConversionRatio(currency_2, currency_1), - Ratio::one(), - ); - } - - pub fn outbound_message_dispatched(f: impl Fn() -> ()) -> bool { - let events_before = frame_system::Pallet::::events(); - - f(); - - frame_system::Pallet::::events() - .into_iter() - .filter(|e1| !events_before.iter().any(|e2| e1 == e2)) - .any(|e| { - if let Ok(event) = e.event.clone().try_into() - as Result, _> - { - match event { - pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { - .. - } => true, - _ => false, - } - } else { - false - } - }) - } -} - -use utils::*; - -mod foreign_investments { - use super::*; - - mod same_currencies { - use super::*; - - #[test_runtimes([development])] - fn increase_deposit_request() { - let mut env = RuntimeEnv::::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 = POOL_ID; - let amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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); - - // 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::DepositRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - }); - } - - #[test_runtimes([development])] - fn decrease_deposit_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let invest_amount: u128 = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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, - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::CancelDepositRequest { - 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 `FulfilledCancelDepositRequest` message. - assert_noop!( - pallet_liquidity_pools::Pallet::::handle( - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - - // Verify investment was decreased into 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 - ); - }); - } - - #[test_runtimes([development])] - fn cancel_deposit_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let invest_amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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, - ); - - // 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::CancelDepositRequest { - 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 `FulfilledCancelDepositRequest` message. - assert_noop!( - pallet_liquidity_pools::Pallet::::handle( - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - - // 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 - ); - }); - } - - #[test_runtimes([development])] - fn collect_deposit_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let sending_domain_locator = - DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); - 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); - 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 - ); - - // Collect investment - assert_ok!(pallet_investments::Pallet::::collect_investments_for( - RawOrigin::Signed(Keyring::Alice.into()).into(), - investor.clone(), - default_investment_id::() - )); - - // 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() - })); - - let nonce = MessageNonceStore::::get(); - - // Clearing of foreign InvestState should be dispatched - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledDepositRequest { - 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, - }, - }, - } - .into() - })); - }); - } - - #[test_runtimes([development])] - fn collect_investment() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let invest_amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let sending_domain_locator = - DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); - create_currency_pool::(pool_id, currency_id, currency_decimals.into()); - do_initial_increase_investment::( - pool_id, - invest_amount, - investor.clone(), - currency_id, - ); - enable_liquidity_pool_transferability::(currency_id); - let investment_currency_id: CurrencyId = default_investment_id::().into(); - - // 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(), - } - )); - - // 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::() - )); - - // 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() - })); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: Message::FulfilledDepositRequest { - 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, - }, - }, - } - .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::() - )); - - // 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() - })); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledDepositRequest { - 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, - }, - }, - } - .into() - })); - - // Collecting through investments should not mutate any state - let events_before = frame_system::Pallet::::events(); - let info_before = - ForeignInvestmentInfo::::get(&investor, default_investment_id::()); - assert_ok!(pallet_investments::Pallet::::collect_investments_for( - RawOrigin::Signed(Keyring::Alice.into()).into(), - investor.clone(), - default_investment_id::() - )); - assert!(!frame_system::Pallet::::events() - .into_iter() - .filter(|e1| !events_before.iter().any(|e2| e1 == e2)) - .any(|e| { - if let Ok(event) = e.event.clone().try_into() - as Result, _> - { - match event { - pallet_liquidity_pools_gateway_queue::Event::MessageSubmitted { - message: - GatewayMessage::Outbound { - router_id: event_router_id, - message: Message::FulfilledDepositRequest { .. }, - }, - .. - } => event_router_id == DEFAULT_ROUTER_ID, - _ => false, - } - } else { - false - } - })); - assert_eq!( - ForeignInvestmentInfo::::get(investor, default_investment_id::()), - info_before - ); - }); - } - - #[test_runtimes([development])] - fn increase_redeem_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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(), - &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), - amount - )); - let msg = LiquidityPoolMessage::RedeemRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg - )); - }); - } - - #[test_runtimes([development])] - fn cancel_redeem_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let redeem_amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let currency_id = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let sending_domain_locator = - DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); - - // 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::::redeem_order_id(default_investment_id::()), - 0 - ); - - // Mock incoming decrease message - let msg = LiquidityPoolMessage::CancelRedeemRequest { - 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::::handle( - 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 - ); - - // 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 - ); - }); - } - - #[test_runtimes([development])] - fn collect_redeem_request() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let redeem_amount = 10 * decimals(12); - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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 - )); - - // 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(), - } - )); - - // 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() - })); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledRedeemRequest { - 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, - }, - }, - } - .into() - })); - // Since foreign currency is pool currency, the swap is immediately fulfilled - // and FulfilledRedeemRequest dispatched - 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!(!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())); - - let nonce = MessageNonceStore::::get(); - - // Clearing of foreign RedeemState should have been dispatched exactly once - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledRedeemRequest { - 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, - }, - }, - } - .into() - })); - }); - } - - mod should_fail { - use super::*; - - mod should_throw_requires_collect { - use super::*; - - #[test_runtimes([development])] - fn invest_requires_collect() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let amount: u128 = 10 * decimals(12); - let investor = - DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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, - ); - 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::DepositRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - increase_msg - ), - pallet_investments::Error::::CollectRequired - ); - - // Should fail to decrease - let decrease_msg = LiquidityPoolMessage::CancelDepositRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - decrease_msg - ), - pallet_investments::Error::::CollectRequired - ); - }); - } - - #[test_runtimes([development])] - fn redeem_requires_collect() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let amount: u128 = 10 * decimals(12); - let investor = - DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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(), - &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), - 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::RedeemRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - increase_msg - ), - pallet_investments::Error::::CollectRequired - ); - - // Should fail to decrease - let decrease_msg = LiquidityPoolMessage::CancelRedeemRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - decrease_msg - ), - pallet_investments::Error::::CollectRequired - ); - }); - } - } - - mod payment_payout_currency { - use super::*; - - #[test_runtimes([development])] - fn redeem_payout_currency_not_found() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let investor = - DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let pool_currency = AUSD_CURRENCY_ID; - let currency_decimals = currency_decimals::AUSD; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let amount = 6 * decimals(18); - - create_currency_pool::(pool_id, pool_currency, currency_decimals.into()); - do_initial_increase_redemption::( - pool_id, - amount, - investor.clone(), - pool_currency, - ); - enable_usdt_trading::(pool_currency, amount, true, true, true); - assert_ok!(orml_tokens::Pallet::::mint_into( - default_investment_id::().into(), - &DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(), - amount, - )); - - // Should fail to decrease or collect for another - // foreign currency as long as `RedemptionState` - // exists - let decrease_msg = LiquidityPoolMessage::CancelRedeemRequest { - 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::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - decrease_msg - ), - pallet_foreign_investments::Error::::MismatchedForeignCurrency - ); - }); - } - } - } - } - - mod mismatching_currencies { - use pallet_liquidity_pools_gateway_queue::MessageNonceStore; - - use super::*; - - #[test_runtimes([development])] - fn collect_foreign_investment_for() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - 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 * decimals(18); - let sending_domain_locator = - DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain().into_account(); - let trader: AccountId = Keyring::Alice.into(); - create_currency_pool::(pool_id, pool_currency, pool_currency_decimals.into()); - - // USDT investment preparations - let invest_amount_foreign_denominated = 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 first investment and fulfill swap order - do_initial_increase_investment::( - pool_id, - invest_amount_foreign_denominated, - investor.clone(), - foreign_currency, - ); - fulfill_swap_into_pool::( - pool_id, - default_order_id::(&investor), - invest_amount_pool_denominated, - invest_amount_foreign_denominated, - trader, - ); - - // Increase invest order to initialize ForeignInvestmentInfo - let msg = LiquidityPoolMessage::DepositRequest { - 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::::handle( - 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!(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 - ); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledDepositRequest { - 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: 2 * invest_amount_pool_denominated, - }, - }, - } - .into() - })); - }); - } - - /// Invest, fulfill swap foreign->pool, cancel, fulfill swap - /// pool->foreign - #[test_runtimes([development])] - fn cancel_unprocessed_investment() { - let mut env = RuntimeEnv::::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 = POOL_ID; - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let trader: AccountId = Keyring::Alice.into(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - 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, - ); - - // Increase such that active swap into USDT is initialized - do_initial_increase_investment::( - pool_id, - invest_amount_foreign_denominated, - investor.clone(), - foreign_currency, - ); - - // Fulfilling order should propagate it from swapping to investing - let swap_order_id = default_order_id::(&investor); - fulfill_swap_into_pool::( - pool_id, - swap_order_id, - invest_amount_pool_denominated, - invest_amount_foreign_denominated, - trader.clone(), - ); - 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, - currency_in: pool_currency, - currency_out: foreign_currency, - ratio: Ratio::one(), - } - .into() - })); - - // Cancel investment - let msg = LiquidityPoolMessage::CancelDepositRequest { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - }; - - // FulfilledCancel message dispatch blocked until pool currency is swapped back - // to foreign - assert!(!outbound_message_dispatched::(|| { - assert_ok!(pallet_liquidity_pools::Pallet::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - msg.clone() - )); - })); - - assert!(!outbound_message_dispatched::(|| { - assert_ok!(pallet_order_book::Pallet::::fill_order( - RawOrigin::Signed(trader.clone()).into(), - default_order_id::(&investor), - invest_amount_pool_denominated / 4 - )); - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_order_book::Event::::OrderFulfillment { - order_id: default_order_id::(&investor), - placing_account: investor.clone(), - fulfilling_account: trader.clone(), - partial_fulfillment: true, - fulfillment_amount: invest_amount_pool_denominated / 4, - currency_in: foreign_currency, - currency_out: pool_currency, - ratio: Ratio::one(), - } - .into() - })); - })); - - let swap_order_id = default_order_id::(&investor); - assert_ok!(pallet_order_book::Pallet::::fill_order( - RawOrigin::Signed(trader.clone()).into(), - swap_order_id, - invest_amount_pool_denominated / 4 * 3 - )); - 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 / 4 * 3, - currency_in: foreign_currency, - currency_out: pool_currency, - ratio: Ratio::one(), - } - .into() - })); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledCancelDepositRequest { - 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, - fulfilled_invest_amount: invest_amount_foreign_denominated, - }, - }, - } - .into() - })); - }); - } - - /// Invest, fulfill swap foreign->pool, process 50% of investment, - /// cancel, swap back pool->foreign of remaining unprocessed investment - #[test_runtimes([development])] - fn cancel_partially_processed_investment() { - let mut env = RuntimeEnv::::from_parachain_storage( - Genesis::default() - .add(genesis::balances::(cfg(1_000))) - .storage(), - ); - - setup_test(&mut env); - - env.parachain_state_mut(|| { - let pool_id = POOL_ID; - let investor = DomainAddress::Evm(CHAIN_ID, Keyring::Bob.in_eth()).account(); - let pool_currency: CurrencyId = AUSD_CURRENCY_ID; - let foreign_currency: CurrencyId = USDT_CURRENCY_ID; - let pool_currency_decimals = currency_decimals::AUSD; - let invest_amount_pool_denominated: u128 = 10 * decimals(18); - let trader: AccountId = Keyring::Alice.into(); - create_currency_pool::(pool_id, pool_currency, pool_currency_decimals.into()); - - // USDT investment preparations - let invest_amount_foreign_denominated = enable_usdt_trading::( - pool_currency, - invest_amount_pool_denominated, - true, - true, - true, - ); - - // Do first investment and fulfill swap order - do_initial_increase_investment::( - pool_id, - invest_amount_foreign_denominated, - investor.clone(), - foreign_currency, - ); - fulfill_swap_into_pool::( - pool_id, - default_order_id::(&investor), - invest_amount_pool_denominated, - invest_amount_foreign_denominated, - trader.clone(), - ); - - // Process 50% 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::from_percent(50), - 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::() - )); - - // Cancel pending deposit request: FulfilledCancel message blocked until pool - // currency is fully swapped back to foreign one - assert!(!outbound_message_dispatched::(|| { - let cancel_msg = LiquidityPoolMessage::CancelDepositRequest { - pool_id, - tranche_id: default_tranche_id::(pool_id), - investor: investor.clone().into(), - currency: general_currency_index::(foreign_currency), - }; - - assert_ok!(pallet_liquidity_pools::Pallet::::handle( - DEFAULT_DOMAIN_ADDRESS_MOONBEAM, - cancel_msg - )); - })); - - assert_ok!(pallet_order_book::Pallet::::fill_order( - RawOrigin::Signed(trader.clone()).into(), - default_order_id::(&investor), - invest_amount_pool_denominated / 2 - )); - - let nonce = MessageNonceStore::::get(); - - assert!(frame_system::Pallet::::events().iter().any(|e| { - e.event - == pallet_liquidity_pools_gateway_queue::Event::::MessageSubmitted { - nonce, - message: GatewayMessage::Outbound { - router_id: DEFAULT_ROUTER_ID, - message: LiquidityPoolMessage::FulfilledCancelDepositRequest { - 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, - fulfilled_invest_amount: invest_amount_foreign_denominated / 2, - }, - }, - } - .into() - })); - }); - } - } -} diff --git a/runtime/integration-tests/src/cases/lp/mod.rs b/runtime/integration-tests/src/cases/lp/mod.rs index ebbfc04976..898b5c99e6 100644 --- a/runtime/integration-tests/src/cases/lp/mod.rs +++ b/runtime/integration-tests/src/cases/lp/mod.rs @@ -10,10 +10,12 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use cfg_primitives::{Balance, PoolId, CFG, SECONDS_PER_HOUR, SECONDS_PER_YEAR}; +use cfg_primitives::{AccountId, Balance, PoolId, CFG, SECONDS_PER_HOUR, SECONDS_PER_YEAR}; use cfg_traits::Seconds; use cfg_types::{ domain_address::{Domain, DomainAddress}, + fixed_point::Ratio, + oracles::OracleKey, permissions::PoolRole, tokens::{CrossChainTransferability, CurrencyId, CustomMetadata, LocalAssetId}, }; @@ -26,10 +28,10 @@ use frame_system::pallet_prelude::OriginFor; use hex_literal::hex; use pallet_axelar_router::{AxelarConfig, AxelarId, DomainConfig, EvmConfig, FeeValues}; use pallet_evm::FeeCalculator; -use runtime_common::routing::RouterId; +use runtime_common::{oracle::Feeder, routing::RouterId}; pub use setup_lp::*; -use sp_core::Get; -use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_core::{bounded_vec::BoundedVec, Get}; +use sp_runtime::traits::{BlakeTwo256, Hash, One}; use crate::{ cases::lp::utils::{pool_a_tranche_1_id, pool_b_tranche_1_id, pool_b_tranche_2_id, Decoder}, diff --git a/runtime/integration-tests/src/cases/lp/setup_lp.rs b/runtime/integration-tests/src/cases/lp/setup_lp.rs index 495889303e..b434d50dbe 100644 --- a/runtime/integration-tests/src/cases/lp/setup_lp.rs +++ b/runtime/integration-tests/src/cases/lp/setup_lp.rs @@ -10,8 +10,6 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use sp_core::bounded_vec::BoundedVec; - use super::*; use crate::cases::lp::utils::pool_c_tranche_1_id; @@ -920,12 +918,34 @@ pub fn setup_investors(evm: &mut impl EvmEnv) { utils::process_gateway_message::(utils::verify_gateway_message_success::); } +/// Adds bidirectional trading pairs with conversion ratio one. +pub fn enable_symmetric_trading_pair( + currency_1: CurrencyId, + currency_2: CurrencyId, + pool_admin: AccountId, + pool_id: PoolId, +) { + assert_ok!(pallet_order_book::Pallet::::set_market_feeder( + ::RuntimeOrigin::root(), + Feeder::root(), + )); + crate::utils::oracle::update_feeders::(pool_admin, pool_id, [Feeder::root()]); + crate::utils::oracle::feed_from_root::( + OracleKey::ConversionRatio(currency_1, currency_2), + Ratio::one(), + ); + crate::utils::oracle::feed_from_root::( + OracleKey::ConversionRatio(currency_2, currency_1), + Ratio::one(), + ); +} + /// Setup symmetric trading pairs and market ratios /// /// NOTE: Necessary in order to be able to invest pub fn setup_market_ratios() { for currency_id in [USDC.id(), FRAX.id(), DAI.id()] { - crate::cases::liquidity_pools::utils::enable_symmetric_trading_pair::( + enable_symmetric_trading_pair::( pallet_foreign_investments::pool_currency_of::((POOL_A, pool_a_tranche_1_id::())) .unwrap(), currency_id, @@ -940,7 +960,7 @@ pub fn setup_market_ratios() { Domain::Evm(EVM_DOMAIN_CHAIN_ID) )); - crate::cases::liquidity_pools::utils::enable_symmetric_trading_pair::( + enable_symmetric_trading_pair::( pallet_foreign_investments::pool_currency_of::((POOL_B, pool_b_tranche_1_id::())) .unwrap(), currency_id, @@ -954,7 +974,7 @@ pub fn setup_market_ratios() { currency_id, Domain::Evm(EVM_DOMAIN_CHAIN_ID) )); - crate::cases::liquidity_pools::utils::enable_symmetric_trading_pair::( + enable_symmetric_trading_pair::( pallet_foreign_investments::pool_currency_of::((POOL_B, pool_b_tranche_2_id::())) .unwrap(), currency_id, @@ -969,7 +989,7 @@ pub fn setup_market_ratios() { Domain::Evm(EVM_DOMAIN_CHAIN_ID) )); - crate::cases::liquidity_pools::utils::enable_symmetric_trading_pair::( + enable_symmetric_trading_pair::( pallet_foreign_investments::pool_currency_of::((POOL_C, pool_c_tranche_1_id::())) .unwrap(), currency_id, diff --git a/runtime/integration-tests/src/cases/liquidity_pools_gateway_queue.rs b/runtime/integration-tests/src/cases/queue.rs similarity index 71% rename from runtime/integration-tests/src/cases/liquidity_pools_gateway_queue.rs rename to runtime/integration-tests/src/cases/queue.rs index 85573a4581..07762104b9 100644 --- a/runtime/integration-tests/src/cases/liquidity_pools_gateway_queue.rs +++ b/runtime/integration-tests/src/cases/queue.rs @@ -10,22 +10,16 @@ use sp_runtime::{traits::One, BoundedVec}; use crate::{ config::Runtime, env::{Blocks, Env}, - envs::fudge_env::{FudgeEnv, FudgeSupport}, + envs::runtime_env::RuntimeEnv, }; pub const DEFAULT_ROUTER_ID: RouterId = RouterId::Axelar(AxelarId::Evm(1)); -/// NOTE - we're using fudge here because in a non-fudge environment, the event -/// can only be read before block finalization. The LP gateway queue is -/// processing messages during the `on_idle` hook, just before the block is -/// finished, after the message is processed, the block is finalized and the -/// event resets. - /// Confirm that an inbound messages reaches its destination: /// LP pallet #[test_runtimes(all)] -fn inbound() { - let mut env = FudgeEnv::::default(); +fn queue_and_dequeue_inbound() { + let mut env = RuntimeEnv::::default(); let expected_event = env.parachain_state_mut(|| { assert_ok!(pallet_liquidity_pools_gateway::Pallet::::set_routers( @@ -33,14 +27,14 @@ fn inbound() { BoundedVec::try_from(vec![DEFAULT_ROUTER_ID]).unwrap(), )); - let nonce = ::MessageNonce::one(); + let nonce = T::MessageNonce::one(); let message = GatewayMessage::Inbound { domain_address: DomainAddress::Evm(1, H160::repeat_byte(2)), router_id: DEFAULT_ROUTER_ID, message: Message::Invalid, }; - assert_ok!(pallet_liquidity_pools_gateway_queue::Pallet::::submit( + assert_ok!(pallet_liquidity_pools_gateway_queue::Pallet::::queue( message.clone() )); @@ -51,17 +45,18 @@ fn inbound() { } }); + // Here we dequeue env.pass(Blocks::UntilEvent { event: expected_event.into(), - limit: 3, + limit: 1, }); } -/// Confirm that an inbound messages reaches its destination: -/// LP gateway pallet +/// Confirm that an outbound messages reaches its destination: +/// The routers #[test_runtimes(all)] -fn outbound() { - let mut env = FudgeEnv::::default(); +fn queue_and_dequeue_outbound() { + let mut env = RuntimeEnv::::default(); let expected_event = env.parachain_state_mut(|| { assert_ok!(pallet_liquidity_pools_gateway::Pallet::::set_routers( @@ -69,13 +64,13 @@ fn outbound() { BoundedVec::try_from(vec![DEFAULT_ROUTER_ID]).unwrap(), )); - let nonce = ::MessageNonce::one(); + let nonce = T::MessageNonce::one(); let message = GatewayMessage::Outbound { router_id: DEFAULT_ROUTER_ID, message: Message::Invalid, }; - assert_ok!(pallet_liquidity_pools_gateway_queue::Pallet::::submit( + assert_ok!(pallet_liquidity_pools_gateway_queue::Pallet::::queue( message.clone() )); @@ -86,8 +81,9 @@ fn outbound() { } }); + // Here we dequeue env.pass(Blocks::UntilEvent { event: expected_event.into(), - limit: 3, + limit: 1, }); } diff --git a/runtime/integration-tests/src/cases/routers.rs b/runtime/integration-tests/src/cases/routers.rs index bd4a7bce0c..dd58bed554 100644 --- a/runtime/integration-tests/src/cases/routers.rs +++ b/runtime/integration-tests/src/cases/routers.rs @@ -143,6 +143,8 @@ mod axelar_evm { message: Message::Invalid, }; + // If the message is correctly processed, it means that the router sends + // correcly the message assert_ok!(pallet_liquidity_pools_gateway::Pallet::::process(gateway_message).0); }); } diff --git a/runtime/integration-tests/src/envs/runtime_env.rs b/runtime/integration-tests/src/envs/runtime_env.rs index 88d05177d4..2bbd06b0c3 100644 --- a/runtime/integration-tests/src/envs/runtime_env.rs +++ b/runtime/integration-tests/src/envs/runtime_env.rs @@ -178,7 +178,9 @@ impl Env for RuntimeEnv { fn __priv_build_block(&mut self, i: BlockNumber) { self.process_pending_extrinsics(); self.parachain_state_mut(|| { - T::Api::finalize_block(); + if i == 0 { + T::Api::finalize_block(); + } Self::prepare_block(i); }); } @@ -251,6 +253,8 @@ impl RuntimeEnv { for extrinsic in inherent_extrinsics { T::Api::apply_extrinsic(extrinsic).unwrap().unwrap(); } + + T::Api::finalize_block(); } fn cumulus_inherent(i: BlockNumber) -> T::RuntimeCallExt {