diff --git a/Cargo.lock b/Cargo.lock index b3454a87f4..58caa7ccad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,40 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "connectors-gateway-routers" +version = "0.0.1" +dependencies = [ + "cfg-mocks", + "cfg-primitives", + "cfg-traits", + "cfg-types", + "cumulus-primitives-core", + "ethabi", + "frame-support", + "frame-system", + "orml-traits", + "pallet-balances", + "pallet-connectors-gateway", + "pallet-ethereum", + "pallet-ethereum-transaction", + "pallet-evm", + "pallet-evm-chain-id", + "pallet-evm-precompile-simple", + "pallet-timestamp", + "pallet-xcm-transactor", + "parity-scale-codec 3.4.0", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "xcm", + "xcm-builder", + "xcm-executor", + "xcm-primitives", +] + [[package]] name = "const-oid" version = "0.9.2" @@ -2605,6 +2639,7 @@ dependencies = [ "cfg-traits", "cfg-types", "chainbridge", + "connectors-gateway-routers", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", @@ -2623,6 +2658,7 @@ dependencies = [ "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", + "getrandom 0.2.8", "hex", "hex-literal 0.3.4", "moonbeam-relay-encoder", diff --git a/Cargo.toml b/Cargo.toml index 2b2ad5ea7d..fa93242894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "pallets/block-rewards", "pallets/connectors", "pallets/connectors-gateway", + "pallets/connectors-gateway/routers", "pallets/claims", "pallets/collator-allowlist", "pallets/crowdloan-claim", diff --git a/pallets/connectors-gateway/routers/Cargo.toml b/pallets/connectors-gateway/routers/Cargo.toml new file mode 100644 index 0000000000..4781e3c378 --- /dev/null +++ b/pallets/connectors-gateway/routers/Cargo.toml @@ -0,0 +1,100 @@ +[package] +authors = ["Centrifuge "] +description = "Centrifuge Connectors Gateway Routers" +edition = "2021" +license = "LGPL-3.0" +name = "connectors-gateway-routers" +repository = "https://github.com/centrifuge/centrifuge-chain" +version = "0.0.1" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"], default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +scale-info = { version = "2.3.0", default-features = false, features = ["derive"] } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# Substrate +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# XCM +pallet-xcm-transactor = { git = "https://github.com/PureStake/moonbeam", default-features = false, rev = "00b3e3d97806e889b02e1bcb4b69e65433dd805d" } +xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "release-v0.9.38" } +xcm-primitives = { git = "https://github.com/PureStake/moonbeam", default-features = false, rev = "00b3e3d97806e889b02e1bcb4b69e65433dd805d" } + +# EVM +ethabi = { version = "16.0", default-features = false } +pallet-ethereum = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +pallet-evm = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } + +# Custom crates +cfg-traits = { path = "../../../libs/traits", default-features = false } +cfg-types = { path = "../../../libs/types", default-features = false } + +# Local pallets +pallet-connectors-gateway = { path = "../.", default-features = false } +pallet-ethereum-transaction = { path = "../../ethereum-transaction", default-features = false } + +[dev-dependencies] +cumulus-primitives-core = { git = "https://github.com/purestake/cumulus", branch = "moonbeam-polkadot-v0.9.38", default-features = false } + +xcm-builder = { git = "https://github.com/purestake/polkadot", branch = "moonbeam-polkadot-v0.9.38", default-features = false } +xcm-executor = { git = "https://github.com/purestake/polkadot", branch = "moonbeam-polkadot-v0.9.38", default-features = false } + +pallet-evm-chain-id = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +pallet-evm-precompile-simple = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +pallet-timestamp = { git = "https://github.com/purestake/substrate", branch = "moonbeam-polkadot-v0.9.38" } + +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } + +orml-traits = { git = "https://github.com/purestake/open-runtime-module-library", branch = "moonbeam-polkadot-v0.9.38", default-features = false } + +cfg-mocks = { path = "../../../libs/mocks" } +cfg-primitives = { path = "../../../libs/primitives" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } + +[features] +default = ["std"] +runtime-benchmarks = [ + "cfg-traits/runtime-benchmarks", + "cfg-types/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-connectors-gateway/runtime-benchmarks", + "pallet-ethereum/runtime-benchmarks", + "pallet-ethereum-transaction/runtime-benchmarks", + "pallet-xcm-transactor/runtime-benchmarks", + "xcm-primitives/runtime-benchmarks", +] +std = [ + "codec/std", + "cfg-types/std", + "cfg-traits/std", + "frame-support/std", + "frame-system/std", + "sp-std/std", + "sp-core/std", + "xcm/std", + "pallet-connectors-gateway/std", + "pallet-xcm-transactor/std", + "pallet-ethereum/std", + "pallet-ethereum-transaction/std", + "xcm-primitives/std", + "ethabi/std", + "scale-info/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "cfg-traits/try-runtime", + "cfg-types/try-runtime", + "pallet-connectors-gateway/try-runtime", + "pallet-ethereum/try-runtime", + "pallet-ethereum-transaction/try-runtime", + "pallet-xcm-transactor/try-runtime", +] diff --git a/pallets/connectors-gateway/routers/src/lib.rs b/pallets/connectors-gateway/routers/src/lib.rs new file mode 100644 index 0000000000..77c8a68501 --- /dev/null +++ b/pallets/connectors-gateway/routers/src/lib.rs @@ -0,0 +1,352 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +#![cfg_attr(not(feature = "std"), no_std)] + +use cfg_traits::{connectors::Router, ethereum::EthereumTransactor}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{DispatchError, DispatchResult, Weight}, + ensure, + traits::OriginTrait, +}; +use pallet_xcm_transactor::{Currency, CurrencyPayment, TransactWeights}; +use scale_info::TypeInfo; +use sp_core::{bounded::BoundedVec, ConstU32, H160, H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_std::{boxed::Box, marker::PhantomData, vec::Vec}; +use xcm::{ + latest::{MultiLocation, OriginKind}, + VersionedMultiLocation, +}; + +use crate::{axelar_evm::AxelarEVMRouter, ethereum_xcm::EthereumXCMRouter}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +pub mod routers; + +pub use routers::*; + +type CurrencyIdOf = ::CurrencyId; +type MessageOf = ::Message; +type AccountIdOf = ::AccountId; + +const CONNECTORS_FUNCTION_NAME: &str = "handle"; +const CONNECTORS_MESSAGE_PARAM: &str = "message"; + +const AXELAR_FUNCTION_NAME: &str = "callContract"; +const AXELAR_DESTINATION_CHAIN_PARAM: &str = "destinationChain"; +const AXELAR_DESTINATION_CONTRACT_ADDRESS_PARAM: &str = "destinationContractAddress"; +const AXELAR_PAYLOAD_PARAM: &str = "payload"; + +/// The routers used for outgoing messages. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum DomainRouter +where + T: frame_system::Config + + pallet_xcm_transactor::Config + + pallet_connectors_gateway::Config + + pallet_ethereum_transaction::Config + + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, +{ + EthereumXCM(EthereumXCMRouter), + AxelarEVM(AxelarEVMRouter), + AxelarXCM(AxelarXCMRouter), +} + +impl Router for DomainRouter +where + T: frame_system::Config + + pallet_xcm_transactor::Config + + pallet_connectors_gateway::Config + + pallet_ethereum_transaction::Config + + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, +{ + type Message = MessageOf; + type Sender = AccountIdOf; + + fn init(&self) -> DispatchResult { + match self { + DomainRouter::EthereumXCM(r) => r.do_init(), + DomainRouter::AxelarEVM(r) => r.do_init(), + DomainRouter::AxelarXCM(r) => r.do_init(), + } + } + + fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResult { + match self { + DomainRouter::EthereumXCM(r) => r.do_send(sender, message), + DomainRouter::AxelarEVM(r) => r.do_send(sender, message), + DomainRouter::AxelarXCM(r) => r.do_send(sender, message), + } + } +} + +/// A generic router used for executing EVM calls. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct EVMRouter +where + T: frame_system::Config + pallet_ethereum_transaction::Config + pallet_evm::Config, +{ + pub evm_domain: EVMDomain, + pub _marker: PhantomData, +} + +impl EVMRouter +where + T: frame_system::Config + pallet_ethereum_transaction::Config + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, +{ + /// Performs an extra check to ensure that the actual contract is deployed + /// at the provided address and that the contract code hash matches. + pub fn do_init(&self) -> DispatchResult { + let code = pallet_evm::AccountCodes::::get(self.evm_domain.target_contract_address); + + ensure!( + BlakeTwo256::hash_of(&code) == self.evm_domain.target_contract_hash, + DispatchError::Other("Target contract code does not match"), + ); + + Ok(()) + } + + /// NOTE - the sender account ID provided here will be converted to an EVM + /// address via truncating. When the call is processed by the underlying EVM + /// 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 { + 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, + msg.as_slice(), + self.evm_domain.fee_values.value, + self.evm_domain.fee_values.gas_price, + self.evm_domain.fee_values.gas_limit, + ) + .map_err(|e| e.error)?; + + Ok(()) + } +} + +/// The EVMDomain holds all relevant information for validating and executing +/// the EVM call. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct EVMDomain { + /// The address of the contract deployed in our EVM. + pub target_contract_address: H160, + + /// The `BlakeTwo256` hash of the target contract code. + /// + /// This is used during router initialization to ensure that the correct + /// contract code is used. + pub target_contract_hash: H256, + + /// The values used when executing the EVM call. + pub fee_values: FeeValues, +} + +/// The FeeValues holds all information related to the transaction costs. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct FeeValues { + /// The value used when executing the EVM call. + pub value: U256, + + /// The gas price used when executing the EVM call. + pub gas_price: U256, + + /// The gas limit used when executing the EVM call. + pub gas_limit: U256, +} + +/// A generic router used for executing XCM calls. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct XCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config, +{ + pub xcm_domain: XcmDomain, + pub _marker: PhantomData, +} + +impl XCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config, +{ + /// Sets the weight information for the provided XCM domain location, and + /// the fee per second for the provided fee asset location. + pub fn do_init(&self) -> DispatchResult { + pallet_xcm_transactor::Pallet::::set_transact_info( + ::RuntimeOrigin::root(), + self.xcm_domain.location.clone(), + self.xcm_domain.transact_info.transact_extra_weight, + self.xcm_domain.transact_info.max_weight, + self.xcm_domain.transact_info.transact_extra_weight_signed, + )?; + + pallet_xcm_transactor::Pallet::::set_fee_per_second( + ::RuntimeOrigin::root(), + self.xcm_domain.fee_asset_location.clone(), + self.xcm_domain.fee_per_second, + ) + } + + /// Encodes the Connectors 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 { + let ethereum_xcm_call = get_encoded_ethereum_xcm_call::(self.xcm_domain.clone(), msg) + .map_err(|_| DispatchError::Other("encoded ethereum xcm call retrieval"))?; + + pallet_xcm_transactor::Pallet::::transact_through_sovereign( + ::RuntimeOrigin::root(), + // The destination to which the message should be sent. + self.xcm_domain.location.clone(), + // The sender will pay for this transaction. + sender, + // The currency in which we want to pay fees. + CurrencyPayment { + currency: Currency::AsCurrencyId(self.xcm_domain.fee_currency.clone()), + fee_amount: None, + }, + // The call to be executed in the destination chain. + ethereum_xcm_call, + OriginKind::SovereignAccount, + TransactWeights { + // Convert the max gas_limit into a max transact weight following + // Moonbeam's formula. + transact_required_weight_at_most: Weight::from_all( + self.xcm_domain.max_gas_limit * 25_000 + 100_000_000, + ), + overall_weight: None, + }, + )?; + + Ok(()) + } +} + +pub(crate) fn get_encoded_ethereum_xcm_call( + xcm_domain: XcmDomain, + msg: Vec, +) -> Result, ()> +where + T: frame_system::Config + pallet_xcm_transactor::Config, +{ + let input = + BoundedVec::>::try_from(msg) + .map_err(|_| ())?; + + let mut encoded: Vec = Vec::new(); + + encoded.append(&mut xcm_domain.ethereum_xcm_transact_call_index.into_inner()); + + encoded.append( + &mut xcm_primitives::EthereumXcmTransaction::V1(xcm_primitives::EthereumXcmTransactionV1 { + gas_limit: U256::from(xcm_domain.max_gas_limit), + fee_payment: xcm_primitives::EthereumXcmFee::Auto, + action: pallet_ethereum::TransactionAction::Call(xcm_domain.contract_address), + value: U256::zero(), + input, + access_list: None, + }) + .encode(), + ); + + Ok(encoded) +} + +/// XcmDomain gathers all the required fields to build and send remote +/// calls to a specific XCM-based Domain. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] +pub struct XcmDomain { + /// The XCM multilocation of the domain. + pub location: Box, + + /// The ethereum_xcm::Call::transact call index on a given domain. + /// It should contain the pallet index + the `transact` call index, to which + /// we will append the eth_tx param. + /// + /// You can obtain this value by building an ethereum_xcm::transact call + /// with Polkadot JS on the target chain. + pub ethereum_xcm_transact_call_index: + BoundedVec>, + + /// The target contract address on a given domain. + pub contract_address: H160, + + /// The max gas_limit we want to propose for a remote evm execution + pub max_gas_limit: u64, + + /// The XCM transact info that will be stored in the + /// `TransactInfoWithWeightLimit` storage of the XCM transactor pallet. + pub transact_info: XcmTransactInfo, + + /// The currency in which execution fees will be paid on + pub fee_currency: CurrencyId, + + /// The fee per second that will be stored in the + /// `DestinationAssetFeePerSecond` storage of the XCM transactor pallet. + pub fee_per_second: u128, + + /// The location of the asset used for paying XCM fees. + pub fee_asset_location: Box, +} + +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +/// XcmTransactInfo hold all the weight related information required for the XCM +/// transactor pallet. +pub struct XcmTransactInfo { + pub transact_extra_weight: Weight, + pub max_weight: Weight, + pub transact_extra_weight_signed: Option, +} + +/// NOTE: Remove this custom implementation once the following underlying data +/// implements MaxEncodedLen: +/// * Polkadot Repo: xcm::VersionedMultiLocation +/// * PureStake Repo: pallet_xcm_transactor::Config::CurrencyId +impl MaxEncodedLen for XcmDomain +where + XcmDomain: Encode, +{ + fn max_encoded_len() -> usize { + // The domain's `VersionedMultiLocation` (custom bound) + MultiLocation::max_encoded_len() + // From the enum wrapping of `VersionedMultiLocation` for the XCM domain location. + .saturating_add(1) + // From the enum wrapping of `VersionedMultiLocation` for the asset fee location. + .saturating_add(1) + // The ethereum xcm call index (default bound) + .saturating_add(BoundedVec::< + u8, + ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>, + >::max_encoded_len()) + // The contract address (default bound) + .saturating_add(H160::max_encoded_len()) + // The fee currency (custom bound) + .saturating_add(cfg_types::tokens::CurrencyId::max_encoded_len()) + // The XcmTransactInfo + .saturating_add(XcmTransactInfo::max_encoded_len()) + } +} diff --git a/pallets/connectors-gateway/routers/src/mock.rs b/pallets/connectors-gateway/routers/src/mock.rs new file mode 100644 index 0000000000..f77d882d60 --- /dev/null +++ b/pallets/connectors-gateway/routers/src/mock.rs @@ -0,0 +1,475 @@ +use std::str::FromStr; + +use cfg_mocks::{pallet_mock_connectors, pallet_mock_routers, MessageMock, RouterMock}; +use cfg_types::domain_address::DomainAddress; +use codec::{Decode, Encode}; +use cumulus_primitives_core::{ + Instruction, MultiAsset, MultiLocation, PalletInstance, Parachain, SendError, Xcm, XcmHash, +}; +use frame_support::{ + parameter_types, + traits::{FindAuthor, PalletInfo as PalletInfoTrait}, + weights::Weight, +}; +use frame_system::EnsureRoot; +use pallet_connectors_gateway::EnsureLocal; +use pallet_ethereum::IntermediateStateRoot; +use pallet_evm::{ + runner::stack::Runner, AddressMapping, EnsureAddressNever, EnsureAddressRoot, FeeCalculator, + FixedGasWeightMapping, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet, + SubstrateBlockHashMapping, +}; +use sp_core::{crypto::AccountId32, ByteArray, ConstU16, ConstU32, ConstU64, H160, H256, U256}; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + ConsensusEngineId, +}; +use sp_std::{cell::RefCell, marker::PhantomData}; +use xcm::{ + latest::{ + Error as XcmError, InteriorMultiLocation, NetworkId, Result as XcmResult, SendResult, + XcmContext, + }, + v3::{Junction, Junctions, MultiAssets, SendXcm}, +}; +use xcm_executor::{ + traits::{TransactAsset, WeightBounds}, + Assets, +}; +use xcm_primitives::{ + HrmpAvailableCalls, HrmpEncodeCall, UtilityAvailableCalls, UtilityEncodeCall, XcmTransact, +}; + +pub type Balance = u128; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + MockConnectors: pallet_mock_connectors, + ConnectorsGateway: pallet_connectors_gateway, + XcmTransactor: pallet_xcm_transactor::{Pallet, Call, Event}, + EVM: pallet_evm::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage}, + Ethereum: pallet_ethereum::{Pallet, Call, Storage, Event, Origin}, + EthereumTransaction: pallet_ethereum_transaction, + } +); + +frame_support::parameter_types! { + pub const MaxConnectorsPerDomain: u32 = 3; + pub const MaxIncomingMessageSize: u32 = 1024; +} + +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId32; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = ConstU64<250>; + type BlockLength = (); + type BlockNumber = u64; + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = u64; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<42>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl pallet_mock_connectors::Config for Runtime { + type DomainAddress = DomainAddress; + type Message = MessageMock; +} + +impl pallet_ethereum_transaction::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + +impl pallet_mock_routers::Config for Runtime {} + +impl pallet_connectors_gateway::Config for Runtime { + type AdminOrigin = EnsureRoot; + type InboundQueue = MockConnectors; + type LocalEVMOrigin = EnsureLocal; + type MaxIncomingMessageSize = MaxIncomingMessageSize; + type Message = MessageMock; + type Router = RouterMock; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type WeightInfo = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1000; +} + +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; + type Moment = u64; + type OnTimestampSet = (); + type WeightInfo = (); +} + +/////////////////////// +// EVM pallet mocks. // +/////////////////////// + +pub struct FixedGasPrice; +impl FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + // Return some meaningful gas price and weight + (1_000_000_000u128.into(), Weight::from_ref_time(7u64)) + } +} + +/// Identity address mapping. +pub struct IdentityAddressMapping; + +impl AddressMapping for IdentityAddressMapping { + fn into_account_id(address: H160) -> AccountId32 { + let tag = b"EVM"; + let mut bytes = [0; 32]; + bytes[0..20].copy_from_slice(address.as_bytes()); + bytes[20..28].copy_from_slice(&1u64.to_be_bytes()); + bytes[28..31].copy_from_slice(tag); + + AccountId32::from_slice(bytes.as_slice()).unwrap() + } +} + +pub struct FindAuthorTruncated; +impl FindAuthor for FindAuthorTruncated { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(H160::from_str("1234500000000000000000000000000000000000").unwrap()) + } +} + +pub struct MockPrecompileSet; + +impl PrecompileSet for MockPrecompileSet { + /// Tries to execute a precompile in the precompile set. + /// If the provided address is not a precompile, returns None. + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + let address = handle.code_address(); + + if address == H160::from_low_u64_be(1) { + return Some(pallet_evm_precompile_simple::Identity::execute(handle)); + } + + None + } + + /// Check if the given address is a precompile. Should only be called to + /// perform the check while not executing the precompile afterward, since + /// `execute` already performs a check internally. + fn is_precompile(&self, address: H160) -> bool { + address == H160::from_low_u64_be(1) + } +} + +parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub WeightPerGas: Weight = Weight::from_ref_time(20_000); + pub MockPrecompiles: MockPrecompileSet = MockPrecompileSet; +} + +impl pallet_evm::Config for Runtime { + type AddressMapping = IdentityAddressMapping; + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = SubstrateBlockHashMapping; + type CallOrigin = EnsureAddressRoot; + type ChainId = (); + type Currency = Balances; + type FeeCalculator = FixedGasPrice; + type FindAuthor = FindAuthorTruncated; + type GasWeightMapping = FixedGasWeightMapping; + type OnChargeTransaction = (); + type OnCreate = (); + type PrecompilesType = MockPrecompileSet; + type PrecompilesValue = MockPrecompiles; + type Runner = Runner; + type RuntimeEvent = RuntimeEvent; + type WeightPerGas = WeightPerGas; + type WithdrawOrigin = EnsureAddressNever; +} + +impl pallet_ethereum::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type StateRoot = IntermediateStateRoot; +} + +/////////////////////////// +// XCM transactor mocks. // +/////////////////////////// + +// Transactors for the mock runtime. Only relay chain +#[derive(Clone, Eq, Debug, PartialEq, Ord, PartialOrd, Encode, Decode, scale_info::TypeInfo)] +pub enum Transactors { + Relay, +} + +#[cfg(feature = "runtime-benchmarks")] +impl Default for Transactors { + fn default() -> Self { + Transactors::Relay + } +} + +impl XcmTransact for Transactors { + fn destination(self) -> MultiLocation { + match self { + Transactors::Relay => MultiLocation::parent(), + } + } +} + +impl UtilityEncodeCall for Transactors { + fn encode_call(self, call: UtilityAvailableCalls) -> Vec { + match self { + Transactors::Relay => match call { + UtilityAvailableCalls::AsDerivative(a, b) => { + let mut call = + RelayCall::Utility(UtilityCall::AsDerivative(a.clone())).encode(); + call.append(&mut b.clone()); + call + } + }, + } + } +} + +pub struct AccountIdToMultiLocation; +impl sp_runtime::traits::Convert for AccountIdToMultiLocation { + fn convert(_account: AccountId32) -> MultiLocation { + let as_h160: H160 = H160::repeat_byte(0xAA); + MultiLocation::new( + 0, + Junctions::X1(Junction::AccountKey20 { + network: None, + key: as_h160.as_fixed_bytes().clone(), + }), + ) + } +} + +pub struct DummyAssetTransactor; +impl TransactAsset for DummyAssetTransactor { + fn deposit_asset(_what: &MultiAsset, _who: &MultiLocation, _context: &XcmContext) -> XcmResult { + Ok(()) + } + + fn withdraw_asset( + _what: &MultiAsset, + _who: &MultiLocation, + _context: Option<&XcmContext>, + ) -> Result { + Ok(Assets::default()) + } +} + +pub struct CurrencyIdToMultiLocation; + +pub type AssetId = u128; + +#[derive(Clone, Eq, Debug, PartialEq, Ord, PartialOrd, Encode, Decode, scale_info::TypeInfo)] +pub enum CurrencyId { + SelfReserve, + OtherReserve(AssetId), +} + +impl sp_runtime::traits::Convert> for CurrencyIdToMultiLocation { + fn convert(currency: CurrencyId) -> Option { + match currency { + CurrencyId::SelfReserve => { + let multi: MultiLocation = SelfReserve::get(); + Some(multi) + } + // To distinguish between relay and others, specially for reserve asset + CurrencyId::OtherReserve(asset) => { + if asset == 0 { + Some(MultiLocation::parent()) + } else { + Some(MultiLocation::new(1, Junctions::X1(Parachain(2)))) + } + } + } + } +} + +pub struct MockHrmpEncoder; + +impl HrmpEncodeCall for MockHrmpEncoder { + fn hrmp_encode_call(call: HrmpAvailableCalls) -> Result, XcmError> { + match call { + HrmpAvailableCalls::InitOpenChannel(_, _, _) => { + Ok(RelayCall::Hrmp(HrmpCall::Init()).encode()) + } + HrmpAvailableCalls::AcceptOpenChannel(_) => { + Ok(RelayCall::Hrmp(HrmpCall::Accept()).encode()) + } + HrmpAvailableCalls::CloseChannel(_) => Ok(RelayCall::Hrmp(HrmpCall::Close()).encode()), + HrmpAvailableCalls::CancelOpenRequest(_, _) => { + Ok(RelayCall::Hrmp(HrmpCall::Close()).encode()) + } + } + } +} + +// Simulates sending a XCM message +thread_local! { + pub static SENT_XCM: RefCell> = RefCell::new(Vec::new()); +} +pub fn sent_xcm() -> Vec<(MultiLocation, xcm::v3::opaque::Xcm)> { + SENT_XCM.with(|q| (*q.borrow()).clone()) +} +pub struct TestSendXcm; +impl SendXcm for TestSendXcm { + type Ticket = (); + + fn validate( + destination: &mut Option, + message: &mut Option, + ) -> SendResult { + SENT_XCM.with(|q| { + q.borrow_mut() + .push((destination.clone().unwrap(), message.clone().unwrap())) + }); + Ok(((), MultiAssets::new())) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(XcmHash::default()) + } +} + +#[derive(Encode, Decode)] +pub enum RelayCall { + #[codec(index = 0u8)] + // the index should match the position of the module in `construct_runtime!` + Utility(UtilityCall), + #[codec(index = 1u8)] + // the index should match the position of the module in `construct_runtime!` + Hrmp(HrmpCall), +} + +#[derive(Encode, Decode)] +pub enum UtilityCall { + #[codec(index = 0u8)] + AsDerivative(u16), +} + +#[derive(Encode, Decode)] +pub enum HrmpCall { + #[codec(index = 0u8)] + Init(), + #[codec(index = 1u8)] + Accept(), + #[codec(index = 2u8)] + Close(), +} + +pub type MaxHrmpRelayFee = xcm_builder::Case; + +pub struct DummyWeigher(PhantomData); + +impl WeightBounds for DummyWeigher { + fn weight(_message: &mut Xcm) -> Result { + Ok(Weight::zero()) + } + + fn instr_weight(_instruction: &Instruction) -> Result { + Ok(Weight::zero()) + } +} + +parameter_types! { + pub const RelayNetwork: NetworkId = NetworkId::Polkadot; + + pub ParachainId: cumulus_primitives_core::ParaId = 100.into(); + + pub SelfLocation: MultiLocation = + MultiLocation::new(1, Junctions::X1(Parachain(ParachainId::get().into()))); + + pub SelfReserve: MultiLocation = MultiLocation::new( + 1, + Junctions::X2( + Parachain(ParachainId::get().into()), + PalletInstance( + ::PalletInfo::index::().unwrap() as u8 + ) + )); + + pub const BaseXcmWeight: xcm::latest::Weight = xcm::latest::Weight::from_ref_time(1000); + + pub MaxFee: MultiAsset = (MultiLocation::parent(), 1_000_000_000_000u128).into(); + + pub UniversalLocation: InteriorMultiLocation = RelayNetwork::get().into(); +} + +impl pallet_xcm_transactor::Config for Runtime { + type AccountIdToMultiLocation = AccountIdToMultiLocation; + type AssetTransactor = DummyAssetTransactor; + type Balance = Balance; + type BaseXcmWeight = BaseXcmWeight; + type CurrencyId = CurrencyId; + type CurrencyIdToMultiLocation = CurrencyIdToMultiLocation; + type DerivativeAddressRegistrationOrigin = EnsureRoot; + type HrmpEncoder = MockHrmpEncoder; + type HrmpManipulatorOrigin = EnsureRoot; + type MaxHrmpFee = MaxHrmpRelayFee; + type ReserveProvider = orml_traits::location::RelativeReserveProvider; + type RuntimeEvent = RuntimeEvent; + type SelfLocation = SelfLocation; + type SovereignAccountDispatcherOrigin = EnsureRoot; + type Transactor = Transactors; + type UniversalLocation = UniversalLocation; + type Weigher = DummyWeigher; + type WeightInfo = (); + type XcmSender = TestSendXcm; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(storage); + ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); + + ext +} diff --git a/pallets/connectors-gateway/routers/src/routers/axelar_evm.rs b/pallets/connectors-gateway/routers/src/routers/axelar_evm.rs new file mode 100644 index 0000000000..33edb3d227 --- /dev/null +++ b/pallets/connectors-gateway/routers/src/routers/axelar_evm.rs @@ -0,0 +1,174 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +use cfg_traits::connectors::Codec; +use codec::{Decode, Encode, MaxEncodedLen}; +use ethabi::{Contract, Function, Param, ParamType, Token}; +use frame_support::dispatch::{DispatchError, DispatchResult}; +use scale_info::{ + prelude::string::{String, ToString}, + TypeInfo, +}; +use sp_core::H160; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, vec, vec::Vec}; + +use crate::{ + AccountIdOf, EVMRouter, MessageOf, AXELAR_DESTINATION_CHAIN_PARAM, + AXELAR_DESTINATION_CONTRACT_ADDRESS_PARAM, AXELAR_FUNCTION_NAME, AXELAR_PAYLOAD_PARAM, + CONNECTORS_FUNCTION_NAME, CONNECTORS_MESSAGE_PARAM, +}; + +/// EVMChain holds all supported EVM chains. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum EVMChain { + Ethereum, + Goerli, +} + +/// Required due to the naming convention defined by Axelar here: +/// +impl ToString for EVMChain { + fn to_string(&self) -> String { + match self { + EVMChain::Ethereum => "Ethereum".to_string(), + EVMChain::Goerli => "ethereum-2".to_string(), + } + } +} + +/// The router used for executing the Connectors contract via Axelar. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct AxelarEVMRouter +where + T: frame_system::Config + + pallet_connectors_gateway::Config + + pallet_ethereum_transaction::Config + + pallet_evm::Config, +{ + pub router: EVMRouter, + pub evm_chain: EVMChain, + pub connectors_contract_address: H160, + pub _marker: PhantomData, +} + +impl AxelarEVMRouter +where + T: frame_system::Config + + pallet_connectors_gateway::Config + + pallet_ethereum_transaction::Config + + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, +{ + /// Calls the init function on the EVM router. + pub fn do_init(&self) -> DispatchResult { + self.router.do_init() + } + + /// Encodes the Connectors 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 { + let eth_msg = get_axelar_encoded_msg( + msg.serialize(), + self.evm_chain.clone(), + self.connectors_contract_address, + ) + .map_err(DispatchError::Other)?; + + self.router.do_send(sender, eth_msg) + } +} + +/// Encodes the provided message into the format required for submitting it +/// to the Axelar contract which in turn submits it to the Connectors +/// contract. +/// +/// Axelar contract call: +/// +/// +/// Connectors contract call: +/// +pub(crate) fn get_axelar_encoded_msg( + serialized_msg: Vec, + target_chain: EVMChain, + target_contract: H160, +) -> Result, &'static str> { + #[allow(deprecated)] + let encoded_connectors_contract = Contract { + constructor: None, + functions: BTreeMap::>::from([( + CONNECTORS_FUNCTION_NAME.to_string(), + vec![Function { + name: CONNECTORS_FUNCTION_NAME.into(), + inputs: vec![Param { + name: CONNECTORS_MESSAGE_PARAM.into(), + kind: ParamType::Bytes, + internal_type: None, + }], + outputs: vec![], + constant: false, + state_mutability: Default::default(), + }], + )]), + events: Default::default(), + errors: Default::default(), + receive: false, + fallback: false, + } + .function(CONNECTORS_FUNCTION_NAME) + .map_err(|_| "cannot retrieve Connectors contract function")? + .encode_input(&[Token::Bytes(serialized_msg)]) + .map_err(|_| "cannot encode input for Connectors contract function")?; + + #[allow(deprecated)] + let encoded_axelar_contract = Contract { + constructor: None, + functions: BTreeMap::>::from([( + AXELAR_FUNCTION_NAME.into(), + vec![Function { + name: AXELAR_FUNCTION_NAME.into(), + inputs: vec![ + Param { + name: AXELAR_DESTINATION_CHAIN_PARAM.into(), + kind: ParamType::String, + internal_type: None, + }, + Param { + name: AXELAR_DESTINATION_CONTRACT_ADDRESS_PARAM.into(), + kind: ParamType::String, + internal_type: None, + }, + Param { + name: AXELAR_PAYLOAD_PARAM.into(), + kind: ParamType::Bytes, + internal_type: None, + }, + ], + outputs: vec![], + constant: false, + state_mutability: Default::default(), + }], + )]), + events: Default::default(), + errors: Default::default(), + receive: false, + fallback: false, + } + .function(AXELAR_FUNCTION_NAME) + .map_err(|_| "cannot retrieve Axelar contract function")? + .encode_input(&[ + Token::String(target_chain.to_string()), + Token::String(target_contract.to_string()), + Token::Bytes(encoded_connectors_contract), + ]) + .map_err(|_| "cannot encode input for Axelar contract function")?; + + Ok(encoded_axelar_contract) +} diff --git a/pallets/connectors-gateway/routers/src/routers/axelar_xcm.rs b/pallets/connectors-gateway/routers/src/routers/axelar_xcm.rs new file mode 100644 index 0000000000..9bd77a0d8c --- /dev/null +++ b/pallets/connectors-gateway/routers/src/routers/axelar_xcm.rs @@ -0,0 +1,62 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_traits::connectors::Codec; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::dispatch::DispatchResult; +use scale_info::TypeInfo; +use sp_core::H160; +use sp_runtime::DispatchError; +use sp_std::marker::PhantomData; + +use crate::{ + axelar_evm::get_axelar_encoded_msg, AccountIdOf, CurrencyIdOf, EVMChain, MessageOf, XCMRouter, + XcmDomain, +}; + +pub type AxelarXcmDomain = XcmDomain>; + +/// The router used for submitting a Connectors message using Axelar via +/// Moonbeam XCM. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct AxelarXCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, +{ + pub router: XCMRouter, + pub axelar_target_chain: EVMChain, + pub axelar_target_contract: H160, + pub _marker: PhantomData, +} + +impl AxelarXCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, +{ + /// Calls the init function on the EVM router. + pub fn do_init(&self) -> DispatchResult { + self.router.do_init() + } + + /// Encodes the Connectors 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 { + let contract_call = get_axelar_encoded_msg( + msg.serialize(), + self.axelar_target_chain.clone(), + self.axelar_target_contract, + ) + .map_err(|_| DispatchError::Other("encoded contract call retrieval"))?; + + self.router.do_send(sender, contract_call) + } +} diff --git a/pallets/connectors-gateway/routers/src/routers/ethereum_xcm.rs b/pallets/connectors-gateway/routers/src/routers/ethereum_xcm.rs new file mode 100644 index 0000000000..4836cd8bbf --- /dev/null +++ b/pallets/connectors-gateway/routers/src/routers/ethereum_xcm.rs @@ -0,0 +1,95 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +use cfg_traits::connectors::Codec; +use codec::{Decode, Encode, MaxEncodedLen}; +use ethabi::{Bytes, Contract}; +use frame_support::{dispatch::DispatchResult, sp_runtime::DispatchError}; +use scale_info::TypeInfo; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, vec, vec::Vec}; + +use crate::{ + AccountIdOf, MessageOf, XCMRouter, CONNECTORS_FUNCTION_NAME, CONNECTORS_MESSAGE_PARAM, +}; + +/// The router used for submitting a Connectors message via Moonbeam XCM. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct EthereumXCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, +{ + pub router: XCMRouter, + pub _marker: PhantomData, +} + +impl EthereumXCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, +{ + /// Calls the init function on the EVM router. + pub fn do_init(&self) -> DispatchResult { + self.router.do_init() + } + + /// Encodes the Connectors message to the required format and executes the + /// call via the XCM router. + pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResult { + let contract_call = get_encoded_contract_call(msg.serialize()) + .map_err(|_| DispatchError::Other("encoded contract call retrieval"))?; + + self.router.do_send(sender, contract_call) + } +} + +/// Return the encoded contract call, i.e, +/// ConnectorsXcmRouter::handle(encoded_msg). +pub(crate) fn get_encoded_contract_call(encoded_msg: Vec) -> Result { + let contract = get_xcm_router_contract(); + let encoded_contract_call = contract + .function(CONNECTORS_FUNCTION_NAME) + .map_err(|_| ())? + .encode_input(&[ethabi::Token::Bytes(encoded_msg)]) + .map_err(|_| ())?; + + Ok(encoded_contract_call) +} + +/// The ConnectorsXcmRouter Abi as in ethabi::Contract +/// Note: We only concern ourselves with the `handle` function of the +/// contract since that's all we need to build the calls for remote EVM +/// execution. +pub(crate) fn get_xcm_router_contract() -> Contract { + let mut functions = BTreeMap::new(); + #[allow(deprecated)] + functions.insert( + CONNECTORS_FUNCTION_NAME.into(), + vec![ethabi::Function { + name: CONNECTORS_FUNCTION_NAME.into(), + inputs: vec![ethabi::Param { + name: CONNECTORS_MESSAGE_PARAM.into(), + kind: ethabi::ParamType::Bytes, + internal_type: None, + }], + outputs: vec![], + constant: false, + state_mutability: Default::default(), + }], + ); + + Contract { + constructor: None, + functions, + events: Default::default(), + errors: Default::default(), + receive: false, + fallback: false, + } +} diff --git a/pallets/connectors-gateway/routers/src/routers/mod.rs b/pallets/connectors-gateway/routers/src/routers/mod.rs new file mode 100644 index 0000000000..2f932293c9 --- /dev/null +++ b/pallets/connectors-gateway/routers/src/routers/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +pub mod axelar_evm; +pub mod axelar_xcm; +pub mod ethereum_xcm; + +pub use axelar_evm::*; +pub use axelar_xcm::*; +pub use ethereum_xcm::*; diff --git a/pallets/connectors-gateway/routers/src/tests.rs b/pallets/connectors-gateway/routers/src/tests.rs new file mode 100644 index 0000000000..9ae736fec0 --- /dev/null +++ b/pallets/connectors-gateway/routers/src/tests.rs @@ -0,0 +1,1090 @@ +use ::xcm::{ + lts::WeightLimit, + v2::OriginKind, + v3::{Instruction::*, MultiAsset, Weight}, +}; +use cfg_mocks::MessageMock; +use cfg_primitives::CFG; +use cfg_traits::connectors::{Codec, Router}; +use cumulus_primitives_core::MultiLocation; +use frame_support::{assert_noop, assert_ok, traits::fungible::Mutate}; +use pallet_evm::AddressMapping; +use pallet_xcm_transactor::RemoteTransactInfoWithMaxWeight; +use sp_core::{bounded_vec, crypto::AccountId32, H160, H256, U256}; +use sp_runtime::{ + traits::{BlakeTwo256, Convert, Hash}, + DispatchError, +}; + +use super::mock::*; +use crate::*; + +mod evm_router { + use util::*; + + use super::*; + + mod util { + use super::*; + + pub struct EVMRouterTestData { + pub test_contract_address: H160, + pub test_contract_code: Vec, + pub test_contract_hash: H256, + pub evm_domain: EVMDomain, + pub sender: AccountId32, + pub sender_h160: H160, + pub derived_sender: AccountId32, + pub msg: Vec, + } + + pub fn get_test_data() -> EVMRouterTestData { + let test_contract_address = H160::from_low_u64_be(1); + let test_contract_code = [0; 32].to_vec(); + let test_contract_hash = BlakeTwo256::hash_of(&test_contract_code); + + let evm_domain = EVMDomain { + target_contract_address: test_contract_address, + target_contract_hash: test_contract_hash, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(10), + gas_price: U256::from(10), + }, + }; + + let sender: AccountId32 = [0; 32].into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + let derived_sender = IdentityAddressMapping::into_account_id(sender_h160); + + let msg = vec![0, 1, 2]; + + EVMRouterTestData { + test_contract_address, + test_contract_code, + test_contract_hash, + evm_domain, + sender, + sender_h160, + derived_sender, + msg, + } + } + } + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + pallet_evm::AccountCodes::::insert( + test_data.test_contract_address, + test_data.test_contract_code, + ); + + let router = EVMRouter:: { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }; + + assert_ok!(router.do_init()); + }); + } + + #[test] + fn failure() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = EVMRouter:: { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }; + + assert_noop!( + router.do_init(), + DispatchError::Other("Target contract code does not match") + ); + + pallet_evm::AccountCodes::::insert( + test_data.test_contract_address, + [1; 32].to_vec(), + ); + + assert_noop!( + router.do_init(), + DispatchError::Other("Target contract code does not match") + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let mut test_data = get_test_data(); + + Balances::mint_into(&test_data.derived_sender.into(), 1_000_000 * CFG).unwrap(); + + let transaction_call_cost = + ::config().gas_transaction_call; + + test_data.evm_domain.fee_values.gas_limit = + U256::from(transaction_call_cost + 10_000); + + let router = EVMRouter:: { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }; + + assert_ok!(router.do_send(test_data.sender, test_data.msg)); + }); + } + + #[test] + fn insufficient_balance() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = EVMRouter:: { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }; + + let res = router.do_send(test_data.sender, test_data.msg); + + assert_eq!( + res.err().unwrap(), + pallet_evm::Error::::BalanceLow.into() + ); + }); + } + } +} + +mod xcm_router { + use util::*; + + use super::*; + + mod util { + use super::*; + + pub struct XCMRouterTestData { + pub currency_id: CurrencyId, + pub dest: MultiLocation, + pub xcm_domain: XcmDomain<::CurrencyId>, + pub sender: AccountId32, + pub msg: Vec, + } + + pub fn get_test_data() -> XCMRouterTestData { + let currency_id = CurrencyId::OtherReserve(1); + let dest = CurrencyIdToMultiLocation::convert(currency_id.clone()).unwrap(); + + let xcm_domain = XcmDomain { + location: Box::new(dest.clone().into_versioned()), + ethereum_xcm_transact_call_index: bounded_vec![0], + contract_address: H160::from_slice([0; 20].as_slice()), + max_gas_limit: 10, + transact_info: XcmTransactInfo { + transact_extra_weight: 1.into(), + max_weight: 100_000_000_000.into(), + transact_extra_weight_signed: None, + }, + fee_currency: currency_id.clone(), + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let sender: AccountId32 = [0; 32].into(); + + let msg = vec![0, 1, 2]; + + XCMRouterTestData { + currency_id, + dest, + xcm_domain, + sender, + msg, + } + } + } + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = XCMRouter:: { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }; + + assert_ok!(router.do_init()); + + let res = pallet_xcm_transactor::TransactInfoWithWeightLimit::::get( + test_data.dest.clone(), + ) + .unwrap(); + + assert_eq!( + res.transact_extra_weight, + test_data.xcm_domain.transact_info.transact_extra_weight + ); + assert_eq!( + res.max_weight, + test_data.xcm_domain.transact_info.max_weight + ); + assert_eq!( + res.transact_extra_weight_signed, + test_data + .xcm_domain + .transact_info + .transact_extra_weight_signed + ); + + assert_eq!( + pallet_xcm_transactor::DestinationAssetFeePerSecond::::get( + test_data.dest + ), + Some(test_data.xcm_domain.fee_per_second), + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = XCMRouter:: { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }; + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + test_data.dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: test_data + .xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: test_data.xcm_domain.transact_info.max_weight.clone(), + transact_extra_weight_signed: None, + }, + ); + + // Manually insert the fee per second in the `DestinationAssetFeePerSecond` + // storage. + + pallet_xcm_transactor::DestinationAssetFeePerSecond::::insert( + test_data.dest, + test_data.xcm_domain.fee_per_second.clone(), + ); + + assert_ok!(router.do_send(test_data.sender, test_data.msg.clone())); + + let sent_messages = sent_xcm(); + assert_eq!(sent_messages.len(), 1); + + let weight_limit = test_data.xcm_domain.max_gas_limit * 25_000 + 100_000_000; + + let (_, xcm) = sent_messages.first().unwrap(); + assert!(xcm.0.contains(&WithdrawAsset( + (MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }) + .into() + ))); + + assert!(xcm.0.contains(&BuyExecution { + fees: MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }, + weight_limit: WeightLimit::Limited(Weight::from_all( + weight_limit + + test_data + .xcm_domain + .transact_info + .transact_extra_weight + .ref_time() + )), + })); + + let expected_call = get_encoded_ethereum_xcm_call::( + test_data.xcm_domain.clone(), + test_data.msg, + ) + .unwrap(); + + assert!(xcm.0.contains(&Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(weight_limit, weight_limit), + call: expected_call.into(), + })); + }); + } + + #[test] + fn success_with_init() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = XCMRouter:: { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }; + + assert_ok!(router.do_init()); + + assert_ok!(router.do_send(test_data.sender, test_data.msg)); + }); + } + + #[test] + fn transactor_info_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = XCMRouter:: { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }; + + // Manually insert the fee per second in the `DestinationAssetFeePerSecond` + // storage. + + pallet_xcm_transactor::DestinationAssetFeePerSecond::::insert( + test_data.dest, + test_data.xcm_domain.fee_per_second.clone(), + ); + + assert_noop!( + router.do_send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::TransactorInfoNotSet, + ); + }); + } + + #[test] + fn fee_per_second_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let router = XCMRouter:: { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }; + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + test_data.dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: test_data + .xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: test_data.xcm_domain.transact_info.max_weight.clone(), + transact_extra_weight_signed: None, + }, + ); + + assert_noop!( + router.do_send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::FeePerSecondNotSet, + ); + }); + } + } +} + +mod axelar_evm { + use util::*; + + use super::*; + + mod util { + use super::*; + + pub struct AxelarEVMTestData { + pub axelar_contract_address: H160, + pub axelar_contract_code: Vec, + pub axelar_contract_hash: H256, + pub connectors_contract_address: H160, + pub evm_domain: EVMDomain, + pub sender: AccountId32, + pub sender_h160: H160, + pub derived_sender: AccountId32, + pub msg: MessageMock, + } + + pub fn get_test_data() -> AxelarEVMTestData { + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code = [0; 32].to_vec(); + let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); + let connectors_contract_address = H160::from_low_u64_be(2); + + 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(10), + gas_price: U256::from(10), + }, + }; + + let sender: AccountId32 = [0; 32].into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + let derived_sender = IdentityAddressMapping::into_account_id(sender_h160); + + let msg = MessageMock::Second; + + AxelarEVMTestData { + axelar_contract_address, + axelar_contract_code, + axelar_contract_hash, + connectors_contract_address, + evm_domain, + sender, + sender_h160, + derived_sender, + msg, + } + } + } + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + pallet_evm::AccountCodes::::insert( + test_data.axelar_contract_address, + test_data.axelar_contract_code, + ); + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + router: EVMRouter { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }, + evm_chain: EVMChain::Ethereum, + connectors_contract_address: test_data.connectors_contract_address, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + }); + } + + #[test] + fn failure() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + router: EVMRouter { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }, + evm_chain: EVMChain::Ethereum, + connectors_contract_address: test_data.connectors_contract_address, + _marker: Default::default(), + }); + + assert_noop!( + domain_router.init(), + DispatchError::Other("Target contract code does not match") + ); + + pallet_evm::AccountCodes::::insert( + test_data.axelar_contract_address, + [1; 32].to_vec(), + ); + + assert_noop!( + domain_router.init(), + DispatchError::Other("Target contract code does not match") + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let mut test_data = get_test_data(); + + Balances::mint_into(&test_data.derived_sender.into(), 1_000_000 * CFG).unwrap(); + + let transaction_call_cost = + ::config().gas_transaction_call; + + test_data.evm_domain.fee_values.gas_limit = + U256::from(transaction_call_cost + 10_000); + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + router: EVMRouter { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }, + evm_chain: EVMChain::Ethereum, + connectors_contract_address: test_data.connectors_contract_address, + _marker: Default::default(), + }); + + assert_ok!(domain_router.send(test_data.sender, test_data.msg)); + }); + } + + #[test] + fn insufficient_balance() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + router: EVMRouter { + evm_domain: test_data.evm_domain, + _marker: Default::default(), + }, + evm_chain: EVMChain::Ethereum, + connectors_contract_address: test_data.connectors_contract_address, + _marker: Default::default(), + }); + + let res = domain_router.send(test_data.sender, test_data.msg); + + assert_eq!( + res.err().unwrap(), + pallet_evm::Error::::BalanceLow.into() + ); + }); + } + } +} + +mod axelar_xcm { + use util::*; + + use super::*; + + mod util { + use super::*; + + pub struct AxelarXCMTestData { + pub currency_id: CurrencyId, + pub dest: MultiLocation, + pub xcm_domain: XcmDomain<::CurrencyId>, + pub axelar_target_chain: EVMChain, + pub axelar_target_contract: H160, + pub sender: AccountId32, + pub msg: MessageMock, + } + + pub fn get_test_data() -> AxelarXCMTestData { + let currency_id = CurrencyId::OtherReserve(1); + let dest = CurrencyIdToMultiLocation::convert(currency_id.clone()).unwrap(); + + let xcm_domain = XcmDomain { + location: Box::new(dest.clone().into_versioned()), + ethereum_xcm_transact_call_index: bounded_vec![0], + contract_address: H160::from_slice([0; 20].as_slice()), + max_gas_limit: 10, + transact_info: XcmTransactInfo { + transact_extra_weight: 1.into(), + max_weight: 100_000_000_000.into(), + transact_extra_weight_signed: None, + }, + fee_currency: currency_id.clone(), + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + let axelar_target_chain = EVMChain::Ethereum; + let axelar_target_contract = H160::from_low_u64_be(1); + + let sender: AccountId32 = [0; 32].into(); + + let msg = MessageMock::First; + + AxelarXCMTestData { + currency_id, + dest, + xcm_domain, + axelar_target_chain, + axelar_target_contract, + sender, + msg, + } + } + } + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarXCM(AxelarXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + axelar_target_chain: test_data.axelar_target_chain, + axelar_target_contract: test_data.axelar_target_contract, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + let res = pallet_xcm_transactor::TransactInfoWithWeightLimit::::get( + test_data.dest.clone(), + ) + .unwrap(); + + assert_eq!( + res.transact_extra_weight, + test_data.xcm_domain.transact_info.transact_extra_weight + ); + assert_eq!( + res.max_weight, + test_data.xcm_domain.transact_info.max_weight + ); + assert_eq!( + res.transact_extra_weight_signed, + test_data + .xcm_domain + .transact_info + .transact_extra_weight_signed + ); + + assert_eq!( + pallet_xcm_transactor::DestinationAssetFeePerSecond::::get( + test_data.dest + ), + Some(test_data.xcm_domain.fee_per_second), + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarXCM(AxelarXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + axelar_target_chain: test_data.axelar_target_chain.clone(), + axelar_target_contract: test_data.axelar_target_contract, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + assert_ok!(domain_router.send(test_data.sender, test_data.msg.clone())); + + let sent_messages = sent_xcm(); + assert_eq!(sent_messages.len(), 1); + + let weight_limit = test_data.xcm_domain.max_gas_limit * 25_000 + 100_000_000; + + let (_, xcm) = sent_messages.first().unwrap(); + assert!(xcm.0.contains(&WithdrawAsset( + (MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }) + .into() + ))); + + assert!(xcm.0.contains(&BuyExecution { + fees: MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }, + weight_limit: WeightLimit::Limited(Weight::from_all( + weight_limit + + test_data + .xcm_domain + .transact_info + .transact_extra_weight + .ref_time() + )), + })); + + let contract_call = get_axelar_encoded_msg( + test_data.msg.serialize(), + test_data.axelar_target_chain.clone(), + test_data.axelar_target_contract, + ) + .unwrap(); + + let expected_call = get_encoded_ethereum_xcm_call::( + test_data.xcm_domain.clone(), + contract_call, + ) + .unwrap(); + + assert!(xcm.0.contains(&Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(weight_limit, weight_limit), + call: expected_call.into(), + })); + }); + } + + #[test] + fn transactor_info_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarXCM(AxelarXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + axelar_target_chain: test_data.axelar_target_chain, + axelar_target_contract: test_data.axelar_target_contract, + _marker: Default::default(), + }); + + // Manually insert the fee per second in the `DestinationAssetFeePerSecond` + // storage. + + pallet_xcm_transactor::DestinationAssetFeePerSecond::::insert( + test_data.dest, + test_data.xcm_domain.fee_per_second.clone(), + ); + + assert_noop!( + domain_router.send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::TransactorInfoNotSet, + ); + }); + } + + #[test] + fn fee_per_second_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::AxelarXCM(AxelarXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + axelar_target_chain: test_data.axelar_target_chain, + axelar_target_contract: test_data.axelar_target_contract, + _marker: Default::default(), + }); + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + test_data.dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: test_data + .xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: test_data.xcm_domain.transact_info.max_weight.clone(), + transact_extra_weight_signed: None, + }, + ); + + assert_noop!( + domain_router.send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::FeePerSecondNotSet, + ); + }); + } + } +} + +mod ethereum_xcm { + use util::*; + + use super::*; + + mod util { + use super::*; + + pub struct EthereumXCMTestData { + pub currency_id: CurrencyId, + pub dest: MultiLocation, + pub xcm_domain: XcmDomain<::CurrencyId>, + pub axelar_target_chain: EVMChain, + pub axelar_target_contract: H160, + pub sender: AccountId32, + pub msg: MessageMock, + } + + pub fn get_test_data() -> EthereumXCMTestData { + let currency_id = CurrencyId::OtherReserve(1); + let dest = CurrencyIdToMultiLocation::convert(currency_id.clone()).unwrap(); + + let xcm_domain = XcmDomain { + location: Box::new(dest.clone().into_versioned()), + ethereum_xcm_transact_call_index: bounded_vec![0], + contract_address: H160::from_slice([0; 20].as_slice()), + max_gas_limit: 10, + transact_info: XcmTransactInfo { + transact_extra_weight: 1.into(), + max_weight: 100_000_000_000.into(), + transact_extra_weight_signed: None, + }, + fee_currency: currency_id.clone(), + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + let axelar_target_chain = EVMChain::Ethereum; + let axelar_target_contract = H160::from_low_u64_be(1); + + let sender: AccountId32 = [0; 32].into(); + + let msg = MessageMock::First; + + EthereumXCMTestData { + currency_id, + dest, + xcm_domain, + axelar_target_chain, + axelar_target_contract, + sender, + msg, + } + } + } + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + let res = pallet_xcm_transactor::TransactInfoWithWeightLimit::::get( + test_data.dest.clone(), + ) + .unwrap(); + + assert_eq!( + res.transact_extra_weight, + test_data.xcm_domain.transact_info.transact_extra_weight + ); + assert_eq!( + res.max_weight, + test_data.xcm_domain.transact_info.max_weight + ); + assert_eq!( + res.transact_extra_weight_signed, + test_data + .xcm_domain + .transact_info + .transact_extra_weight_signed + ); + + assert_eq!( + pallet_xcm_transactor::DestinationAssetFeePerSecond::::get( + test_data.dest + ), + Some(test_data.xcm_domain.fee_per_second), + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success_with_init() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + assert_ok!(domain_router.send(test_data.sender, test_data.msg.clone())); + + let sent_messages = sent_xcm(); + assert_eq!(sent_messages.len(), 1); + + let weight_limit = test_data.xcm_domain.max_gas_limit * 25_000 + 100_000_000; + + let (_, xcm) = sent_messages.first().unwrap(); + assert!(xcm.0.contains(&WithdrawAsset( + (MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }) + .into() + ))); + + assert!(xcm.0.contains(&BuyExecution { + fees: MultiAsset { + id: ::xcm::v3::AssetId::Concrete(MultiLocation::here()), + fun: ::xcm::v3::Fungibility::Fungible(1), + }, + weight_limit: WeightLimit::Limited(Weight::from_all( + weight_limit + + test_data + .xcm_domain + .transact_info + .transact_extra_weight + .ref_time() + )), + })); + + let contract_call = get_encoded_contract_call(test_data.msg.serialize()).unwrap(); + let expected_call = get_encoded_ethereum_xcm_call::( + test_data.xcm_domain.clone(), + contract_call, + ) + .unwrap(); + + assert!(xcm.0.contains(&Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(weight_limit, weight_limit), + call: expected_call.into(), + })); + }); + } + + #[test] + fn transactor_info_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + _marker: Default::default(), + }); + + // Manually insert the fee per second in the `DestinationAssetFeePerSecond` + // storage. + + pallet_xcm_transactor::DestinationAssetFeePerSecond::::insert( + test_data.dest, + test_data.xcm_domain.fee_per_second.clone(), + ); + + assert_noop!( + domain_router.send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::TransactorInfoNotSet, + ); + }); + } + + #[test] + fn fee_per_second_not_set() { + new_test_ext().execute_with(|| { + let test_data = get_test_data(); + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + router: XCMRouter { + xcm_domain: test_data.xcm_domain.clone(), + _marker: Default::default(), + }, + _marker: Default::default(), + }); + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + test_data.dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: test_data + .xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: test_data.xcm_domain.transact_info.max_weight.clone(), + transact_extra_weight_signed: None, + }, + ); + + assert_noop!( + domain_router.send(test_data.sender, test_data.msg), + pallet_xcm_transactor::Error::::FeePerSecondNotSet, + ); + }); + } + } +} diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 749516013e..a1eb702001 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -21,7 +21,6 @@ mod tests; pub mod account_conversion; pub mod apis; pub mod evm; -pub mod routers; #[macro_export] macro_rules! production_or_benchmark { diff --git a/runtime/common/src/routers.rs b/runtime/common/src/routers.rs deleted file mode 100644 index b73cc855ff..0000000000 --- a/runtime/common/src/routers.rs +++ /dev/null @@ -1,25 +0,0 @@ -use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; -use cfg_traits::connectors::Router; -use cfg_types::{domain_address::Domain, fixed_point::Rate}; -use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::dispatch::DispatchResult; -use pallet_connectors::Message; -use scale_info::TypeInfo; - -type ConnectorsMessage = Message; - -// TODO(cdamian): This will be removed when the gateway routers are added. -#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] -pub struct DummyRouter(); -impl Router for DummyRouter { - type Message = ConnectorsMessage; - type Sender = AccountId; - - fn init(&self) -> DispatchResult { - Ok(()) - } - - fn send(&self, _sender: Self::Sender, _message: Self::Message) -> DispatchResult { - Ok(()) - } -} diff --git a/runtime/development/Cargo.toml b/runtime/development/Cargo.toml index 1fc5034d08..2af46c3a82 100644 --- a/runtime/development/Cargo.toml +++ b/runtime/development/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/centrifuge/centrifuge-chain" [dependencies] # third-party dependencies codec = { package = "parity-scale-codec", version = "3.0", default-features = false, features = ["derive"] } +getrandom = { version = "0.2", features = ["js"] } hex = { version = "0.4.3", default_features = false } hex-literal = { version = "0.3.4", optional = true } scale-info = { version = "2.3.0", default-features = false, features = ["derive"] } @@ -112,6 +113,7 @@ runtime-common = { path = "../common", default-features = false } chainbridge = { git = "https://github.com/centrifuge/chainbridge-substrate.git", default-features = false, branch = "polkadot-v0.9.38" } # our custom pallets +connectors-gateway-routers = { path = "../../pallets/connectors-gateway/routers", default-features = false } pallet-anchors = { path = "../../pallets/anchors", default-features = false } pallet-block-rewards = { path = "../../pallets/block-rewards", default-features = false } pallet-bridge = { path = "../../pallets/bridge", default-features = false } @@ -157,6 +159,7 @@ std = [ "chainbridge/std", "chainbridge/std", "codec/std", + "connectors-gateway-routers/std", "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", "cumulus-pallet-xcm/std", @@ -266,11 +269,13 @@ std = [ "fp-rpc/std", "fp-self-contained/std", "moonbeam-relay-encoder/std", + "getrandom/std", ] runtime-benchmarks = [ "cfg-types/runtime-benchmarks", "chainbridge/runtime-benchmarks", + "connectors-gateway-routers/runtime-benchmarks", "cumulus-pallet-session-benchmarking/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", @@ -338,6 +343,7 @@ runtime-benchmarks = [ ] try-runtime = [ + "connectors-gateway-routers/try-runtime", "pallet-connectors/try-runtime", "pallet-connectors-gateway/try-runtime", "cfg-primitives/try-runtime", diff --git a/runtime/development/src/connectors.rs b/runtime/development/src/connectors.rs index 70eecc69ba..41d06b172b 100644 --- a/runtime/development/src/connectors.rs +++ b/runtime/development/src/connectors.rs @@ -14,7 +14,6 @@ use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; use cfg_types::{domain_address::Domain, fixed_point::Rate}; use frame_support::parameter_types; use frame_system::EnsureRoot; -use runtime_common::routers::DummyRouter; use super::{Runtime, RuntimeEvent, RuntimeOrigin}; use crate::Connectors; @@ -31,7 +30,7 @@ impl pallet_connectors_gateway::Config for Runtime { type LocalEVMOrigin = pallet_connectors_gateway::EnsureLocal; type MaxIncomingMessageSize = MaxIncomingMessageSize; type Message = ConnectorsMessage; - type Router = DummyRouter; + type Router = connectors_gateway_routers::DomainRouter; type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; type WeightInfo = ();