From 70babe3024f300636b6158241d938c49c335807e Mon Sep 17 00:00:00 2001 From: Cosmin Damian <17934949+cdamian@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:34:51 +0100 Subject: [PATCH] lp-gateway: Add queue for outbound messages (#1696) * lp-gateway: Add queue for outbound messages * primitives: Add primitive type for outbound message nonce * lp-gateway: Adjust weight calculations when processing outbound messages * integration-tests: Add nonce when checking success events * lp-gateway: Add tests for outbound message extrinsics * integration-tests: Use correct sender when checking LP gateway events, remove extra checks for instances * lp-gateway: Remove redundant events, use get when checking storage --- Cargo.lock | 1 + libs/mocks/src/liquidity_pools.rs | 4 +- .../src/liquidity_pools_gateway_routers.rs | 15 +- libs/primitives/src/lib.rs | 3 + libs/traits/src/liquidity_pools.rs | 4 +- pallets/liquidity-pools-gateway/Cargo.toml | 1 + .../routers/src/lib.rs | 21 +- .../routers/src/mock.rs | 3 +- .../routers/src/routers/axelar_evm.rs | 4 +- .../routers/src/routers/axelar_xcm.rs | 4 +- .../routers/src/routers/ethereum_xcm.rs | 7 +- .../routers/src/tests.rs | 4 +- pallets/liquidity-pools-gateway/src/lib.rs | 326 +++++++++++++++- pallets/liquidity-pools-gateway/src/mock.rs | 4 +- pallets/liquidity-pools-gateway/src/tests.rs | 281 +++++++++++++- .../liquidity-pools-gateway/src/weights.rs | 24 ++ pallets/liquidity-pools/src/message.rs | 4 +- runtime/altair/src/liquidity_pools.rs | 4 +- runtime/centrifuge/src/liquidity_pools.rs | 5 +- runtime/common/src/xcm.rs | 2 + runtime/development/src/liquidity_pools.rs | 3 +- .../src/generic/cases/liquidity_pools.rs | 357 ++++++++++++++---- 22 files changed, 939 insertions(+), 142 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a72f4c59b..e496ee9ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7440,6 +7440,7 @@ name = "pallet-liquidity-pools-gateway" version = "0.0.1" dependencies = [ "cfg-mocks", + "cfg-primitives", "cfg-traits", "cfg-types", "cfg-utils", diff --git a/libs/mocks/src/liquidity_pools.rs b/libs/mocks/src/liquidity_pools.rs index 89722945cb..208c2aee4f 100644 --- a/libs/mocks/src/liquidity_pools.rs +++ b/libs/mocks/src/liquidity_pools.rs @@ -1,8 +1,8 @@ use cfg_traits::liquidity_pools::Codec; -use parity_scale_codec::{Decode, Encode, Error, Input}; +use parity_scale_codec::{Decode, Encode, Error, Input, MaxEncodedLen}; use scale_info::TypeInfo; -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, TypeInfo)] +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)] pub enum MessageMock { First, Second, diff --git a/libs/mocks/src/liquidity_pools_gateway_routers.rs b/libs/mocks/src/liquidity_pools_gateway_routers.rs index 429e1aa8bc..b50ff55621 100644 --- a/libs/mocks/src/liquidity_pools_gateway_routers.rs +++ b/libs/mocks/src/liquidity_pools_gateway_routers.rs @@ -28,7 +28,9 @@ pub mod pallet { register_call!(move |()| f()); } - pub fn mock_send(f: impl Fn(T::AccountId, MessageMock) -> DispatchResult + 'static) { + pub fn mock_send( + f: impl Fn(T::AccountId, MessageMock) -> DispatchResultWithPostInfo + 'static, + ) { register_call!(move |(sender, message)| f(sender, message)); } } @@ -41,7 +43,7 @@ pub mod pallet { execute_call!(()) } - fn send(sender: Self::Sender, message: MessageMock) -> DispatchResult { + fn send(sender: Self::Sender, message: MessageMock) -> DispatchResultWithPostInfo { execute_call!((sender, message)) } } @@ -68,7 +70,10 @@ impl RouterMock { pallet::Pallet::::mock_init(f) } - pub fn mock_send(&self, f: impl Fn(T::AccountId, MessageMock) -> DispatchResult + 'static) { + pub fn mock_send( + &self, + f: impl Fn(T::AccountId, MessageMock) -> DispatchResultWithPostInfo + 'static, + ) { pallet::Pallet::::mock_send(f) } } @@ -83,7 +88,7 @@ impl Router for RouterMock { pallet::Pallet::::init() } - fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResult { + fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResultWithPostInfo { pallet::Pallet::::send(sender, message) } } @@ -105,5 +110,5 @@ trait MockedRouter { fn init() -> DispatchResult; /// Send the message to the router's destination. - fn send(sender: Self::Sender, message: Self::Message) -> DispatchResult; + fn send(sender: Self::Sender, message: Self::Message) -> DispatchResultWithPostInfo; } diff --git a/libs/primitives/src/lib.rs b/libs/primitives/src/lib.rs index 62d57baae2..a0185a4294 100644 --- a/libs/primitives/src/lib.rs +++ b/libs/primitives/src/lib.rs @@ -179,6 +179,9 @@ pub mod types { /// The representation of a pool fee identifier pub type PoolFeeId = u64; + + /// The type for outbound LP message nonces. + pub type OutboundMessageNonce = u64; } /// Common constants for all runtimes diff --git a/libs/traits/src/liquidity_pools.rs b/libs/traits/src/liquidity_pools.rs index 4287b9ec8e..d4aa0ae880 100644 --- a/libs/traits/src/liquidity_pools.rs +++ b/libs/traits/src/liquidity_pools.rs @@ -11,7 +11,7 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use frame_support::dispatch::DispatchResult; +use frame_support::dispatch::{DispatchResult, DispatchResultWithPostInfo}; use parity_scale_codec::Input; use sp_std::vec::Vec; @@ -34,7 +34,7 @@ pub trait Router { fn init(&self) -> DispatchResult; /// Send the message to the router's destination. - fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResult; + fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResultWithPostInfo; } /// The trait required for processing outbound messages. diff --git a/pallets/liquidity-pools-gateway/Cargo.toml b/pallets/liquidity-pools-gateway/Cargo.toml index 38c3a62913..c9748fd239 100644 --- a/pallets/liquidity-pools-gateway/Cargo.toml +++ b/pallets/liquidity-pools-gateway/Cargo.toml @@ -32,6 +32,7 @@ cfg-utils = { path = "../../libs/utils", default-features = false } [dev-dependencies] cfg-mocks = { path = "../../libs/mocks", features = ["std"] } +cfg-primitives = { path = "../../libs/primitives" } hex-literal = "0.4.1" sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } diff --git a/pallets/liquidity-pools-gateway/routers/src/lib.rs b/pallets/liquidity-pools-gateway/routers/src/lib.rs index 8f12e2b9ee..edd5363283 100644 --- a/pallets/liquidity-pools-gateway/routers/src/lib.rs +++ b/pallets/liquidity-pools-gateway/routers/src/lib.rs @@ -29,7 +29,9 @@ pub const GAS_TO_WEIGHT_MULTIPLIER: u64 = 25_000; use cfg_traits::{ethereum::EthereumTransactor, liquidity_pools::Router}; use frame_support::{ - dispatch::{DispatchError, DispatchResult, Weight}, + dispatch::{ + DispatchError, DispatchResult, DispatchResultWithPostInfo, PostDispatchInfo, Weight, + }, ensure, traits::OriginTrait, }; @@ -113,7 +115,7 @@ where } } - fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResult { + fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResultWithPostInfo { match self { DomainRouter::EthereumXCM(r) => r.do_send(sender, message), DomainRouter::AxelarEVM(r) => r.do_send(sender, message), @@ -159,12 +161,9 @@ where /// pallet, this EVM address will be converted back into a substrate account /// which will be charged for the transaction. This converted substrate /// account is not the same as the original account. - pub fn do_send(&self, sender: T::AccountId, msg: Vec) -> DispatchResult { + pub fn do_send(&self, sender: T::AccountId, msg: Vec) -> DispatchResultWithPostInfo { let sender_evm_address = H160::from_slice(&sender.as_ref()[0..20]); - // TODO(cdamian): This returns a `DispatchResultWithPostInfo`. Should we - // propagate that to another layer that will eventually charge for the - // weight in the PostDispatchInfo? as EthereumTransactor>::call( sender_evm_address, self.evm_domain.target_contract_address, @@ -173,9 +172,6 @@ where self.evm_domain.fee_values.gas_price, self.evm_domain.fee_values.gas_limit, ) - .map_err(|e| e.error)?; - - Ok(()) } } @@ -231,7 +227,7 @@ where /// Encodes the message to the required format and executes the /// call via the XCM transactor pallet. - pub fn do_send(&self, sender: T::AccountId, msg: Vec) -> DispatchResult { + pub fn do_send(&self, sender: T::AccountId, msg: Vec) -> DispatchResultWithPostInfo { let ethereum_xcm_call = get_encoded_ethereum_xcm_call::(self.xcm_domain.clone(), msg) .map_err(|_| DispatchError::Other("encoded ethereum xcm call retrieval"))?; @@ -257,7 +253,10 @@ where true, )?; - Ok(()) + Ok(PostDispatchInfo { + actual_weight: Some(self.xcm_domain.overall_weight), + pays_fee: Default::default(), + }) } } diff --git a/pallets/liquidity-pools-gateway/routers/src/mock.rs b/pallets/liquidity-pools-gateway/routers/src/mock.rs index 81bdcd93c4..bc3d70fe15 100644 --- a/pallets/liquidity-pools-gateway/routers/src/mock.rs +++ b/pallets/liquidity-pools-gateway/routers/src/mock.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use cfg_mocks::{pallet_mock_liquidity_pools, pallet_mock_routers, MessageMock, RouterMock}; -use cfg_primitives::{BLOCK_STORAGE_LIMIT, MAX_POV_SIZE}; +use cfg_primitives::{OutboundMessageNonce, BLOCK_STORAGE_LIMIT, MAX_POV_SIZE}; use cfg_traits::TryConvert; use cfg_types::domain_address::DomainAddress; use cumulus_primitives_core::{ @@ -153,6 +153,7 @@ impl pallet_liquidity_pools_gateway::Config for Runtime { type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = MessageMock; type OriginRecovery = MockOriginRecovery; + type OutboundMessageNonce = OutboundMessageNonce; type Router = RouterMock; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs index 87eddaec56..1b94f9c5e1 100644 --- a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs +++ b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs @@ -11,7 +11,7 @@ // GNU General Public License for more details. use cfg_traits::liquidity_pools::Codec; use ethabi::{Contract, Function, Param, ParamType, Token}; -use frame_support::dispatch::{DispatchError, DispatchResult}; +use frame_support::dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo}; use frame_system::pallet_prelude::OriginFor; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::{ @@ -61,7 +61,7 @@ where /// Encodes the message to the required format, /// then executes the EVM call using the generic EVM router. - pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResult { + pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResultWithPostInfo { let eth_msg = get_axelar_encoded_msg( msg.serialize(), self.evm_chain.clone().into_inner(), diff --git a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_xcm.rs b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_xcm.rs index ed49d9cdec..74d4de14be 100644 --- a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_xcm.rs +++ b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_xcm.rs @@ -11,7 +11,7 @@ // GNU General Public License for more details. use cfg_traits::liquidity_pools::Codec; -use frame_support::dispatch::DispatchResult; +use frame_support::dispatch::{DispatchResult, DispatchResultWithPostInfo}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::{bounded::BoundedVec, ConstU32, H160}; @@ -53,7 +53,7 @@ where /// Encodes the message to the required format, /// then executes the EVM call using the generic XCM router. - pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResult { + pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResultWithPostInfo { let contract_call = get_axelar_encoded_msg( msg.serialize(), self.axelar_target_chain.clone().into_inner(), diff --git a/pallets/liquidity-pools-gateway/routers/src/routers/ethereum_xcm.rs b/pallets/liquidity-pools-gateway/routers/src/routers/ethereum_xcm.rs index e0169879b8..83f49cf796 100644 --- a/pallets/liquidity-pools-gateway/routers/src/routers/ethereum_xcm.rs +++ b/pallets/liquidity-pools-gateway/routers/src/routers/ethereum_xcm.rs @@ -11,7 +11,10 @@ // GNU General Public License for more details. use cfg_traits::liquidity_pools::Codec; use ethabi::{Bytes, Contract}; -use frame_support::{dispatch::DispatchResult, sp_runtime::DispatchError}; +use frame_support::{ + dispatch::{DispatchResult, DispatchResultWithPostInfo}, + sp_runtime::DispatchError, +}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, vec, vec::Vec}; @@ -43,7 +46,7 @@ where /// Encodes the message to the required format and executes the /// call via the XCM router. - pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResult { + pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResultWithPostInfo { let contract_call = get_encoded_contract_call(msg.serialize()) .map_err(|_| DispatchError::Other("encoded contract call retrieval"))?; diff --git a/pallets/liquidity-pools-gateway/routers/src/tests.rs b/pallets/liquidity-pools-gateway/routers/src/tests.rs index 82d021ff5e..7c268d8db4 100644 --- a/pallets/liquidity-pools-gateway/routers/src/tests.rs +++ b/pallets/liquidity-pools-gateway/routers/src/tests.rs @@ -169,7 +169,7 @@ mod evm_router { let res = router.do_send(test_data.sender, test_data.msg); assert_eq!( - res.err().unwrap(), + res.err().unwrap().error, pallet_evm::Error::::BalanceLow.into() ); }); @@ -483,7 +483,7 @@ mod axelar_evm { let res = domain_router.send(test_data.sender, test_data.msg); assert_eq!( - res.err().unwrap(), + res.err().unwrap().error, pallet_evm::Error::::BalanceLow.into() ); }); diff --git a/pallets/liquidity-pools-gateway/src/lib.rs b/pallets/liquidity-pools-gateway/src/lib.rs index da87b89f40..d1c0baa994 100644 --- a/pallets/liquidity-pools-gateway/src/lib.rs +++ b/pallets/liquidity-pools-gateway/src/lib.rs @@ -19,9 +19,13 @@ use cfg_traits::{ }; use cfg_types::domain_address::{Domain, DomainAddress}; use frame_support::{dispatch::DispatchResult, pallet_prelude::*, PalletError}; -use frame_system::pallet_prelude::OriginFor; +use frame_system::{ + ensure_signed, + pallet_prelude::{BlockNumberFor, OriginFor}, +}; pub use pallet::*; use parity_scale_codec::{EncodeLike, FullCodec}; +use sp_runtime::traits::{AtLeast32BitUnsigned, EnsureAdd, EnsureAddAssign, One}; use sp_std::{convert::TryInto, vec::Vec}; use crate::weights::WeightInfo; @@ -57,6 +61,15 @@ pub mod pallet { const BYTES_U32: usize = 4; const BYTES_ACCOUNT_20: usize = 20; + /// Some gateway routers do not return an actual weight when sending a + /// message, thus, this default is required, and it's based on: + /// + /// https://github.com/centrifuge/centrifuge-chain/pull/1696#discussion_r1456370592 + const DEFAULT_WEIGHT_REF_TIME: u64 = 5_000_000_000; + + use frame_support::dispatch::PostDispatchInfo; + use sp_runtime::DispatchErrorWithPostInfo; + use super::*; use crate::RelayerMessageDecodingError::{ MalformedMessage, MalformedSourceAddress, MalformedSourceAddressLength, @@ -94,7 +107,7 @@ pub mod pallet { /// /// NOTE - this `Codec` trait is the Centrifuge trait for liquidity /// pools' messages. - type Message: Codec + Clone + Debug + PartialEq; + type Message: Codec + Clone + Debug + PartialEq + MaxEncodedLen + TypeInfo + FullCodec; /// The message router type that is stored for each domain. type Router: DomainRouter @@ -122,6 +135,17 @@ pub mod pallet { /// implementation. #[pallet::constant] type Sender: Get; + + /// Type used for outbound message identification. + type OutboundMessageNonce: Parameter + + Member + + AtLeast32BitUnsigned + + Default + + Copy + + EnsureAdd + + MaybeSerializeDeserialize + + TypeInfo + + MaxEncodedLen; } #[pallet::event] @@ -148,13 +172,31 @@ pub mod pallet { domain: Domain, message: T::Message, }, + + /// Outbound message execution failure. + OutboundMessageExecutionFailure { + nonce: T::OutboundMessageNonce, + sender: T::AccountId, + domain: Domain, + message: T::Message, + error: DispatchError, + }, + + /// Outbound message execution success. + OutboundMessageExecutionSuccess { + nonce: T::OutboundMessageNonce, + sender: T::AccountId, + domain: Domain, + message: T::Message, + }, } /// Storage for domain routers. /// /// This can only be set by an admin. #[pallet::storage] - pub(crate) type DomainRouters = StorageMap<_, Blake2_128Concat, Domain, T::Router>; + #[pallet::getter(fn domain_routers)] + pub type DomainRouters = StorageMap<_, Blake2_128Concat, Domain, T::Router>; /// Storage that contains a limited number of whitelisted instances of /// deployed liquidity pools for a particular domain. @@ -162,7 +204,7 @@ pub mod pallet { /// This can only be modified by an admin. #[pallet::storage] #[pallet::getter(fn allowlist)] - pub(crate) type Allowlist = + pub type Allowlist = StorageDoubleMap<_, Blake2_128Concat, Domain, Blake2_128Concat, DomainAddress, ()>; /// Storage that contains a limited number of whitelisted instances of @@ -171,9 +213,35 @@ pub mod pallet { /// This can only be modified by an admin. #[pallet::storage] #[pallet::getter(fn relayer)] - pub(crate) type RelayerList = + pub type RelayerList = StorageDoubleMap<_, Blake2_128Concat, Domain, Blake2_128Concat, DomainAddress, ()>; + #[pallet::storage] + #[pallet::getter(fn outbound_message_nonce_store)] + pub type OutboundMessageNonceStore = + StorageValue<_, T::OutboundMessageNonce, ValueQuery>; + + /// Storage for outbound messages that will be processed during the + /// `on_idle` hook. + #[pallet::storage] + #[pallet::getter(fn outbound_message_queue)] + pub type OutboundMessageQueue = StorageMap< + _, + Blake2_128Concat, + T::OutboundMessageNonce, + (Domain, T::AccountId, T::Message), + >; + + /// Storage for failed outbound messages that can be manually re-triggered. + #[pallet::storage] + #[pallet::getter(fn failed_outbound_messages)] + pub type FailedOutboundMessages = StorageMap< + _, + Blake2_128Concat, + T::OutboundMessageNonce, + (Domain, T::AccountId, T::Message, DispatchError), + >; + #[pallet::error] pub enum Error { /// Router initialization failed. @@ -212,6 +280,19 @@ pub mod pallet { /// Decoding that is essential and this error /// signals malforming of the wrapping information. RelayerMessageDecodingFailed { reason: RelayerMessageDecodingError }, + + /// Outbound message not found in storage. + OutboundMessageNotFound, + + /// Failed outbound message not found in storage. + FailedOutboundMessageNotFound, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_idle(_now: T::BlockNumber, max_weight: Weight) -> Weight { + Self::service_outbound_message_queue(max_weight) + } } #[pallet::call] @@ -423,6 +504,87 @@ pub mod pallet { T::InboundQueue::submit(domain_address, incoming_msg) } + + /// Convenience method for manually processing an outbound message. + /// + /// If the execution fails, the message gets moved to the + /// `FailedOutboundMessages` storage. + #[pallet::weight(T::WeightInfo::process_outbound_message())] + #[pallet::call_index(6)] + pub fn process_outbound_message( + origin: OriginFor, + nonce: T::OutboundMessageNonce, + ) -> DispatchResult { + ensure_signed(origin)?; + + let (domain, sender, message) = OutboundMessageQueue::::take(nonce) + .ok_or(Error::::OutboundMessageNotFound)?; + + match Self::process_message(domain.clone(), sender.clone(), message.clone()) { + Ok(_) => { + Self::deposit_event(Event::::OutboundMessageExecutionSuccess { + nonce, + domain, + sender, + message, + }); + + Ok(()) + } + Err(e) => { + Self::deposit_event(Event::::OutboundMessageExecutionFailure { + nonce, + domain: domain.clone(), + sender: sender.clone(), + message: message.clone(), + error: e.error, + }); + + FailedOutboundMessages::::insert(nonce, (domain, sender, message, e.error)); + + Ok(()) + } + } + } + + /// Manually process a failed outbound message. + #[pallet::weight(T::WeightInfo::process_failed_outbound_message())] + #[pallet::call_index(7)] + pub fn process_failed_outbound_message( + origin: OriginFor, + nonce: T::OutboundMessageNonce, + ) -> DispatchResult { + ensure_signed(origin)?; + + let (domain, sender, message, _) = FailedOutboundMessages::::get(nonce) + .ok_or(Error::::OutboundMessageNotFound)?; + + match Self::process_message(domain.clone(), sender.clone(), message.clone()) { + Ok(_) => { + Self::deposit_event(Event::::OutboundMessageExecutionSuccess { + nonce, + domain, + sender, + message, + }); + + FailedOutboundMessages::::remove(nonce); + + Ok(()) + } + Err(e) => { + Self::deposit_event(Event::::OutboundMessageExecutionFailure { + nonce, + domain: domain.clone(), + sender: sender.clone(), + message: message.clone(), + error: e.error, + }); + + Ok(()) + } + } + } } impl Pallet { @@ -462,6 +624,140 @@ pub mod pallet { Ok((address, incoming_msg)) } + + /// Iterates over the outbound messages stored in the queue and attempts + /// to process them. + /// + /// If a message fails to process it is moved to the + /// `FailedOutboundMessages` storage so that it can be executed again + /// via the `process_failed_outbound_message` extrinsic. + fn service_outbound_message_queue(max_weight: Weight) -> Weight { + let mut weight_used = Weight::zero(); + + let mut processed_entries = Vec::new(); + + for (nonce, (domain, sender, message)) in OutboundMessageQueue::::iter() { + processed_entries.push(nonce); + + let weight = + match Self::process_message(domain.clone(), sender.clone(), message.clone()) { + Ok(post_info) => { + Self::deposit_event(Event::OutboundMessageExecutionSuccess { + nonce, + sender, + domain, + message, + }); + + post_info + .actual_weight + .expect("Message processing success already ensured") + // Extra weight breakdown: + // + // 1 read for the outbound message + // 1 write for the event + // 1 write for the outbound message removal + .saturating_add(T::DbWeight::get().reads_writes(1, 2)) + } + Err(e) => { + Self::deposit_event(Event::OutboundMessageExecutionFailure { + nonce, + sender: sender.clone(), + domain: domain.clone(), + message: message.clone(), + error: e.error, + }); + + FailedOutboundMessages::::insert( + nonce, + (domain, sender, message, e.error), + ); + + e.post_info + .actual_weight + .expect("Message processing success already ensured") + // Extra weight breakdown: + // + // 1 read for the outbound message + // 1 write for the event + // 1 write for the failed outbound message + // 1 write for the outbound message removal + .saturating_add(T::DbWeight::get().reads_writes(1, 3)) + } + }; + + weight_used = weight_used.saturating_add(weight); + + if weight_used.all_gte(max_weight) { + break; + } + } + + for entry in processed_entries { + OutboundMessageQueue::::remove(entry); + } + + weight_used + } + + /// Retrieves the router stored for the provided domain and sends the + /// message, calculating and returning the required weight for these + /// operations in the `DispatchResultWithPostInfo`. + fn process_message( + domain: Domain, + sender: T::AccountId, + message: T::Message, + ) -> DispatchResultWithPostInfo { + let read_weight = T::DbWeight::get().reads(1); + + let router = DomainRouters::::get(domain).ok_or(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: Error::::RouterNotFound.into(), + })?; + + let post_dispatch_info_fn = + |actual_weight: Option, extra_weight: Weight| -> PostDispatchInfo { + PostDispatchInfo { + actual_weight: Some(Self::get_outbound_message_processing_weight( + actual_weight, + extra_weight, + )), + pays_fee: Pays::Yes, + } + }; + + match router.send(sender, message) { + Ok(dispatch_info) => Ok(post_dispatch_info_fn( + dispatch_info.actual_weight, + read_weight, + )), + Err(e) => Err(DispatchErrorWithPostInfo { + post_info: post_dispatch_info_fn(e.post_info.actual_weight, read_weight), + error: e.error, + }), + } + } + + /// Calculates the weight used by a router when processing an outbound + /// message. + fn get_outbound_message_processing_weight( + router_call_weight: Option, + extra_weight: Weight, + ) -> Weight { + let pov_weight: u64 = (Domain::max_encoded_len() + + T::AccountId::max_encoded_len() + + T::Message::max_encoded_len()) + .try_into() + .expect("can calculate outbound message POV weight"); + + router_call_weight + .unwrap_or(Weight::from_parts(DEFAULT_WEIGHT_REF_TIME, 0)) + .saturating_add(Weight::from_parts(0, pov_weight)) + .saturating_add(extra_weight) + } } /// This pallet will be the `OutboundQueue` used by other pallets to send @@ -476,7 +772,7 @@ pub mod pallet { type Sender = T::AccountId; fn submit( - sender: Self::Sender, + _sender: Self::Sender, destination: Self::Destination, message: Self::Message, ) -> DispatchResult { @@ -485,13 +781,23 @@ pub mod pallet { Error::::DomainNotSupported ); - let router = - DomainRouters::::get(destination.clone()).ok_or(Error::::RouterNotFound)?; + ensure!( + DomainRouters::::contains_key(destination.clone()), + Error::::RouterNotFound + ); - router.send(T::Sender::get(), message.clone())?; + let nonce = >::try_mutate(|n| { + n.ensure_add_assign(T::OutboundMessageNonce::one())?; + Ok::(*n) + })?; + + OutboundMessageQueue::::insert( + nonce, + (destination.clone(), T::Sender::get(), message.clone()), + ); Self::deposit_event(Event::OutboundMessageSubmitted { - sender, + sender: T::Sender::get(), domain: destination, message, }); diff --git a/pallets/liquidity-pools-gateway/src/mock.rs b/pallets/liquidity-pools-gateway/src/mock.rs index 04d0de0515..3e79d2ecce 100644 --- a/pallets/liquidity-pools-gateway/src/mock.rs +++ b/pallets/liquidity-pools-gateway/src/mock.rs @@ -2,6 +2,7 @@ use cfg_mocks::{ pallet_mock_liquidity_pools, pallet_mock_routers, pallet_mock_try_convert, MessageMock, RouterMock, }; +use cfg_primitives::OutboundMessageNonce; use cfg_types::domain_address::DomainAddress; use frame_system::EnsureRoot; use sp_core::{crypto::AccountId32, ConstU128, ConstU16, ConstU32, ConstU64, H256}; @@ -79,7 +80,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type HoldIdentifier = (); type MaxFreezes = (); - type MaxHolds = frame_support::traits::ConstU32<1>; + type MaxHolds = ConstU32<1>; type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = (); @@ -111,6 +112,7 @@ impl pallet_liquidity_pools_gateway::Config for Runtime { type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = MessageMock; type OriginRecovery = MockOriginRecovery; + type OutboundMessageNonce = OutboundMessageNonce; type Router = RouterMock; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/pallets/liquidity-pools-gateway/src/tests.rs b/pallets/liquidity-pools-gateway/src/tests.rs index 1238f0f85b..d1162397d2 100644 --- a/pallets/liquidity-pools-gateway/src/tests.rs +++ b/pallets/liquidity-pools-gateway/src/tests.rs @@ -1,16 +1,24 @@ use cfg_mocks::*; +use cfg_primitives::OutboundMessageNonce; use cfg_traits::liquidity_pools::{Codec, OutboundQueue}; use cfg_types::domain_address::*; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{ + assert_noop, assert_ok, + dispatch::{Pays, PostDispatchInfo, Weight}, +}; use sp_core::{crypto::AccountId32, ByteArray, H160}; -use sp_runtime::{DispatchError, DispatchError::BadOrigin}; +use sp_runtime::{ + traits::{One, Zero}, + DispatchError, + DispatchError::BadOrigin, + DispatchErrorWithPostInfo, +}; use super::{ mock::{RuntimeEvent as MockEvent, *}, origin::*, pallet::*, }; -use crate::pallet; mod utils { use super::*; @@ -786,13 +794,82 @@ mod process_msg_domain { } } -mod outbound_queue_impl { +mod process_outbound_message { use super::*; #[test] fn success() { new_test_ext().execute_with(|| { let domain = Domain::EVM(0); + + let router = RouterMock::::default(); + router.mock_init(move || Ok(())); + + assert_ok!(LiquidityPoolsGateway::set_domain_router( + RuntimeOrigin::root(), + domain.clone(), + router.clone(), + )); + + let sender = get_test_account_id(); + let msg = MessageMock::First; + + router.mock_send({ + let sender = sender.clone(); + let msg = msg.clone(); + + move |mock_sender, mock_msg| { + assert_eq!(sender, mock_sender); + assert_eq!(msg, mock_msg); + + Ok(PostDispatchInfo { + actual_weight: Some(Weight::from_parts(100, 100)), + pays_fee: Pays::Yes, + }) + } + }); + + let nonce = OutboundMessageNonce::one(); + + OutboundMessageQueue::::insert( + nonce, + (domain.clone(), sender.clone(), msg.clone()), + ); + + assert_ok!(LiquidityPoolsGateway::process_outbound_message( + RuntimeOrigin::signed(sender.clone()), + nonce + )); + + assert!(!OutboundMessageQueue::::contains_key(nonce)); + + event_exists(Event::::OutboundMessageExecutionSuccess { + nonce, + sender, + domain, + message: msg, + }); + }); + } + + #[test] + fn message_not_found() { + new_test_ext().execute_with(|| { + assert_noop!( + LiquidityPoolsGateway::process_outbound_message( + RuntimeOrigin::signed(get_test_account_id()), + OutboundMessageNonce::zero(), + ), + Error::::OutboundMessageNotFound, + ); + }); + } + + #[test] + fn failure() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let router = RouterMock::::default(); router.mock_init(move || Ok(())); @@ -804,26 +881,68 @@ mod outbound_queue_impl { let sender = get_test_account_id(); let msg = MessageMock::First; + let err = DispatchError::Unavailable; router.mock_send({ + let sender = sender.clone(); let msg = msg.clone(); + let err = err.clone(); move |mock_sender, mock_msg| { - assert_eq!(::Sender::get(), mock_sender); + assert_eq!(sender, mock_sender); assert_eq!(msg, mock_msg); - Ok(()) + Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(100, 100)), + pays_fee: Pays::Yes, + }, + error: err, + }) } }); - assert_ok!(LiquidityPoolsGateway::submit(sender, domain, msg)); + let nonce = OutboundMessageNonce::one(); + + OutboundMessageQueue::::insert( + nonce, + (domain.clone(), sender.clone(), msg.clone()), + ); + + assert_ok!(LiquidityPoolsGateway::process_outbound_message( + RuntimeOrigin::signed(sender.clone()), + nonce + )); + + assert!(!OutboundMessageQueue::::contains_key(nonce)); + + let failed_queue_entry = FailedOutboundMessages::::get(nonce) + .expect("expected failed message queue entry"); + + assert_eq!( + failed_queue_entry, + (domain.clone(), sender.clone(), msg.clone(), err.clone()) + ); + + event_exists(Event::::OutboundMessageExecutionFailure { + nonce, + sender, + domain, + message: msg, + error: err, + }); }); } +} + +mod process_failed_outbound_message { + use super::*; #[test] - fn router_error() { + fn success() { new_test_ext().execute_with(|| { let domain = Domain::EVM(0); + let router = RouterMock::::default(); router.mock_init(move || Ok(())); @@ -835,26 +954,162 @@ mod outbound_queue_impl { let sender = get_test_account_id(); let msg = MessageMock::First; - let expected_error = DispatchError::Other("router error"); + let err = DispatchError::Unavailable; router.mock_send({ + let sender = sender.clone(); let msg = msg.clone(); move |mock_sender, mock_msg| { - assert_eq!(::Sender::get(), mock_sender); + assert_eq!(sender, mock_sender); assert_eq!(msg, mock_msg); - Err(expected_error) + Ok(PostDispatchInfo { + actual_weight: Some(Weight::from_parts(100, 100)), + pays_fee: Pays::Yes, + }) } }); + let nonce = OutboundMessageNonce::one(); + + FailedOutboundMessages::::insert( + nonce, + (domain.clone(), sender.clone(), msg.clone(), err), + ); + + assert_ok!(LiquidityPoolsGateway::process_failed_outbound_message( + RuntimeOrigin::signed(sender.clone()), + nonce + )); + + assert!(!FailedOutboundMessages::::contains_key(nonce)); + + event_exists(Event::::OutboundMessageExecutionSuccess { + nonce, + sender, + domain, + message: msg, + }); + }); + } + + #[test] + fn message_not_found() { + new_test_ext().execute_with(|| { assert_noop!( - LiquidityPoolsGateway::submit(sender, domain, msg), - expected_error, + LiquidityPoolsGateway::process_failed_outbound_message( + RuntimeOrigin::signed(get_test_account_id()), + OutboundMessageNonce::zero(), + ), + Error::::OutboundMessageNotFound, ); }); } + #[test] + fn failure() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + + let router = RouterMock::::default(); + router.mock_init(move || Ok(())); + + assert_ok!(LiquidityPoolsGateway::set_domain_router( + RuntimeOrigin::root(), + domain.clone(), + router.clone(), + )); + + let sender = get_test_account_id(); + let msg = MessageMock::First; + let err = DispatchError::Unavailable; + + router.mock_send({ + let sender = sender.clone(); + let msg = msg.clone(); + let err = err.clone(); + + move |mock_sender, mock_msg| { + assert_eq!(sender, mock_sender); + assert_eq!(msg, mock_msg); + + Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(100, 100)), + pays_fee: Pays::Yes, + }, + error: err, + }) + } + }); + + let nonce = OutboundMessageNonce::one(); + + FailedOutboundMessages::::insert( + nonce, + (domain.clone(), sender.clone(), msg.clone(), err.clone()), + ); + + assert_ok!(LiquidityPoolsGateway::process_failed_outbound_message( + RuntimeOrigin::signed(sender.clone()), + nonce + )); + + assert!(FailedOutboundMessages::::contains_key(nonce)); + + event_exists(Event::::OutboundMessageExecutionFailure { + nonce, + sender, + domain, + message: msg, + error: err, + }); + }); + } +} + +mod outbound_queue_impl { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let sender = get_test_account_id(); + let msg = MessageMock::First; + + let router = RouterMock::::default(); + router.mock_init(move || Ok(())); + + assert_ok!(LiquidityPoolsGateway::set_domain_router( + RuntimeOrigin::root(), + domain.clone(), + router.clone(), + )); + + assert_ok!(LiquidityPoolsGateway::submit( + sender.clone(), + domain.clone(), + msg.clone() + )); + + let expected_nonce = OutboundMessageNonce::one(); + + let queue_entry = OutboundMessageQueue::::get(expected_nonce) + .expect("an entry is added to the queue"); + + let gateway_sender = ::Sender::get(); + + assert_eq!(queue_entry, (domain.clone(), gateway_sender, msg.clone())); + + event_exists(Event::::OutboundMessageSubmitted { + sender: ::Sender::get(), + domain, + message: msg, + }); + }); + } #[test] fn local_domain() { new_test_ext().execute_with(|| { diff --git a/pallets/liquidity-pools-gateway/src/weights.rs b/pallets/liquidity-pools-gateway/src/weights.rs index b33dbf3f1b..d72e6e0f91 100644 --- a/pallets/liquidity-pools-gateway/src/weights.rs +++ b/pallets/liquidity-pools-gateway/src/weights.rs @@ -19,6 +19,8 @@ pub trait WeightInfo { fn add_relayer() -> Weight; fn remove_relayer() -> Weight; fn process_msg() -> Weight; + fn process_outbound_message() -> Weight; + fn process_failed_outbound_message() -> Weight; } // NOTE: We use temporary weights here. `execute_epoch` is by far our heaviest @@ -98,4 +100,26 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) } + + fn process_outbound_message() -> Weight { + // TODO: BENCHMARK CORRECTLY + // + // NOTE: Reasonable weight taken from `PoolSystem::set_max_reserve` + // This one has one read and one write for sure and possible one + // read for `AdminOrigin` + Weight::from_parts(30_117_000, 5991) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + + fn process_failed_outbound_message() -> Weight { + // TODO: BENCHMARK CORRECTLY + // + // NOTE: Reasonable weight taken from `PoolSystem::set_max_reserve` + // This one has one read and one write for sure and possible one + // read for `AdminOrigin` + Weight::from_parts(30_117_000, 5991) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(1)) + } } diff --git a/pallets/liquidity-pools/src/message.rs b/pallets/liquidity-pools/src/message.rs index 3e6a84de39..6c2a7a1476 100644 --- a/pallets/liquidity-pools/src/message.rs +++ b/pallets/liquidity-pools/src/message.rs @@ -1,7 +1,7 @@ use cfg_traits::{liquidity_pools::Codec, Seconds}; use cfg_utils::{decode, decode_be_bytes, encode_be}; use frame_support::RuntimeDebug; -use parity_scale_codec::{Decode, Encode, Input}; +use parity_scale_codec::{Decode, Encode, Input, MaxEncodedLen}; use scale_info::TypeInfo; use sp_std::{vec, vec::Vec}; @@ -28,7 +28,7 @@ pub const TOKEN_SYMBOL_SIZE: usize = 32; /// /// NOTE: The sender of a message cannot ensure whether the /// corresponding receiver rejects it. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum Message where Domain: Codec, diff --git a/runtime/altair/src/liquidity_pools.rs b/runtime/altair/src/liquidity_pools.rs index a31e050620..1a1723a40d 100644 --- a/runtime/altair/src/liquidity_pools.rs +++ b/runtime/altair/src/liquidity_pools.rs @@ -11,7 +11,8 @@ // GNU General Public License for more details. use cfg_primitives::{ - liquidity_pools::GeneralCurrencyPrefix, AccountId, Balance, PalletIndex, PoolId, TrancheId, + liquidity_pools::GeneralCurrencyPrefix, AccountId, Balance, OutboundMessageNonce, PalletIndex, + PoolId, TrancheId, }; use cfg_types::{ fixed_point::Ratio, @@ -90,6 +91,7 @@ impl pallet_liquidity_pools_gateway::Config for Runtime { type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = LiquidityPoolsMessage; type OriginRecovery = crate::LiquidityPoolsAxelarGateway; + type OutboundMessageNonce = OutboundMessageNonce; type Router = liquidity_pools_gateway_routers::DomainRouter; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/runtime/centrifuge/src/liquidity_pools.rs b/runtime/centrifuge/src/liquidity_pools.rs index 98832fb21f..aea622284c 100644 --- a/runtime/centrifuge/src/liquidity_pools.rs +++ b/runtime/centrifuge/src/liquidity_pools.rs @@ -11,8 +11,8 @@ // GNU General Public License for more details. use cfg_primitives::{ - liquidity_pools::GeneralCurrencyPrefix, AccountId, Balance, EnsureRootOr, PalletIndex, PoolId, - TrancheId, TwoThirdOfCouncil, + liquidity_pools::GeneralCurrencyPrefix, AccountId, Balance, EnsureRootOr, OutboundMessageNonce, + PalletIndex, PoolId, TrancheId, TwoThirdOfCouncil, }; use cfg_types::{ fixed_point::Ratio, @@ -108,6 +108,7 @@ impl pallet_liquidity_pools_gateway::Config for Runtime { type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = LiquidityPoolsMessage; type OriginRecovery = LiquidityPoolsAxelarGateway; + type OutboundMessageNonce = OutboundMessageNonce; type Router = liquidity_pools_gateway_routers::DomainRouter; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/runtime/common/src/xcm.rs b/runtime/common/src/xcm.rs index 3faf392572..21e4700252 100644 --- a/runtime/common/src/xcm.rs +++ b/runtime/common/src/xcm.rs @@ -126,6 +126,7 @@ mod test { pallet_mock_liquidity_pools, pallet_mock_routers, pallet_mock_try_convert, MessageMock, RouterMock, }; + use cfg_primitives::OutboundMessageNonce; use frame_support::{assert_ok, traits::EnsureOrigin}; use frame_system::EnsureRoot; use pallet_liquidity_pools_gateway::{EnsureLocal, GatewayOrigin}; @@ -218,6 +219,7 @@ mod test { type MaxIncomingMessageSize = ConstU32<1024>; type Message = MessageMock; type OriginRecovery = MockOriginRecovery; + type OutboundMessageNonce = OutboundMessageNonce; type Router = RouterMock; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/runtime/development/src/liquidity_pools.rs b/runtime/development/src/liquidity_pools.rs index d073f5119f..5b6cf8d7df 100644 --- a/runtime/development/src/liquidity_pools.rs +++ b/runtime/development/src/liquidity_pools.rs @@ -12,7 +12,7 @@ use cfg_primitives::{ liquidity_pools::GeneralCurrencyPrefix, AccountId, Balance, EnsureRootOr, HalfOfCouncil, - PalletIndex, PoolId, TrancheId, + OutboundMessageNonce, PalletIndex, PoolId, TrancheId, }; use cfg_types::{ fixed_point::Ratio, @@ -93,6 +93,7 @@ impl pallet_liquidity_pools_gateway::Config for Runtime { type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = LiquidityPoolsMessage; type OriginRecovery = LiquidityPoolsAxelarGateway; + type OutboundMessageNonce = OutboundMessageNonce; type Router = liquidity_pools_gateway_routers::DomainRouter; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index 48f912a97f..a8ab13d4d5 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -263,7 +263,7 @@ type FudgeRelayRuntime = <::FudgeHandle as FudgeHandle> use utils::*; mod development { - use development_runtime::{LocationToAccountId, MinFulfillmentAmountNative, TreasuryAccount}; + use development_runtime::{LocationToAccountId, MinFulfillmentAmountNative}; use super::*; @@ -901,6 +901,8 @@ mod development { use utils::*; mod add_allow_upgrade { + use cfg_types::tokens::LiquidityPoolsWrappedToken; + use super::*; fn add_pool() { @@ -1151,7 +1153,7 @@ mod development { setup_test(&mut env); - env.parachain_state_mut(|| { + let (domain, sender, message) = env.parachain_state_mut(|| { let gateway_sender = ::Sender::get(); let currency_id = AUSD_CURRENCY_ID; @@ -1168,12 +1170,48 @@ mod development { currency_id, )); + let currency_index = + pallet_liquidity_pools::Pallet::::try_get_general_index(currency_id) + .expect("can get general index for currency"); + + let LiquidityPoolsWrappedToken::EVM { + address: evm_address, + .. + } = pallet_liquidity_pools::Pallet::::try_get_wrapped_token(¤cy_id) + .expect("can get wrapped token"); + + let outbound_message = + pallet_liquidity_pools_gateway::OutboundMessageQueue::::get( + T::OutboundMessageNonce::one(), + ) + .expect("expected outbound queue message"); + assert_eq!( - orml_tokens::Pallet::::free_balance(GLMR_CURRENCY_ID, &gateway_sender), - // Ensure it only charged the 0.2 GLMR of fee - DEFAULT_BALANCE_GLMR - decimals(18).saturating_div(5) + outbound_message.2, + Message::AddCurrency { + currency: currency_index, + evm_address, + }, ); + + outbound_message + }); + + let expected_event = + pallet_liquidity_pools_gateway::Event::::OutboundMessageExecutionSuccess { + sender, + domain, + message, + nonce: T::OutboundMessageNonce::one(), + }; + + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, }); + + env.check_event(expected_event) + .expect("expected RouterExecutionSuccess event"); } fn add_currency_should_fail() { @@ -2305,11 +2343,13 @@ mod development { .into() })); + let sender = ::Sender::get(); + // Clearing of foreign InvestState should be dispatched assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectInvest { pool_id, @@ -2424,10 +2464,13 @@ mod development { } .into() })); + + let sender = ::Sender::get(); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: pallet_liquidity_pools::Message::ExecutedCollectInvest { pool_id, @@ -2519,10 +2562,11 @@ mod development { } .into() })); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectInvest { pool_id, @@ -2712,10 +2756,12 @@ mod development { final_amount ); + let sender = ::Sender::get(); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedDecreaseRedeemOrder { pool_id, @@ -2961,11 +3007,13 @@ mod development { .into() })); + let sender = ::Sender::get(); + // Clearing of foreign RedeemState should be dispatched assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectRedeem { pool_id, @@ -3066,10 +3114,13 @@ mod development { } .into() })); + + let sender = ::Sender::get(); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectRedeem { pool_id, @@ -3166,7 +3217,7 @@ mod development { assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectRedeem { pool_id, @@ -3857,10 +3908,13 @@ mod development { ), invest_amount_pool_denominated * 2 ); + + let sender = ::Sender::get(); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedCollectInvest { pool_id, @@ -4083,10 +4137,13 @@ mod development { } .into() })); + + let sender = ::Sender::get(); + assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { - sender: TreasuryAccount::get(), + sender: sender.clone(), domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), message: LiquidityPoolMessage::ExecutedDecreaseInvestOrder { pool_id, @@ -4739,6 +4796,8 @@ mod development { use super::*; mod axelar_evm { + use std::ops::AddAssign; + use super::*; mod utils { @@ -4817,21 +4876,15 @@ mod development { let test_router = DomainRouter::::AxelarEVM(axelar_evm_router); - let set_domain_router_call = - set_domain_router_call(test_domain.clone(), test_router.clone()); - - let council_threshold = 2; - let voting_period = 3; - - execute_via_democracy::( - &mut env, - get_council_members(), - set_domain_router_call, - council_threshold, - voting_period, - 0, - 0, - ); + env.parachain_state_mut(|| { + assert_ok!( + pallet_liquidity_pools_gateway::Pallet::::set_domain_router( + ::RuntimeOrigin::root(), + test_domain.clone(), + test_router, + ) + ); + }); let sender = Keyring::Alice.to_account_id(); let gateway_sender = env.parachain_state(|| { @@ -4843,6 +4896,45 @@ mod development { [0..20], ); + let msg = LiquidityPoolMessage::Transfer { + currency: 0, + sender: Keyring::Alice.to_account_id().into(), + receiver: Keyring::Bob.to_account_id().into(), + amount: 1_000u128, + }; + + // Failure - gateway sender account is not funded. + assert_ok!(env.parachain_state_mut(|| { + as OutboundQueue>::submit( + sender.clone(), + test_domain.clone(), + msg.clone(), + ) + })); + + let mut nonce = T::OutboundMessageNonce::one(); + + let expected_event = + pallet_liquidity_pools_gateway::Event::::OutboundMessageExecutionFailure { + sender: gateway_sender.clone(), + domain: test_domain.clone(), + message: msg.clone(), + error: pallet_evm::Error::::BalanceLow.into(), + nonce, + }; + + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); + + env.check_event(expected_event) + .expect("expected RouterExecutionFailure event"); + + nonce.add_assign(T::OutboundMessageNonce::one()); + + // Success + // Note how both the target address and the gateway sender need to have some // balance. mint_balance_into_derived_account::( @@ -4856,20 +4948,43 @@ mod development { cfg(1_000_000), ); - let msg = LiquidityPoolMessage::Transfer { - currency: 0, - sender: Keyring::Alice.to_account_id().into(), - receiver: Keyring::Bob.to_account_id().into(), - amount: 1_000u128, - }; - - assert_ok!(env.parachain_state(|| { + assert_ok!(env.parachain_state_mut(|| { as OutboundQueue>::submit( - sender, - test_domain, - msg, + sender.clone(), + test_domain.clone(), + msg.clone(), ) })); + + let expected_event = + pallet_liquidity_pools_gateway::Event::::OutboundMessageExecutionSuccess { + sender: gateway_sender.clone(), + domain: test_domain.clone(), + message: msg.clone(), + nonce, + }; + + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); + + env.check_event(expected_event) + .expect("expected RouterExecutionSuccess event"); + + // Router not found + let unused_domain = Domain::EVM(1234); + + env.parachain_state_mut(|| { + assert_noop!( + as OutboundQueue>::submit( + sender, + unused_domain.clone(), + msg, + ), + pallet_liquidity_pools_gateway::Error::::RouterNotFound + ); + }); } crate::test_for_runtimes!([development], test_via_outbound_queue); @@ -4892,6 +5007,13 @@ mod development { setup_test(&mut env); + let msg = Message::::Transfer { + currency: 0, + sender: Keyring::Alice.into(), + receiver: Keyring::Bob.into(), + amount: 1_000u128, + }; + env.parachain_state_mut(|| { let domain_router = router_creation_fn( MultiLocation { @@ -4910,14 +5032,6 @@ mod development { ) ); - let msg = - Message::::Transfer { - currency: 0, - sender: Keyring::Alice.into(), - receiver: Keyring::Bob.into(), - amount: 1_000u128, - }; - assert_ok!( as OutboundQueue>::submit( Keyring::Alice.into(), @@ -4925,16 +5039,27 @@ mod development { msg.clone(), ) ); + }); - assert_noop!( - as OutboundQueue>::submit( - Keyring::Alice.into(), - Domain::EVM(1285), - msg.clone(), - ), - pallet_liquidity_pools_gateway::Error::::RouterNotFound, - ); + let gateway_sender = env.parachain_state(|| { + ::Sender::get() }); + + let expected_event = + pallet_liquidity_pools_gateway::Event::::OutboundMessageExecutionSuccess { + sender: gateway_sender, + domain: TEST_DOMAIN, + message: msg, + nonce: T::OutboundMessageNonce::one(), + }; + + env.pass(Blocks::UntilEvent { + event: expected_event.clone().into(), + limit: 3, + }); + + env.check_event(expected_event) + .expect("expected RouterExecutionSuccess event"); } type RouterCreationFn = @@ -5031,6 +5156,77 @@ mod development { mod gateway { use super::*; + fn set_domain_router() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::::default() + .add(genesis::balances::(cfg(1_000))) + .add::(genesis::council_members::( + get_council_members(), + )) + .storage(), + ); + + let test_domain = Domain::EVM(1); + + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code: Vec = vec![0, 0, 0]; + let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); + let liquidity_pools_contract_address = H160::from_low_u64_be(2); + + env.parachain_state_mut(|| { + pallet_evm::AccountCodes::::insert(axelar_contract_address, axelar_contract_code) + }); + + let evm_domain = EVMDomain { + target_contract_address: axelar_contract_address, + target_contract_hash: axelar_contract_hash, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(1_000_000), + gas_price: U256::from(10), + }, + }; + + let axelar_evm_router = AxelarEVMRouter:: { + router: EVMRouter { + evm_domain, + _marker: Default::default(), + }, + evm_chain: BoundedVec::>::try_from( + "ethereum".as_bytes().to_vec(), + ) + .unwrap(), + _marker: Default::default(), + liquidity_pools_contract_address, + }; + + let test_router = DomainRouter::::AxelarEVM(axelar_evm_router); + + let set_domain_router_call = + set_domain_router_call(test_domain.clone(), test_router.clone()); + + let council_threshold = 2; + let voting_period = 3; + + execute_via_democracy::( + &mut env, + get_council_members(), + set_domain_router_call, + council_threshold, + voting_period, + 0, + 0, + ); + + env.parachain_state(|| { + let router = + pallet_liquidity_pools_gateway::Pallet::::domain_routers(test_domain) + .expect("domain router is set"); + + assert!(router.eq(&test_router)); + }); + } + fn add_remove_instances() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::::default() @@ -5058,17 +5254,15 @@ mod development { 0, ); - let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceAdded { - instance: test_instance.clone(), - }; - - env.pass(Blocks::UntilEvent { - event: expected_event.clone().into(), - limit: 3, + env.parachain_state(|| { + assert!( + pallet_liquidity_pools_gateway::Allowlist::::contains_key( + test_instance.domain(), + test_instance.clone() + ) + ); }); - env.check_event(expected_event); - let remove_instance_call = remove_instance_call::(test_instance.clone()); execute_via_democracy::( @@ -5081,16 +5275,14 @@ mod development { ref_index, ); - let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceRemoved { - instance: test_instance.clone(), - }; - - env.pass(Blocks::UntilEvent { - event: expected_event.clone().into(), - limit: 3, + env.parachain_state(|| { + assert!( + !pallet_liquidity_pools_gateway::Allowlist::::contains_key( + test_instance.domain(), + test_instance.clone() + ) + ); }); - - env.check_event(expected_event); } fn process_msg() { @@ -5120,17 +5312,15 @@ mod development { 0, ); - let expected_event = pallet_liquidity_pools_gateway::Event::::InstanceAdded { - instance: test_instance.clone(), - }; - - env.pass(Blocks::UntilEvent { - event: expected_event.clone().into(), - limit: 3, + env.parachain_state(|| { + assert!( + pallet_liquidity_pools_gateway::Allowlist::::contains_key( + test_instance.domain(), + test_instance.clone() + ) + ); }); - env.check_event(expected_event); - let msg = LiquidityPoolMessage::AddPool { pool_id: 123 }; let encoded_msg = msg.serialize(); @@ -5152,6 +5342,7 @@ mod development { }); } + crate::test_for_runtimes!([development], set_domain_router); crate::test_for_runtimes!([development], add_remove_instances); crate::test_for_runtimes!([development], process_msg); }