diff --git a/Cargo.lock b/Cargo.lock index 581b6b3526..1e4863a6a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1224,12 +1224,14 @@ dependencies = [ "frame-system", "mock-builder", "orml-traits", + "pallet-xcm-transactor", "parity-scale-codec 3.4.0", "scale-info", "sp-core", "sp-io", "sp-runtime", "sp-std", + "xcm", ] [[package]] @@ -1544,6 +1546,61 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "connectors-gateway-axelar-precompile" +version = "0.1.0" +dependencies = [ + "cfg-types", + "ethabi 18.0.0", + "fp-evm", + "frame-support", + "frame-system", + "pallet-connectors-gateway", + "pallet-evm", + "parity-scale-codec 3.4.0", + "precompile-utils", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "connectors-gateway-routers" +version = "0.0.1" +dependencies = [ + "cfg-mocks", + "cfg-primitives", + "cfg-traits", + "cfg-types", + "cumulus-primitives-core", + "ethabi 16.0.0", + "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", + "rand 0.8.5", + "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 +2662,7 @@ dependencies = [ "cfg-traits", "cfg-types", "chainbridge", + "connectors-gateway-routers", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", @@ -2623,6 +2681,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", @@ -2645,12 +2704,14 @@ dependencies = [ "pallet-collator-selection", "pallet-collective", "pallet-connectors", + "pallet-connectors-gateway", "pallet-crowdloan-claim", "pallet-crowdloan-reward", "pallet-data-collector", "pallet-democracy", "pallet-elections-phragmen", "pallet-ethereum", + "pallet-ethereum-transaction", "pallet-evm", "pallet-evm-chain-id", "pallet-evm-precompile-dispatch", @@ -3043,6 +3104,20 @@ dependencies = [ "uint", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types 0.14.1", + "hex", + "serde", + "sha3 0.10.6", + "thiserror", + "uint", +] + [[package]] name = "ethbloom" version = "0.11.1" @@ -7009,7 +7084,7 @@ dependencies = [ "cfg-traits", "cfg-types", "cfg-utils", - "ethabi", + "ethabi 16.0.0", "fp-self-contained", "frame-benchmarking", "frame-support", @@ -7032,6 +7107,26 @@ dependencies = [ "xcm-primitives", ] +[[package]] +name = "pallet-connectors-gateway" +version = "0.0.1" +dependencies = [ + "cfg-mocks", + "cfg-traits", + "cfg-types", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec 3.4.0", + "rand 0.8.5", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-conviction-voting" version = "4.0.0-dev" @@ -7212,6 +7307,32 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-ethereum-transaction" +version = "0.0.1" +dependencies = [ + "cfg-traits", + "ethabi 16.0.0", + "ethereum", + "fp-ethereum", + "fp-evm", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-ethereum", + "pallet-evm", + "pallet-evm-precompile-simple", + "pallet-timestamp", + "parity-scale-codec 3.4.0", + "rand 0.8.5", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm" version = "6.0.0-dev" @@ -7279,6 +7400,16 @@ dependencies = [ "xcm-primitives", ] +[[package]] +name = "pallet-evm-precompile-simple" +version = "2.0.0-dev" +source = "git+https://github.com/PureStake/frontier?branch=moonbeam-polkadot-v0.9.38#df4e329ef9b1ef54d83114deff98124139f1dd6d" +dependencies = [ + "fp-evm", + "ripemd", + "sp-io", +] + [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" @@ -10486,6 +10617,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "rlp" version = "0.5.2" @@ -10728,12 +10868,15 @@ dependencies = [ "cfg-traits", "cfg-types", "cfg-utils", + "connectors-gateway-routers", "cumulus-primitives-core", "development-runtime", + "ethereum", "frame-benchmarking", "frame-support", "frame-system", "fudge", + "getrandom 0.2.8", "hex", "kusama-runtime", "lazy_static", @@ -10746,17 +10889,25 @@ dependencies = [ "pallet-balances", "pallet-block-rewards", "pallet-collator-selection", + "pallet-collective", "pallet-connectors", + "pallet-connectors-gateway", + "pallet-democracy", + "pallet-ethereum-transaction", + "pallet-evm", + "pallet-evm-chain-id", "pallet-investments", "pallet-loans", "pallet-permissions", "pallet-pool-registry", "pallet-pool-system", + "pallet-preimage", "pallet-rewards", "pallet-session", "pallet-transaction-payment", "pallet-uniques", "pallet-xcm", + "pallet-xcm-transactor", "parachain-info", "parity-scale-codec 3.4.0", "polkadot-core-primitives", @@ -10765,6 +10916,7 @@ dependencies = [ "polkadot-runtime", "polkadot-runtime-common", "polkadot-runtime-parachains", + "rand 0.8.5", "rococo-runtime", "runtime-common", "sc-client-api", @@ -14385,7 +14537,7 @@ checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" dependencies = [ "bitflags", "chrono", - "rustc_version 0.2.3", + "rustc_version 0.4.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f78a63d54d..b18c5636ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,14 @@ members = [ "pallets/bridge", "pallets/block-rewards", "pallets/connectors", + "pallets/connectors-gateway", + "pallets/connectors-gateway/connectors-gateway-routers", + "pallets/connectors-gateway/connectors-gateway-axelar-precompile", "pallets/claims", "pallets/collator-allowlist", "pallets/crowdloan-claim", "pallets/crowdloan-reward", + "pallets/ethereum-transaction", "pallets/fees", "pallets/interest-accrual", "pallets/investments", diff --git a/libs/mocks/Cargo.toml b/libs/mocks/Cargo.toml index 4791a5e95a..69309dc068 100644 --- a/libs/mocks/Cargo.toml +++ b/libs/mocks/Cargo.toml @@ -20,6 +20,9 @@ sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +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" } + cfg-primitives = { path = "../primitives", default-features = false } cfg-traits = { path = "../traits", default-features = false } cfg-types = { path = "../types", default-features = false } @@ -42,6 +45,8 @@ std = [ "sp-io/std", "sp-runtime/std", "orml-traits/std", + "pallet-xcm-transactor/std", + "xcm/std", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", @@ -50,6 +55,7 @@ runtime-benchmarks = [ "cfg-traits/runtime-benchmarks", "cfg-types/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-xcm-transactor/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", @@ -58,4 +64,5 @@ try-runtime = [ "cfg-primitives/try-runtime", "cfg-traits/try-runtime", "sp-runtime/try-runtime", + "pallet-xcm-transactor/try-runtime", ] diff --git a/libs/mocks/src/connectors.rs b/libs/mocks/src/connectors.rs new file mode 100644 index 0000000000..077f228e52 --- /dev/null +++ b/libs/mocks/src/connectors.rs @@ -0,0 +1,76 @@ +use cfg_traits::connectors::Codec; +use codec::{Error, Input}; + +#[derive(Debug, Eq, PartialEq)] +pub enum MessageMock { + First, + Second, +} + +impl MessageMock { + fn call_type(&self) -> u8 { + match self { + MessageMock::First => 0, + MessageMock::Second => 1, + } + } +} + +impl Codec for MessageMock { + fn serialize(&self) -> Vec { + vec![self.call_type()] + } + + fn deserialize(input: &mut I) -> Result { + let call_type = input.read_byte()?; + + match call_type { + 0 => Ok(MessageMock::First), + 1 => Ok(MessageMock::Second), + _ => Err("unsupported message".into()), + } + } +} + +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::connectors::InboundQueue; + use cfg_types::domain_address::DomainAddress; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call, register_call}; + + use crate::connectors::MessageMock; + + #[pallet::config] + pub trait Config: frame_system::Config { + type DomainAddress; + type Message; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl Pallet { + pub fn mock_submit(f: impl Fn(DomainAddress, MessageMock) -> DispatchResult + 'static) { + register_call!(move |(sender, msg)| f(sender, msg)); + } + } + + impl InboundQueue for Pallet { + type Message = T::Message; + type Sender = T::DomainAddress; + + fn submit(sender: Self::Sender, msg: Self::Message) -> DispatchResult { + execute_call!((sender, msg)) + } + } +} diff --git a/libs/mocks/src/connectors_gateway_routers.rs b/libs/mocks/src/connectors_gateway_routers.rs new file mode 100644 index 0000000000..5acc25212f --- /dev/null +++ b/libs/mocks/src/connectors_gateway_routers.rs @@ -0,0 +1,33 @@ +use cfg_traits::connectors::Router; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::dispatch::DispatchResult; +use scale_info::TypeInfo; +use sp_std::{default::Default, marker::PhantomData}; + +use crate::MessageMock; + +#[derive(Default, Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct DomainRouterMock { + _marker: PhantomData, +} + +impl DomainRouterMock { + pub fn new() -> Self { + Self { + _marker: PhantomData::default(), + } + } +} + +impl Router for DomainRouterMock { + type Message = MessageMock; + type Sender = T::AccountId; + + fn init(&self) -> DispatchResult { + Ok(()) + } + + fn send(&self, _sender: Self::Sender, _message: Self::Message) -> DispatchResult { + Ok(()) + } +} diff --git a/libs/mocks/src/ethereum_transaction.rs b/libs/mocks/src/ethereum_transaction.rs new file mode 100644 index 0000000000..50c6c8b517 --- /dev/null +++ b/libs/mocks/src/ethereum_transaction.rs @@ -0,0 +1,45 @@ +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::ethereum::EthereumTransactor; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call, register_call}; + use sp_core::{H160, U256}; + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl Pallet { + pub fn mock_call( + f: impl Fn(H160, H160, &[u8], U256, U256, U256) -> DispatchResult + 'static, + ) { + register_call!(move |(from, to, data, value, gas_price, gas_limit)| f( + from, to, data, value, gas_price, gas_limit + )); + } + } + + impl EthereumTransactor for Pallet { + fn call( + from: H160, + to: H160, + data: &[u8], + value: U256, + gas_price: U256, + gas_limit: U256, + ) -> DispatchResultWithPostInfo { + execute_call!((from, to, data, value, gas_price, gas_limit)) + } + } +} diff --git a/libs/mocks/src/lib.rs b/libs/mocks/src/lib.rs index f4be5ffab6..5ac452cfd4 100644 --- a/libs/mocks/src/lib.rs +++ b/libs/mocks/src/lib.rs @@ -1,5 +1,8 @@ mod change_guard; +mod connectors; +mod connectors_gateway_routers; mod data; +mod ethereum_transaction; mod fees; mod permissions; mod pools; @@ -7,7 +10,10 @@ mod rewards; mod time; pub use change_guard::pallet_mock_change_guard; +pub use connectors::{pallet as pallet_mock_connectors, MessageMock}; +pub use connectors_gateway_routers::*; pub use data::pallet as pallet_mock_data; +pub use ethereum_transaction::pallet as pallet_mock_ethereum_transaction; pub use fees::pallet as pallet_mock_fees; pub use permissions::pallet as pallet_mock_permissions; pub use pools::pallet as pallet_mock_pools; diff --git a/libs/traits/src/connectors.rs b/libs/traits/src/connectors.rs index 8def95e930..02d7da453b 100644 --- a/libs/traits/src/connectors.rs +++ b/libs/traits/src/connectors.rs @@ -1,11 +1,10 @@ -// Copyright 2021 Centrifuge GmbH (centrifuge.io). -// This file is part of Centrifuge chain project. - +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. // Centrifuge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version (see http://www.gnu.org/licenses). - // Centrifuge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the @@ -22,21 +21,36 @@ pub trait Codec: Sized { fn deserialize(input: &mut I) -> Result; } +/// The trait required for sending outbound messages. +pub trait Router { + /// The sender type of the outbound message. + type Sender; + + /// The outbound message type. + type Message; + + /// Initialize the router. + fn init(&self) -> DispatchResult; + + /// Send the message to the router's destination. + fn send(&self, sender: Self::Sender, message: Self::Message) -> DispatchResult; +} + /// The trait required for processing outbound connectors messages. pub trait OutboundQueue { /// The sender type of the outgoing message. type Sender; - /// The destination this message should go to. - type Destination; - /// The message type that is processed. type Message; + /// The destination this message should go to. + type Destination; + /// Submit a message to the outbound queue. fn submit( - sender: Self::Sender, destination: Self::Destination, + sender: Self::Sender, msg: Self::Message, ) -> DispatchResult; } diff --git a/libs/traits/src/ethereum.rs b/libs/traits/src/ethereum.rs new file mode 100644 index 0000000000..9921397107 --- /dev/null +++ b/libs/traits/src/ethereum.rs @@ -0,0 +1,16 @@ +use frame_support::dispatch::DispatchResultWithPostInfo; +use sp_runtime::app_crypto::sp_core::{H160, U256}; + +/// Something capable of managing transactions in an EVM/Ethereum context +pub trait EthereumTransactor { + /// Transacts the specified call in the EVM context, + /// exposing the call and any events to the EVM block. + fn call( + from: H160, + to: H160, + data: &[u8], + value: U256, + gas_price: U256, + gas_limit: U256, + ) -> DispatchResultWithPostInfo; +} diff --git a/libs/traits/src/lib.rs b/libs/traits/src/lib.rs index d1fe42e651..44f99f7acf 100644 --- a/libs/traits/src/lib.rs +++ b/libs/traits/src/lib.rs @@ -45,6 +45,9 @@ pub mod interest; /// Traits related to rewards. pub mod rewards; +/// Traits related to Ethereum. +pub mod ethereum; + /// A trait used for loosely coupling the claim pallet with a reward mechanism. /// /// ## Overview diff --git a/pallets/connectors-gateway/Cargo.toml b/pallets/connectors-gateway/Cargo.toml new file mode 100644 index 0000000000..cc7599fb97 --- /dev/null +++ b/pallets/connectors-gateway/Cargo.toml @@ -0,0 +1,66 @@ +[package] +authors = ["Centrifuge "] +description = "Centrifuge Connectors Gateway Pallet" +edition = "2021" +license = "LGPL-3.0" +name = "pallet-connectors-gateway" +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-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# Benchmarking +frame-benchmarking = { git = "https://github.com/paritytech/substrate", default-features = false, optional = true, branch = "polkadot-v0.9.38" } + +# Substrate crates +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# Our custom pallets +cfg-traits = { path = "../../libs/traits", default-features = false } +cfg-types = { path = "../../libs/types", default-features = false } + +[dev-dependencies] +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" } + +cfg-mocks = { path = "../../libs/mocks" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +rand = "0.8.5" + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "cfg-traits/runtime-benchmarks", + "cfg-types/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "cfg-types/std", + "cfg-traits/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "scale-info/std", +] +try-runtime = [ + "cfg-traits/try-runtime", + "cfg-types/try-runtime", + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/Cargo.toml b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/Cargo.toml new file mode 100644 index 0000000000..b6f61d2b12 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "connectors-gateway-axelar-precompile" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[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-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +sp-io = { 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" } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +ethabi = { version = "18.0.0", default-features = false } +fp-evm = { 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" } +precompile-utils = { git = "https://github.com/PureStake/moonbeam", default-features = false, rev = "00b3e3d97806e889b02e1bcb4b69e65433dd805d" } + +cfg-types = { path = "../../../libs/types" } +pallet-connectors-gateway = { path = "../../connectors-gateway" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "sp-std/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "scale-info/std", + "fp-evm/std", + "precompile-utils/std", + "pallet-evm/std", + "pallet-connectors-gateway/std", + "cfg-types/std", + "ethabi/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol new file mode 100644 index 0000000000..ecaaafef86 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IAxelarGateway } from './IAxelarGateway.sol'; +import { IERC20 } from './IERC20.sol'; + +abstract contract IAxelarForecallable { + error NotApprovedByGateway(); + error AlreadyForecalled(); + error TransferFailed(); + + IAxelarGateway public gateway; + mapping(bytes32 => address) forecallers; + + constructor(address gatewayAddress) { + gateway = IAxelarGateway(gatewayAddress); + } + + function forecall( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload, + address forecaller + ) external { + _checkForecall(sourceChain, sourceAddress, payload, forecaller); + if (forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] != address(0)) revert AlreadyForecalled(); + forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] = forecaller; + _execute(sourceChain, sourceAddress, payload); + } + + function execute( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + bytes32 payloadHash = keccak256(payload); + if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash)) revert NotApprovedByGateway(); + address forecaller = forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))]; + if (forecaller != address(0)) { + forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] = address(0); + } else { + _execute(sourceChain, sourceAddress, payload); + } + } + + function forecallWithToken( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload, + string calldata tokenSymbol, + uint256 amount, + address forecaller + ) external { + address token = gateway.tokenAddresses(tokenSymbol); + uint256 amountPost = amountPostFee(amount, payload); + _safeTransferFrom(token, msg.sender, amountPost); + _checkForecallWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount, forecaller); + if (forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] != address(0)) + revert AlreadyForecalled(); + forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] = forecaller; + _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amountPost); + } + + function executeWithToken( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload, + string calldata tokenSymbol, + uint256 amount + ) external { + bytes32 payloadHash = keccak256(payload); + if (!gateway.validateContractCallAndMint(commandId, sourceChain, sourceAddress, payloadHash, tokenSymbol, amount)) + revert NotApprovedByGateway(); + address forecaller = forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))]; + if (forecaller != address(0)) { + forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] = address(0); + address token = gateway.tokenAddresses(tokenSymbol); + _safeTransfer(token, forecaller, amount); + } else { + _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount); + } + } + + function _execute( + string memory sourceChain, + string memory sourceAddress, + bytes calldata payload + ) internal virtual {} + + function _executeWithToken( + string memory sourceChain, + string memory sourceAddress, + bytes calldata payload, + string memory tokenSymbol, + uint256 amount + ) internal virtual {} + + // Override this to keep a fee. + function amountPostFee( + uint256 amount, + bytes calldata /*payload*/ + ) public virtual returns (uint256) { + return amount; + } + + // Override this and revert if you want to only allow certain people/calls to be able to forecall. + function _checkForecall( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload, + address forecaller + ) internal virtual {} + + // Override this and revert if you want to only allow certain people/calls to be able to forecall. + function _checkForecallWithToken( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload, + string calldata tokenSymbol, + uint256 amount, + address forecaller + ) internal virtual {} + + function _safeTransfer( + address tokenAddress, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); + bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); + + if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); + } + + function _safeTransferFrom( + address tokenAddress, + address from, + uint256 amount + ) internal { + (bool success, bytes memory returnData) = tokenAddress.call( + abi.encodeWithSelector(IERC20.transferFrom.selector, from, address(this), amount) + ); + bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); + + if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); + } +} \ No newline at end of file diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarGateway.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarGateway.sol new file mode 100644 index 0000000000..30701869fd --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarGateway.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IAxelarGateway { + /**********\ + |* Errors *| + \**********/ + + error NotSelf(); + error NotProxy(); + error InvalidCodeHash(); + error SetupFailed(); + error InvalidAuthModule(); + error InvalidTokenDeployer(); + error InvalidAmount(); + error InvalidChainId(); + error InvalidCommands(); + error TokenDoesNotExist(string symbol); + error TokenAlreadyExists(string symbol); + error TokenDeployFailed(string symbol); + error TokenContractDoesNotExist(address token); + error BurnFailed(string symbol); + error MintFailed(string symbol); + error InvalidSetMintLimitsParams(); + error ExceedMintLimit(string symbol); + + /**********\ + |* Events *| + \**********/ + + event TokenSent(address indexed sender, string destinationChain, string destinationAddress, string symbol, uint256 amount); + + event ContractCall( + address indexed sender, + string destinationChain, + string destinationContractAddress, + bytes32 indexed payloadHash, + bytes payload + ); + + event ContractCallWithToken( + address indexed sender, + string destinationChain, + string destinationContractAddress, + bytes32 indexed payloadHash, + bytes payload, + string symbol, + uint256 amount + ); + + event Executed(bytes32 indexed commandId); + + event TokenDeployed(string symbol, address tokenAddresses); + + event ContractCallApproved( + bytes32 indexed commandId, + string sourceChain, + string sourceAddress, + address indexed contractAddress, + bytes32 indexed payloadHash, + bytes32 sourceTxHash, + uint256 sourceEventIndex + ); + + event ContractCallApprovedWithMint( + bytes32 indexed commandId, + string sourceChain, + string sourceAddress, + address indexed contractAddress, + bytes32 indexed payloadHash, + string symbol, + uint256 amount, + bytes32 sourceTxHash, + uint256 sourceEventIndex + ); + + event TokenMintLimitUpdated(string symbol, uint256 limit); + + event OperatorshipTransferred(bytes newOperatorsData); + + event Upgraded(address indexed implementation); + + /********************\ + |* Public Functions *| + \********************/ + + function sendToken( + string calldata destinationChain, + string calldata destinationAddress, + string calldata symbol, + uint256 amount + ) external; + + function callContract( + string calldata destinationChain, + string calldata contractAddress, + bytes calldata payload + ) external; + + function callContractWithToken( + string calldata destinationChain, + string calldata contractAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount + ) external; + + function isContractCallApproved( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + address contractAddress, + bytes32 payloadHash + ) external view returns (bool); + + function isContractCallAndMintApproved( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + address contractAddress, + bytes32 payloadHash, + string calldata symbol, + uint256 amount + ) external view returns (bool); + + function validateContractCall( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash + ) external returns (bool); + + function validateContractCallAndMint( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash, + string calldata symbol, + uint256 amount + ) external returns (bool); + + /***********\ + |* Getters *| + \***********/ + + function authModule() external view returns (address); + + function tokenDeployer() external view returns (address); + + function tokenMintLimit(string memory symbol) external view returns (uint256); + + function tokenMintAmount(string memory symbol) external view returns (uint256); + + function allTokensFrozen() external view returns (bool); + + function implementation() external view returns (address); + + function tokenAddresses(string memory symbol) external view returns (address); + + function tokenFrozen(string memory symbol) external view returns (bool); + + function isCommandExecuted(bytes32 commandId) external view returns (bool); + + function adminEpoch() external view returns (uint256); + + function adminThreshold(uint256 epoch) external view returns (uint256); + + function admins(uint256 epoch) external view returns (address[] memory); + + /*******************\ + |* Admin Functions *| + \*******************/ + + function setTokenMintLimits(string[] calldata symbols, uint256[] calldata limits) external; + + function upgrade( + address newImplementation, + bytes32 newImplementationCodeHash, + bytes calldata setupParams + ) external; + + /**********************\ + |* External Functions *| + \**********************/ + + function setup(bytes calldata params) external; + + function execute(bytes calldata input) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IERC20.sol new file mode 100644 index 0000000000..3d0aa3bc23 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IERC20.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + error InvalidAccount(); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs new file mode 100644 index 0000000000..2194e81bbc --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs @@ -0,0 +1,243 @@ +// 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 ethabi::Token; +use fp_evm::PrecompileHandle; +use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; +use pallet_evm::{ExitError, PrecompileFailure}; +use precompile_utils::prelude::*; +use sp_core::{bounded::BoundedVec, ConstU32, Get, H160, H256, U256}; +use sp_runtime::{DispatchError, DispatchResult}; + +pub const MAX_SOURCE_CHAIN_BYTES: u32 = 32; +pub const MAX_SOURCE_ADDRESS_BYTES: u32 = 32; +pub const MAX_TOKEN_SYMBOL_BYTES: u32 = 32; +pub const MAX_PAYLOAD_BYTES: u32 = 32; + +pub type String = BoundedString>; +pub type Bytes = BoundedBytes>; + +pub const PREFIX_CONTRACT_CALL_APPROVED: [u8; 32] = keccak256!("contract-call-approved"); + +/// Precompile implementing IAxelarForecallable. +/// MUST be used as the receiver of calls over the Axelar bridge. +/// - `Axelar` defines the address of our local Axelar bridge contract +/// (AxelarGatewayProxy.sol). +/// - `ConvertSource` converts a Tuple `(String, String)` and tries to convert +/// this into a `DomainAddress`. +/// - First string: Defines `sourceChain` +/// - Second string: Defines `sourceAddress` +pub struct AxelarForecallable( + core::marker::PhantomData<(Runtime, Axelar, ConvertSource)>, +); + +#[precompile_utils::precompile] +impl AxelarForecallable +where + Runtime: frame_system::Config + pallet_evm::Config + pallet_connectors_gateway::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + ::RuntimeOrigin: + From, + Axelar: Get, + ConvertSource: sp_runtime::traits::Convert< + (Vec, Vec), + Result, + >, +{ + // Mimics: + + // function execute( + // bytes32 commandId, + // string calldata sourceChain, + // string calldata sourceAddress, + // bytes calldata payload + // ) external { + // bytes32 payloadHash = keccak256(payload); + // if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, + // payloadHash)) revert NotApprovedByGateway(); _execute(sourceChain, + // sourceAddress, payload); } + // + // Note: The _execute logic in this case will forward all calls to the + // pallet-connectors-gateway with a special runtime local origin + // + #[precompile::public("execute(bytes32,string,string,bytes)")] + fn execute( + handle: &mut impl PrecompileHandle, + command_id: H256, + source_chain: String, + source_address: String, + payload: Bytes, + ) -> EvmResult { + // CREATE HASH OF PAYLOAD + // - bytes32 payloadHash = keccak256(payload); + let payload_hash = H256::from(sp_io::hashing::keccak_256(payload.as_bytes())); + + // CHECK EVM STORAGE OF GATEWAY + // - keccak256(abi.encode(PREFIX_CONTRACT_CALL_APPROVED, commandId, sourceChain, + // sourceAddress, contractAddress, payloadHash)); + let key = H256::from(sp_io::hashing::keccak_256(ðabi::encode(&[ + Token::FixedBytes(PREFIX_CONTRACT_CALL_APPROVED.into()), + Token::FixedBytes(command_id.as_bytes().into()), + Token::String(source_chain.clone().try_into().map_err(|_| { + RevertReason::read_out_of_bounds("utf-8 encoding failing".to_string()) + })?), + Token::String(source_address.clone().try_into().map_err(|_| { + RevertReason::read_out_of_bounds("utf-8 encoding failing".to_string()) + })?), + // TODO: Check if this is really the address of this precompile + Token::Address(handle.context().address), + Token::FixedBytes(payload_hash.as_bytes().into()), + ]))); + + let msg = BoundedVec::< + u8, + ::MaxIncomingMessageSize, + >::try_from(payload.as_bytes().to_vec()) + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("payload conversion".into()), + })?; + + Self::execute_call(key, || { + pallet_connectors_gateway::Pallet::::process_msg( + pallet_connectors_gateway::GatewayOrigin::Local(ConvertSource::convert(( + source_chain.as_bytes().to_vec(), + source_address.as_bytes().to_vec(), + ))?) + .into(), + msg, + ) + }) + } + + // Mimics: + // + // function executeWithToken( + // bytes32 commandId, + // string calldata sourceChain, + // string calldata sourceAddress, + // bytes calldata payload, + // string calldata tokenSymbol, + // uint256 amount + // ) external { + // ... + // } + // + // Note: NOT SUPPORTED + // + #[precompile::public("executeWithToken(bytes32,string,string,bytes,string,uint256)")] + fn execute_with_token( + _handle: &mut impl PrecompileHandle, + _command_id: H256, + _source_chain: String, + _source_address: String, + _payload: Bytes, + _token_symbol: String, + _amount: U256, + ) -> EvmResult { + // TODO: Check whether this is enough or if we should error out + Ok(()) + } + + fn execute_call(key: H256, f: impl FnOnce() -> DispatchResult) -> EvmResult { + // TODO: Is the storage address actual the Gateway contract address ??? + let valid = Self::get_validate_call(Axelar::get(), key); + + if valid { + // Prevent re-entrance + Self::set_validate_call(Axelar::get(), key, false); + + f().map(|_| ()).map_err(TryDispatchError::Substrate)?; + + // Invalidate the storage entry of the call executed successfully + // TODO: Is the storage address actual the Gateway contract address ??? + Self::set_validate_call(Axelar::get(), key, false); + + Ok(()) + } else { + Err(RevertReason::Custom("Call not validated".to_string()).into()) + } + } + + fn get_validate_call(from: H160, key: H256) -> bool { + Self::h256_to_bool(pallet_evm::AccountStorages::::get( + from, + Self::get_index_validate_call(key), + )) + } + + fn set_validate_call(from: H160, key: H256, valid: bool) { + pallet_evm::AccountStorages::::set( + from, + Self::get_index_validate_call(key), + Self::bool_to_h256(valid), + ) + } + + fn get_index_validate_call(key: H256) -> H256 { + // Generate right index: + // + // From the solidty contract of Axelar (EnternalStorage.sol) + // mapping(bytes32 => uint256) private _uintStorage; -> Slot 0 + // mapping(bytes32 => string) private _stringStorage; -> Slot 1 + // mapping(bytes32 => address) private _addressStorage; -> Slot 2 + // mapping(bytes32 => bytes) private _bytesStorage; -> Slot 3 + // mapping(bytes32 => bool) private _boolStorage; -> Slot 4 + // mapping(bytes32 => int256) private _intStorage; -> Slot 5 + // + // This means our slot is U256::from(4) + let slot = U256::from(4); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(key.as_bytes()); + + let mut be_bytes: [u8; 32] = [0u8; 32]; + // TODO: Is endnianess correct here? + slot.to_big_endian(&mut be_bytes); + bytes.extend_from_slice(&be_bytes); + + H256::from(sp_io::hashing::keccak_256(&bytes)) + } + + // In Solidity, a boolean value (bool) is stored as a single byte (8 bits) in + // contract storage. The byte value 0x01 represents true, and the byte value + // 0x00 represents false. + // + // When you declare a boolean variable within a contract and store its value in + // storage, the contract reserves one storage slot, which is 32 bytes (256 bits) + // in size. However, only the first byte (8 bits) of that storage slot is used + // to store the boolean value. The remaining 31 bytes are left unused. + fn h256_to_bool(value: H256) -> bool { + let first = value.0[0]; + + // TODO; Should we check the other values too and error out then? + first == 1 + } + + // In Solidity, a boolean value (bool) is stored as a single byte (8 bits) in + // contract storage. The byte value 0x01 represents true, and the byte value + // 0x00 represents false. + // + // When you declare a boolean variable within a contract and store its value in + // storage, the contract reserves one storage slot, which is 32 bytes (256 bits) + // in size. However, only the first byte (8 bits) of that storage slot is used + // to store the boolean value. The remaining 31 bytes are left unused. + fn bool_to_h256(value: bool) -> H256 { + let mut bytes: [u8; 32] = [0u8; 32]; + + if value { + bytes[0] = 1; + } + + H256::from(bytes) + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml b/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml new file mode 100644 index 0000000000..df592d6b43 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml @@ -0,0 +1,101 @@ +[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 } +rand = "0.8.5" + +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/connectors-gateway-routers/src/axelar_evm.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/axelar_evm.rs new file mode 100644 index 0000000000..671346deae --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/axelar_evm.rs @@ -0,0 +1,251 @@ +// 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, ethereum::EthereumTransactor}; +use codec::{Decode, Encode, MaxEncodedLen}; +use ethabi::{Contract, Function, Param, ParamType, Token}; +use frame_support::{ + dispatch::{DispatchError, DispatchResult}, + ensure, +}; +use scale_info::{ + prelude::string::{String, ToString}, + TypeInfo, +}; +use sp_core::{H160, H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, vec, vec::Vec}; + +use crate::{AccountIdOf, MessageOf, CONNECTORS_FUNCTION_NAME, CONNECTORS_MESSAGE_PARAM}; + +/// 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 domain: EVMDomain, + pub _marker: PhantomData, +} + +/// The EVMDomain holds all relevant information for validating and executing +/// the call to the Axelar contract. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct EVMDomain { + /// The chain to which the router will send the message to. + pub chain: EVMChain, + + /// The address of the Axelar contract deployed in our EVM. + pub axelar_contract_address: H160, + + /// The `BlakeTwo256` hash of the Axelar contract code. + /// This is used during router initialization to ensure that the correct + /// contract code is used. + pub axelar_contract_hash: H256, + + /// The address of the Connectors contract that we are going to call through + /// the Axelar contract. + pub connectors_contract_address: H160, + + /// The values used when executing the EVM call to the Axelar contract. + 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, +} + +/// EVMChain holds all supported EVM chains. +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum EVMChain { + Ethereum, +} + +/// 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(), + } + } +} + +const AXELAR_FUNCTION_NAME: &'static str = "callContract"; +const AXELAR_DESTINATION_CHAIN_PARAM: &'static str = "destinationChain"; +const AXELAR_DESTINATION_CONTRACT_ADDRESS_PARAM: &'static str = "destinationContractAddress"; +const AXELAR_PAYLOAD_PARAM: &'static str = "payload"; + +impl AxelarEVMRouter +where + T: frame_system::Config + + pallet_connectors_gateway::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.domain.axelar_contract_address); + + ensure!( + BlakeTwo256::hash_of(&code) == self.domain.axelar_contract_hash, + DispatchError::Other("Axelar contract code does not match"), + ); + + Ok(()) + } + + /// Encodes the Connectors message to the required format, + /// then executes the EVM call using the Ethereum transaction pallet. + /// + /// NOTE - there 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: AccountIdOf, msg: MessageOf) -> DispatchResult { + let eth_msg = self.get_eth_msg(msg).map_err(DispatchError::Other)?; + + 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.domain.axelar_contract_address, + eth_msg.as_slice(), + self.domain.fee_values.value, + self.domain.fee_values.gas_price, + self.domain.fee_values.gas_limit, + ) + .map_err(|e| e.error)?; + + Ok(()) + } + + /// Encodes the provided message into the format required for submitting it + /// to the Axelar contract which in turn submits it to the Connectors + /// contract. + fn get_eth_msg(&self, msg: MessageOf) -> Result, &'static str> { + // `AxelarEVMRouter` -> `callContract` on the Axelar Gateway contract + // deployed in the Centrifuge EVM pallet. + // + // Axelar Gateway contract -> `handle` on the Connectors gateway contract + // deployed on Ethereum. + + // Connectors Call: + // + // function handle(bytes memory _message) external onlyRouter {} + + #[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(msg.serialize())]) + .map_err(|_| "cannot encode input for Connectors contract function")?; + + // Axelar Call: + // + // function callContract( + // string calldata destinationChain, + // string calldata destinationContractAddress, + // bytes calldata payload, + // ) external { + // emit ContractCall( + // msg.sender, + // destinationChain, + // destinationContractAddress, + // keccak256(payload), + // payload, + // ); + // } + + #[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(self.domain.chain.to_string()), + Token::String(self.domain.connectors_contract_address.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/connectors-gateway-routers/src/ethereum_xcm.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/ethereum_xcm.rs new file mode 100644 index 0000000000..3fe6240bc2 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/ethereum_xcm.rs @@ -0,0 +1,264 @@ +// 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 core::convert::TryFrom; + +use cfg_traits::connectors::Codec; +use codec::{Decode, Encode, MaxEncodedLen}; +use ethabi::{Bytes, Contract}; +use frame_support::{ + dispatch::DispatchResult, sp_runtime::DispatchError, traits::OriginTrait, weights::Weight, +}; +use pallet_xcm_transactor::{Currency, CurrencyPayment, TransactWeights}; +use scale_info::TypeInfo; +use sp_core::{bounded::BoundedVec, ConstU32, H160, U256}; +use sp_std::{boxed::Box, marker::PhantomData, vec, vec::Vec}; +use xcm::{ + v2::{MultiLocation, OriginKind}, + VersionedMultiLocation, +}; + +use crate::{ + AccountIdOf, CurrencyIdOf, MessageOf, 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 xcm_domain: XcmDomain>, + pub _marker: PhantomData, +} + +impl EthereumXCMRouter +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::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: AccountIdOf, msg: MessageOf) -> DispatchResult { + let contract_call = get_encoded_contract_call(msg.serialize()) + .map_err(|_| DispatchError::Other("encoded contract call retrieval"))?; + + let ethereum_xcm_call = + get_encoded_ethereum_xcm_call::(self.xcm_domain.clone(), contract_call) + .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(()) + } +} + +/// Build the encoded `ethereum_xcm::transact(eth_tx)` call that should +/// request to execute `evm_call`. +/// +/// * `xcm_domain` - All the necessary info regarding the xcm-based domain +/// where this `ethereum_xcm` call is to be executed +/// * `evm_call` - The encoded EVM call calling ConnectorsXcmRouter::handle(msg) +pub(crate) fn get_encoded_ethereum_xcm_call( + xcm_domain: XcmDomain>, + evm_call: Vec, +) -> Result, ()> +where + T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, +{ + let input = + BoundedVec::>::try_from( + evm_call, + ) + .map_err(|_| ())?; + + let mut encoded: Vec = Vec::new(); + + encoded.append( + &mut xcm_domain + .ethereum_xcm_transact_call_index + .clone() + .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) +} + +/// 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 { + use sp_std::collections::btree_map::BTreeMap; + + 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, + } +} + +/// 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 ConnectorsXcmRouter 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/connectors-gateway-routers/src/lib.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs new file mode 100644 index 0000000000..a0d6224834 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs @@ -0,0 +1,80 @@ +// 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; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::dispatch::DispatchResult; +use scale_info::TypeInfo; + +use crate::{axelar_evm::AxelarEVMRouter, ethereum_xcm::EthereumXCMRouter}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +pub mod axelar_evm; +pub mod ethereum_xcm; + +pub use axelar_evm::*; +pub use ethereum_xcm::*; + +type CurrencyIdOf = ::CurrencyId; +type MessageOf = ::Message; +type AccountIdOf = ::AccountId; + +const CONNECTORS_FUNCTION_NAME: &'static str = "handle"; +const CONNECTORS_MESSAGE_PARAM: &'static str = "message"; + +/// 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), +} + +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(), + } + } + + 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), + } + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-routers/src/mock.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/mock.rs new file mode 100644 index 0000000000..856fdefba8 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/mock.rs @@ -0,0 +1,473 @@ +use std::str::FromStr; + +use cfg_mocks::{pallet_mock_connectors, DomainRouterMock, MessageMock}; +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_connectors_gateway::Config for Runtime { + type AdminOrigin = EnsureRoot; + type InboundQueue = MockConnectors; + type LocalOrigin = EnsureLocal; + type MaxIncomingMessageSize = MaxIncomingMessageSize; + type Message = MessageMock; + type Router = DomainRouterMock; + 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/connectors-gateway-routers/src/tests.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/tests.rs new file mode 100644 index 0000000000..60ab70f0d4 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/tests.rs @@ -0,0 +1,480 @@ +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}; +use xcm::{ + lts::WeightLimit, + v2::OriginKind, + v3::{Instruction::*, MultiAsset, Weight}, +}; + +use super::mock::*; +use crate::{ + axelar_evm::AxelarEVMRouter, + ethereum_xcm::{get_encoded_contract_call, get_encoded_ethereum_xcm_call, EthereumXCMRouter}, + DomainRouter, EVMChain, EVMDomain, FeeValues, XcmDomain, XcmTransactInfo, +}; + +mod axelar_evm { + use super::*; + + mod init { + use sp_runtime::DispatchError; + + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code = rand::random::<[u8; 32]>().to_vec(); + let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); + let connectors_contract_address = H160::from_low_u64_be(2); + + pallet_evm::AccountCodes::::insert( + axelar_contract_address, + axelar_contract_code, + ); + + let evm_domain = EVMDomain { + chain: EVMChain::Ethereum, + axelar_contract_address, + axelar_contract_hash, + connectors_contract_address, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(10), + gas_price: U256::from(10), + }, + }; + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + domain: evm_domain, + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + }); + } + + #[test] + fn failure() { + new_test_ext().execute_with(|| { + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code = rand::random::<[u8; 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 { + chain: EVMChain::Ethereum, + axelar_contract_address, + axelar_contract_hash, + connectors_contract_address, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(10), + gas_price: U256::from(10), + }, + }; + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + domain: evm_domain, + _marker: Default::default(), + }); + + assert_noop!( + domain_router.init(), + DispatchError::Other("Axelar contract code does not match") + ); + + pallet_evm::AccountCodes::::insert( + axelar_contract_address, + rand::random::<[u8; 32]>().to_vec(), + ); + + assert_noop!( + domain_router.init(), + DispatchError::Other("Axelar contract code does not match") + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + let derived_sender = IdentityAddressMapping::into_account_id(sender_h160); + + Balances::mint_into(&derived_sender.into(), 1_000_000 * CFG).unwrap(); + + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_hash = H256::random(); + let connectors_contract_address = H160::from_low_u64_be(2); + + let transaction_call_cost = + ::config().gas_transaction_call; + + let evm_domain = EVMDomain { + chain: EVMChain::Ethereum, + axelar_contract_address, + axelar_contract_hash, + connectors_contract_address, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(transaction_call_cost + 10_000), + gas_price: U256::from(10), + }, + }; + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + domain: evm_domain, + _marker: Default::default(), + }); + + let msg = MessageMock::Second; + + assert_ok!(domain_router.send(sender, msg)); + }); + } + + #[test] + fn insufficient_balance() { + new_test_ext().execute_with(|| { + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_hash = H256::random(); + let connectors_contract_address = H160::from_low_u64_be(2); + + let evm_domain = EVMDomain { + chain: EVMChain::Ethereum, + axelar_contract_address, + axelar_contract_hash, + connectors_contract_address, + fee_values: FeeValues { + value: U256::from(1), + gas_limit: U256::from(10), + gas_price: U256::from(1), + }, + }; + + let domain_router = + DomainRouter::::AxelarEVM(AxelarEVMRouter:: { + domain: evm_domain, + _marker: Default::default(), + }); + + let msg = MessageMock::Second; + + let res = domain_router.send(sender, msg); + assert_eq!( + res.err().unwrap(), + pallet_evm::Error::::BalanceLow.into() + ); + }); + } + } +} + +mod ethereum_xcm { + use super::*; + + mod init { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + 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(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + xcm_domain: xcm_domain.clone(), + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + let res = pallet_xcm_transactor::TransactInfoWithWeightLimit::::get( + dest.clone(), + ) + .unwrap(); + + assert_eq!( + res.transact_extra_weight, + xcm_domain.transact_info.transact_extra_weight + ); + assert_eq!(res.max_weight, xcm_domain.transact_info.max_weight); + assert_eq!( + res.transact_extra_weight_signed, + xcm_domain.transact_info.transact_extra_weight_signed + ); + + assert_eq!( + pallet_xcm_transactor::DestinationAssetFeePerSecond::::get(dest), + Some(xcm_domain.fee_per_second), + ); + }); + } + } + + mod send { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + 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(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + xcm_domain: xcm_domain.clone(), + _marker: Default::default(), + }); + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: 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( + dest, + xcm_domain.fee_per_second.clone(), + ); + + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let msg = MessageMock::Second; + + assert_ok!(domain_router.send(sender, msg)); + + let sent_messages = sent_xcm(); + assert_eq!(sent_messages.len(), 1); + + let weight_limit = 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 + xcm_domain.transact_info.transact_extra_weight.ref_time() + )), + })); + + let msg = MessageMock::Second; + let contract_call = get_encoded_contract_call(msg.serialize()).unwrap(); + let expected_call = + get_encoded_ethereum_xcm_call::(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 success_with_init() { + new_test_ext().execute_with(|| { + 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(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + xcm_domain: xcm_domain.clone(), + _marker: Default::default(), + }); + + assert_ok!(domain_router.init()); + + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let msg = MessageMock::Second; + + assert_ok!(domain_router.send(sender, msg)); + }); + } + + #[test] + fn transactor_info_not_set() { + new_test_ext().execute_with(|| { + 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(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + xcm_domain: xcm_domain.clone(), + _marker: Default::default(), + }); + + // Manually insert the fee per second in the `DestinationAssetFeePerSecond` + // storage. + + pallet_xcm_transactor::DestinationAssetFeePerSecond::::insert( + dest, + xcm_domain.fee_per_second.clone(), + ); + + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let msg = MessageMock::Second; + + assert_noop!( + domain_router.send(sender, msg), + pallet_xcm_transactor::Error::::TransactorInfoNotSet, + ); + }); + } + + #[test] + fn fee_per_second_not_set() { + new_test_ext().execute_with(|| { + 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(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(dest.clone().into_versioned()), + }; + + let domain_router = + DomainRouter::::EthereumXCM(EthereumXCMRouter:: { + xcm_domain: xcm_domain.clone(), + _marker: Default::default(), + }); + + // Manually insert the transact weight info in the `TransactInfoWithWeightLimit` + // storage. + + pallet_xcm_transactor::TransactInfoWithWeightLimit::::insert( + dest.clone(), + RemoteTransactInfoWithMaxWeight { + transact_extra_weight: xcm_domain + .transact_info + .transact_extra_weight + .clone(), + max_weight: xcm_domain.transact_info.max_weight.clone(), + transact_extra_weight_signed: None, + }, + ); + + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let msg = MessageMock::Second; + + assert_noop!( + domain_router.send(sender, msg), + pallet_xcm_transactor::Error::::FeePerSecondNotSet, + ); + }); + } + } +} diff --git a/pallets/connectors-gateway/src/lib.rs b/pallets/connectors-gateway/src/lib.rs new file mode 100644 index 0000000000..e4800b2826 --- /dev/null +++ b/pallets/connectors-gateway/src/lib.rs @@ -0,0 +1,273 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +mod origin; +pub use origin::*; + +pub mod weights; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +pub mod pallet { + use core::fmt::Debug; + + use cfg_traits::connectors::{Codec, InboundQueue, OutboundQueue, Router as DomainRouter}; + use cfg_types::domain_address::{Domain, DomainAddress}; + use codec::{EncodeLike, FullCodec}; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::OriginFor; + use sp_std::convert::TryInto; + + use super::*; + use crate::weights::WeightInfo; + + #[pallet::pallet] + #[pallet::generate_store(pub (super) trait Store)] + pub struct Pallet(_); + + #[pallet::origin] + pub type Origin = GatewayOrigin; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The origin type. + type RuntimeOrigin: Into::RuntimeOrigin>> + + From; + + /// The event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The LocalOrigin ensures that some calls can only be performed from a + /// local context i.e. a different pallet. + type LocalOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = DomainAddress, + >; + + /// The AdminOrigin ensures that some calls can only be performed by + /// admins. + type AdminOrigin: EnsureOrigin<::RuntimeOrigin>; + + /// The incoming and outgoing message type. + /// + /// NOTE - this `Codec` trait is the Centrifuge trait for connectors + /// messages. + type Message: Codec; + + /// The message router type that is stored for each domain. + type Router: DomainRouter + + Clone + + Debug + + MaxEncodedLen + + TypeInfo + + FullCodec + + EncodeLike + + PartialEq; + + /// The type that processes incoming messages. + type InboundQueue: InboundQueue; + + type WeightInfo: WeightInfo; + + /// Maximum size of an incoming message. + #[pallet::constant] + type MaxIncomingMessageSize: Get; + } + + #[pallet::event] + #[pallet::generate_deposit(pub (super) fn deposit_event)] + pub enum Event { + /// The router for a given domain was set. + DomainRouterSet { domain: Domain, router: T::Router }, + + /// A connector was added to a domain. + ConnectorAdded { connector: DomainAddress }, + + /// A connector was removed from a domain. + ConnectorRemoved { connector: DomainAddress }, + } + + /// Storage for domain routers. + /// + /// This can only be set by an admin. + #[pallet::storage] + pub(crate) type DomainRouters = StorageMap<_, Blake2_128Concat, Domain, T::Router>; + + /// Storage that contains a limited number of whitelisted connectors for a + /// particular domain. + /// + /// This can only be modified by an admin. + #[pallet::storage] + pub(crate) type ConnectorsAllowlist = StorageDoubleMap< + _, + Blake2_128Concat, + Domain, + Blake2_128Concat, + DomainAddress, + (), + ValueQuery, + >; + + #[pallet::error] + pub enum Error { + /// Router initialization failed. + RouterInitFailed, + + /// The origin of the message to be processed is invalid. + InvalidMessageOrigin, + + /// The domain is not supported. + DomainNotSupported, + + /// Message decoding error. + MessageDecode, + + /// Connector was already added to the domain. + ConnectorAlreadyAdded, + + /// Maximum number of connectors for a domain was reached. + MaxConnectorsReached, + + /// Connector was not found. + ConnectorNotFound, + + /// Unknown connector. + UnknownConnector, + + /// Router not found. + RouterNotFound, + } + + #[pallet::call] + impl Pallet { + /// Set a domain's router, + #[pallet::weight(< T as Config >::WeightInfo::set_domain_router())] + #[pallet::call_index(0)] + pub fn set_domain_router( + origin: OriginFor, + domain: Domain, + router: T::Router, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + ensure!(domain != Domain::Centrifuge, Error::::DomainNotSupported); + + router.init().map_err(|_| Error::::RouterInitFailed)?; + + >::insert(domain.clone(), router.clone()); + + Self::deposit_event(Event::DomainRouterSet { domain, router }); + + Ok(()) + } + + /// Add a connector for a specific domain. + #[pallet::weight(< T as Config >::WeightInfo::add_connector())] + #[pallet::call_index(1)] + pub fn add_connector(origin: OriginFor, connector: DomainAddress) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + ensure!( + connector.domain() != Domain::Centrifuge, + Error::::DomainNotSupported + ); + + ensure!( + !ConnectorsAllowlist::::contains_key(connector.domain(), connector.clone()), + Error::::ConnectorAlreadyAdded, + ); + + ConnectorsAllowlist::::insert(connector.domain(), connector.clone(), ()); + + Self::deposit_event(Event::ConnectorAdded { connector }); + + Ok(()) + } + + /// Remove a connector from a specific domain. + #[pallet::weight(< T as Config >::WeightInfo::remove_connector())] + #[pallet::call_index(2)] + pub fn remove_connector(origin: OriginFor, connector: DomainAddress) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin.clone())?; + + ensure!( + ConnectorsAllowlist::::contains_key(connector.domain(), connector.clone()), + Error::::ConnectorNotFound, + ); + + ConnectorsAllowlist::::remove(connector.domain(), connector.clone()); + + Self::deposit_event(Event::ConnectorRemoved { connector }); + + Ok(()) + } + + /// Process an incoming message. + #[pallet::weight(0)] + #[pallet::call_index(3)] + pub fn process_msg( + origin: OriginFor, + msg: BoundedVec, + ) -> DispatchResult { + let domain_address = T::LocalOrigin::ensure_origin(origin)?; + + match domain_address { + DomainAddress::EVM(_, _) => { + ensure!( + ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + ), + Error::::UnknownConnector, + ); + + let incoming_msg = T::Message::deserialize(&mut msg.as_slice()) + .map_err(|_| Error::::MessageDecode)?; + + T::InboundQueue::submit(domain_address, incoming_msg) + } + DomainAddress::Centrifuge(_) => Err(Error::::InvalidMessageOrigin.into()), + } + } + } + + /// This pallet will be the `OutboundQueue` used by other pallets to send + /// outgoing Connectors messages. + impl OutboundQueue for Pallet { + type Destination = Domain; + type Message = T::Message; + type Sender = T::AccountId; + + fn submit( + destination: Self::Destination, + sender: Self::Sender, + msg: Self::Message, + ) -> DispatchResult { + ensure!( + destination != Domain::Centrifuge, + Error::::DomainNotSupported + ); + + let router = DomainRouters::::get(destination).ok_or(Error::::RouterNotFound)?; + + router.send(sender, msg) + } + } +} diff --git a/pallets/connectors-gateway/src/mock.rs b/pallets/connectors-gateway/src/mock.rs new file mode 100644 index 0000000000..c927ecd59e --- /dev/null +++ b/pallets/connectors-gateway/src/mock.rs @@ -0,0 +1,99 @@ +use cfg_mocks::{pallet_mock_connectors, DomainRouterMock, MessageMock}; +use cfg_types::domain_address::DomainAddress; +use frame_system::EnsureRoot; +use sp_core::{crypto::AccountId32, ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +use crate::{pallet as pallet_connectors_gateway, EnsureLocal}; + +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, + } +); + +frame_support::parameter_types! { + 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_connectors_gateway::Config for Runtime { + type AdminOrigin = EnsureRoot; + type InboundQueue = MockConnectors; + type LocalOrigin = EnsureLocal; + type MaxIncomingMessageSize = MaxIncomingMessageSize; + type Message = MessageMock; + type Router = DomainRouterMock; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type WeightInfo = (); +} + +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/src/origin.rs b/pallets/connectors-gateway/src/origin.rs new file mode 100644 index 0000000000..ad5408b016 --- /dev/null +++ b/pallets/connectors-gateway/src/origin.rs @@ -0,0 +1,44 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_types::domain_address::DomainAddress; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::EnsureOrigin; +use scale_info::TypeInfo; +#[cfg(feature = "runtime-benchmarks")] +use sp_core::H160; +use sp_runtime::RuntimeDebug; + +#[derive(Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum GatewayOrigin { + Local(DomainAddress), +} + +pub struct EnsureLocal; + +impl> + From> EnsureOrigin for EnsureLocal { + type Success = DomainAddress; + + fn try_origin(o: O) -> Result { + o.into().map(|o| match o { + GatewayOrigin::Local(domain_address) => domain_address, + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(GatewayOrigin::Local(DomainAddress::EVM( + 1, + H160::from_low_u64_be(1).into(), + )))) + } +} diff --git a/pallets/connectors-gateway/src/tests.rs b/pallets/connectors-gateway/src/tests.rs new file mode 100644 index 0000000000..cf2294fb78 --- /dev/null +++ b/pallets/connectors-gateway/src/tests.rs @@ -0,0 +1,480 @@ +use cfg_mocks::*; +use cfg_traits::connectors::{Codec, OutboundQueue}; +use cfg_types::domain_address::*; +use frame_support::{assert_noop, assert_ok}; +use sp_core::{crypto::AccountId32, ByteArray, H160}; +use sp_runtime::DispatchError::BadOrigin; + +use super::{ + mock::{RuntimeEvent as MockEvent, *}, + origin::*, + pallet::*, +}; + +mod utils { + use super::*; + + pub fn get_random_test_account_id() -> AccountId32 { + rand::random::<[u8; 32]>().into() + } + + pub fn event_exists>(e: E) { + let actual: Vec = frame_system::Pallet::::events() + .iter() + .map(|e| e.event.clone()) + .collect(); + + let e: MockEvent = e.into(); + let mut exists = false; + for evt in actual { + if evt == e { + exists = true; + break; + } + } + assert!(exists); + } +} + +use utils::*; + +mod set_domain_router { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let router = DomainRouterMock::new(); + + assert_ok!(ConnectorsGateway::set_domain_router( + RuntimeOrigin::root(), + domain.clone(), + router.clone(), + )); + + let storage_entry = DomainRouters::::get(domain.clone()); + assert_eq!(storage_entry.unwrap(), router); + + event_exists(Event::::DomainRouterSet { domain, router }); + }); + } + + #[test] + fn bad_origin() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let router = DomainRouterMock::new(); + + assert_noop!( + ConnectorsGateway::set_domain_router( + RuntimeOrigin::signed(get_random_test_account_id()), + domain.clone(), + router, + ), + BadOrigin + ); + + let storage_entry = DomainRouters::::get(domain); + assert!(storage_entry.is_none()); + }); + } + + #[test] + fn unsupported_domain() { + new_test_ext().execute_with(|| { + let domain = Domain::Centrifuge; + let router = DomainRouterMock::new(); + + assert_noop!( + ConnectorsGateway::set_domain_router(RuntimeOrigin::root(), domain.clone(), router), + Error::::DomainNotSupported + ); + + let storage_entry = DomainRouters::::get(domain); + assert!(storage_entry.is_none()); + }); + } +} + +mod add_connector { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address, + }); + }); + } + + #[test] + fn bad_origin() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_noop!( + ConnectorsGateway::add_connector( + RuntimeOrigin::signed(get_random_test_account_id()), + domain_address.clone(), + ), + BadOrigin + ); + + assert!(!ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + }); + } + + #[test] + fn unsupported_domain() { + new_test_ext().execute_with(|| { + let domain_address = DomainAddress::Centrifuge(get_random_test_account_id().into()); + + assert_noop!( + ConnectorsGateway::add_connector(RuntimeOrigin::root(), domain_address.clone()), + Error::::DomainNotSupported + ); + + assert!(!ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + }); + } + + #[test] + fn connector_already_added() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + assert_noop!( + ConnectorsGateway::add_connector(RuntimeOrigin::root(), domain_address,), + Error::::ConnectorAlreadyAdded + ); + }); + } +} + +mod remove_connector { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address.clone(), + }); + + assert_ok!(ConnectorsGateway::remove_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(!ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address.clone(), + }); + }); + } + + #[test] + fn bad_origin() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_noop!( + ConnectorsGateway::remove_connector( + RuntimeOrigin::signed(get_random_test_account_id()), + domain_address.clone(), + ), + BadOrigin + ); + }); + } + + #[test] + fn connector_not_found() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_noop!( + ConnectorsGateway::remove_connector(RuntimeOrigin::root(), domain_address.clone(),), + Error::::ConnectorNotFound, + ); + }); + } +} + +mod process_msg { + use sp_core::bounded::BoundedVec; + + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address.clone(), + }); + + let expected_msg = MessageMock::First; + let encoded_msg = expected_msg.serialize(); + + let expected_domain_address = domain_address.clone(); + + MockConnectors::mock_submit(move |domain, message| { + assert_eq!(domain, expected_domain_address); + assert_eq!(message, expected_msg); + Ok(()) + }); + + assert_ok!(ConnectorsGateway::process_msg( + GatewayOrigin::Local(domain_address).into(), + BoundedVec::::try_from(encoded_msg).unwrap() + )); + }); + } + + #[test] + fn bad_origin() { + new_test_ext().execute_with(|| { + let encoded_msg = MessageMock::First.serialize(); + + assert_noop!( + ConnectorsGateway::process_msg( + RuntimeOrigin::root(), + BoundedVec::::try_from(encoded_msg).unwrap() + ), + BadOrigin, + ); + }); + } + + #[test] + fn invalid_message_origin() { + new_test_ext().execute_with(|| { + let domain_address = DomainAddress::Centrifuge(get_random_test_account_id().into()); + let encoded_msg = MessageMock::First.serialize(); + + assert_noop!( + ConnectorsGateway::process_msg( + GatewayOrigin::Local(domain_address).into(), + BoundedVec::::try_from(encoded_msg).unwrap() + ), + Error::::InvalidMessageOrigin, + ); + }); + } + + #[test] + fn unknown_connector() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + let encoded_msg = MessageMock::First.serialize(); + + assert_noop!( + ConnectorsGateway::process_msg( + GatewayOrigin::Local(domain_address).into(), + BoundedVec::::try_from(encoded_msg).unwrap() + ), + Error::::UnknownConnector, + ); + }); + } + + #[test] + fn message_decode() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address.clone(), + }); + + let encoded_msg: Vec = vec![11]; + + assert_noop!( + ConnectorsGateway::process_msg( + GatewayOrigin::Local(domain_address).into(), + BoundedVec::::try_from(encoded_msg).unwrap() + ), + Error::::MessageDecode, + ); + }); + } + + #[test] + fn connectors_error() { + new_test_ext().execute_with(|| { + let address = H160::from_slice(&get_random_test_account_id().as_slice()[..20]); + let domain_address = DomainAddress::EVM(0, address.into()); + + assert_ok!(ConnectorsGateway::add_connector( + RuntimeOrigin::root(), + domain_address.clone(), + )); + + assert!(ConnectorsAllowlist::::contains_key( + domain_address.domain(), + domain_address.clone() + )); + + event_exists(Event::::ConnectorAdded { + connector: domain_address.clone(), + }); + + let expected_msg = MessageMock::First; + let encoded_msg = expected_msg.serialize(); + + let expected_domain_address = domain_address.clone(); + + let err = sp_runtime::DispatchError::from("connectors error"); + + MockConnectors::mock_submit(move |domain, message| { + assert_eq!(domain, expected_domain_address); + assert_eq!(message, expected_msg); + Err(err) + }); + + assert_noop!( + ConnectorsGateway::process_msg( + GatewayOrigin::Local(domain_address).into(), + BoundedVec::::try_from(encoded_msg).unwrap() + ), + err, + ); + }); + } +} + +mod outbound_queue_impl { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let router = DomainRouterMock::new(); + + assert_ok!(ConnectorsGateway::set_domain_router( + RuntimeOrigin::root(), + domain.clone(), + router.clone(), + )); + + let storage_entry = DomainRouters::::get(domain.clone()); + assert_eq!(storage_entry.unwrap(), router); + + event_exists(Event::::DomainRouterSet { + domain: domain.clone(), + router, + }); + + let sender = get_random_test_account_id(); + let msg = MessageMock::First; + + assert_ok!(ConnectorsGateway::submit(domain, sender, msg)); + }); + } + + #[test] + fn local_domain() { + new_test_ext().execute_with(|| { + let domain = Domain::Centrifuge; + let sender = get_random_test_account_id(); + let msg = MessageMock::First; + + assert_noop!( + ConnectorsGateway::submit(domain, sender, msg), + Error::::DomainNotSupported + ); + }); + } + + #[test] + fn router_not_found() { + new_test_ext().execute_with(|| { + let domain = Domain::EVM(0); + let sender = get_random_test_account_id(); + let msg = MessageMock::First; + + assert_noop!( + ConnectorsGateway::submit(domain, sender, msg), + Error::::RouterNotFound + ); + }); + } +} diff --git a/pallets/connectors-gateway/src/weights.rs b/pallets/connectors-gateway/src/weights.rs new file mode 100644 index 0000000000..9702a4a9a4 --- /dev/null +++ b/pallets/connectors-gateway/src/weights.rs @@ -0,0 +1,38 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use frame_support::weights::Weight; + +pub trait WeightInfo { + fn set_domain_router() -> Weight; + fn add_connector() -> Weight; + fn remove_connector() -> Weight; + fn process_msg() -> Weight; +} + +impl WeightInfo for () { + fn set_domain_router() -> Weight { + Weight::zero() + } + + fn add_connector() -> Weight { + Weight::zero() + } + + fn remove_connector() -> Weight { + Weight::zero() + } + + fn process_msg() -> Weight { + Weight::zero() + } +} diff --git a/pallets/connectors/src/lib.rs b/pallets/connectors/src/lib.rs index e249473c47..5073c5eb5f 100644 --- a/pallets/connectors/src/lib.rs +++ b/pallets/connectors/src/lib.rs @@ -372,7 +372,7 @@ pub mod pallet { Error::::PoolNotFound ); - T::OutboundQueue::submit(who, domain, Message::AddPool { pool_id })?; + T::OutboundQueue::submit(domain, who, Message::AddPool { pool_id })?; Ok(()) } @@ -404,8 +404,8 @@ pub mod pallet { // Send the message to the domain T::OutboundQueue::submit( - who, domain, + who, Message::AddTranche { pool_id, tranche_id, @@ -435,8 +435,8 @@ pub mod pallet { .price; T::OutboundQueue::submit( - who, domain, + who, Message::UpdateTrancheTokenPrice { pool_id, tranche_id, @@ -484,8 +484,8 @@ pub mod pallet { ); T::OutboundQueue::submit( - who, domain_address.domain(), + who, Message::UpdateMember { pool_id, tranche_id, @@ -538,8 +538,8 @@ pub mod pallet { )?; T::OutboundQueue::submit( - who.clone(), domain_address.domain(), + who.clone(), Message::TransferTrancheTokens { pool_id, tranche_id, @@ -600,8 +600,8 @@ pub mod pallet { )?; T::OutboundQueue::submit( - who.clone(), receiver.domain(), + who.clone(), Message::Transfer { amount, currency, @@ -631,8 +631,8 @@ pub mod pallet { } = Self::try_get_wrapped_token(¤cy_id)?; T::OutboundQueue::submit( - who, Domain::EVM(chain_id), + who, Message::AddCurrency { currency, evm_address, @@ -679,8 +679,8 @@ pub mod pallet { Self::try_get_wrapped_token(¤cy_id)?; T::OutboundQueue::submit( - who, Domain::EVM(chain_id), + who, Message::AllowPoolCurrency { pool_id, currency }, )?; diff --git a/pallets/ethereum-transaction/Cargo.toml b/pallets/ethereum-transaction/Cargo.toml new file mode 100644 index 0000000000..cb9514e1fa --- /dev/null +++ b/pallets/ethereum-transaction/Cargo.toml @@ -0,0 +1,83 @@ +[package] +authors = ["Centrifuge "] +description = "Centrifuge Ethereum Transaction Pallet" +edition = "2021" +license = "LGPL-3.0" +name = "pallet-ethereum-transaction" +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-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# Benchmarking +frame-benchmarking = { git = "https://github.com/paritytech/substrate", default-features = false, optional = true, branch = "polkadot-v0.9.38" } + +# Substrate crates +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } + +# Ethereum +ethabi = { version = "16.0", default-features = false } +ethereum = { version = "0.14.0", default-features = false } +fp-ethereum = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +fp-evm = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +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" } + +# Our custom traits +cfg-traits = { path = "../../libs/traits", default-features = false } + +[dev-dependencies] +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" } + +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "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" } + +rand = "0.8.5" + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "cfg-traits/runtime-benchmarks", + "pallet-ethereum/runtime-benchmarks", + "pallet-evm/runtime-benchmarks", +] +std = [ + "codec/std", + "ethabi/std", + "cfg-traits/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + "sp-core/std", + "sp-std/std", + "sp-runtime/std", + "scale-info/std", + "pallet-ethereum/std", + "pallet-evm/std", + "fp-ethereum/std", + "fp-evm/std", + "ethereum/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "cfg-traits/try-runtime", + "pallet-ethereum/try-runtime", + "pallet-evm/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/ethereum-transaction/src/lib.rs b/pallets/ethereum-transaction/src/lib.rs new file mode 100644 index 0000000000..73774045b5 --- /dev/null +++ b/pallets/ethereum-transaction/src/lib.rs @@ -0,0 +1,206 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +#![cfg_attr(not(feature = "std"), no_std)] + +use cfg_traits::ethereum::EthereumTransactor; +use ethereum::{LegacyTransaction, TransactionAction, TransactionSignature, TransactionV2}; +use fp_evm::CallOrCreateInfo; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo}, + pallet_prelude::*, +}; +pub use pallet::*; +use pallet_evm::{ExitError, ExitFatal, ExitReason}; +use sp_core::{H160, H256, U256}; +use sp_std::vec::Vec; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +const TRANSACTION_RECOVERY_ID: u64 = 42; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub (super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_ethereum::Config { + /// The event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + /// Storage for nonce. + #[pallet::storage] + pub(crate) type Nonce = StorageValue<_, U256, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub (super) fn deposit_event)] + pub enum Event { + /// A call was executed. + Executed { + from: H160, + to: H160, + exit_reason: ExitReason, + value: Vec, + }, + } + + impl Pallet { + fn get_transaction_signature() -> Option { + //TODO(cdamian): Same signature as the one in ethereum-xcm. + TransactionSignature::new( + TRANSACTION_RECOVERY_ID, + H256::from_low_u64_be(1u64), + H256::from_low_u64_be(1u64), + ) + } + } + + impl EthereumTransactor for Pallet { + /// This implementation serves as a wrapper around the Ethereum pallet + /// execute functionality. It keeps track of the nonce used for each + /// call and builds a fake signature for executing the provided call. + /// + /// NOTE - The execution fees are charged by the Ethereum pallet, + /// we only have to charge for the nonce read operation. + fn call( + from: H160, + to: H160, + data: &[u8], + value: U256, + gas_price: U256, + gas_limit: U256, + ) -> DispatchResultWithPostInfo { + let nonce = Nonce::::get(); + let read_weight = T::DbWeight::get().reads(1); + + let signature = + Pallet::::get_transaction_signature().ok_or(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: DispatchError::Other("Failed to create transaction signature"), + })?; + + let transaction = TransactionV2::Legacy(LegacyTransaction { + nonce, + gas_price, + gas_limit, + action: TransactionAction::Call(to), + value, + input: data.into(), + signature, + }); + + Nonce::::put(nonce.saturating_add(U256::one())); + + let (_target, _value, info) = pallet_ethereum::Pallet::::execute( + from, + &transaction, + Some(T::config().clone()), + ) + .map_err(|e| { + let weight = e.post_info.actual_weight.map_or(Weight::zero(), |w| w); + + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(weight.saturating_add(read_weight)), + pays_fee: Pays::Yes, + }, + error: e.error, + } + })?; + match info { + CallOrCreateInfo::Call(call_info) => { + Self::deposit_event(Event::Executed { + from, + to, + exit_reason: call_info.exit_reason.clone(), + value: call_info.value.clone(), + }); + + match call_info.exit_reason { + ExitReason::Succeed(_) => Ok(PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }), + ExitReason::Error(e) => Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: map_evm_error(e), + }), + ExitReason::Revert(_) => Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: DispatchError::Other("EVM encountered an explicit revert"), + }), + ExitReason::Fatal(e) => Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: map_evm_fatal_error(e), + }), + } + } + CallOrCreateInfo::Create(_) => Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(read_weight), + pays_fee: Pays::Yes, + }, + error: DispatchError::Other("unexpected execute result"), + }), + } + } + } + + fn map_evm_error(e: ExitError) -> DispatchError { + match e { + ExitError::StackUnderflow => DispatchError::Other("stack underflow"), + ExitError::StackOverflow => DispatchError::Other("stack overflow"), + ExitError::InvalidJump => DispatchError::Other("invalid jump"), + ExitError::InvalidRange => DispatchError::Other("invalid range"), + ExitError::DesignatedInvalid => DispatchError::Other("designated invalid"), + ExitError::CallTooDeep => DispatchError::Other("call too deep"), + ExitError::CreateCollision => DispatchError::Other("create collision"), + ExitError::CreateContractLimit => DispatchError::Other("create contract limit"), + ExitError::InvalidCode(_) => DispatchError::Other("invalid op code"), + ExitError::OutOfOffset => DispatchError::Other("out of offset"), + ExitError::OutOfGas => DispatchError::Other("out of gas"), + ExitError::OutOfFund => DispatchError::Other("out of fund"), + ExitError::PCUnderflow => DispatchError::Other("PC underflow"), + ExitError::CreateEmpty => DispatchError::Other("create empty"), + ExitError::Other(_) => DispatchError::Other("evm error"), + } + } + + fn map_evm_fatal_error(e: ExitFatal) -> DispatchError { + match e { + ExitFatal::NotSupported => DispatchError::Other("not supported"), + ExitFatal::UnhandledInterrupt => DispatchError::Other("unhandled interrupt"), + ExitFatal::CallErrorAsFatal(e) => map_evm_error(e), + ExitFatal::Other(_) => DispatchError::Other("evm fatal error"), + } + } +} diff --git a/pallets/ethereum-transaction/src/mock.rs b/pallets/ethereum-transaction/src/mock.rs new file mode 100644 index 0000000000..3f084c567f --- /dev/null +++ b/pallets/ethereum-transaction/src/mock.rs @@ -0,0 +1,197 @@ +use std::str::FromStr; + +use fp_evm::{FeeCalculator, Precompile, PrecompileResult}; +use frame_support::{parameter_types, traits::FindAuthor, weights::Weight}; +use pallet_ethereum::IntermediateStateRoot; +use pallet_evm::{ + runner::stack::Runner, AddressMapping, EnsureAddressNever, EnsureAddressRoot, + FixedGasWeightMapping, PrecompileHandle, PrecompileSet, SubstrateBlockHashMapping, +}; +use sp_core::{crypto::AccountId32, ByteArray, ConstU16, ConstU32, ConstU64, H160, H256, U256}; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + ConsensusEngineId, +}; + +use crate::pallet as pallet_ethereum_transaction; + +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, + 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; +} + +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 = (); +} + +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(&2000u64.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; +} + +impl pallet_ethereum_transaction::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + +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/ethereum-transaction/src/tests.rs b/pallets/ethereum-transaction/src/tests.rs new file mode 100644 index 0000000000..69396c68ec --- /dev/null +++ b/pallets/ethereum-transaction/src/tests.rs @@ -0,0 +1,117 @@ +use cfg_traits::ethereum::EthereumTransactor; +use frame_support::{assert_ok, traits::fungible::Mutate}; +use pallet_evm::{AddressMapping, Error::BalanceLow}; +use sp_core::{crypto::AccountId32, H160, U256}; +use sp_runtime::DispatchError; + +use super::mock::*; +use crate::pallet::Nonce; + +mod call { + use super::*; + + #[test] + fn success() { + new_test_ext().execute_with(|| { + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + let derived_sender = IdentityAddressMapping::into_account_id(sender_h160); + + Balances::mint_into(&derived_sender.into(), 1_000_000_000_000_000).unwrap(); + + let to = H160::from_low_u64_be(rand::random()); + let data = rand::random::<[u8; 10]>(); + let value = U256::from(10); + let gas_price = U256::from(10); + + let transaction_call_cost = + ::config().gas_transaction_call; + + // Ensure that the gas limit is enough to cover for executing a call. + let gas_limit = U256::from(transaction_call_cost + 10_000); + + assert_eq!(Nonce::::get(), U256::from(0)); + + assert_ok!(::call( + sender_h160, + to, + data.as_slice(), + value, + gas_price, + gas_limit + )); + + assert_eq!(Nonce::::get(), U256::from(1)); + }); + } + + #[test] + fn insufficient_balance() { + new_test_ext().execute_with(|| { + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + + let to = H160::from_low_u64_be(rand::random()); + let data = rand::random::<[u8; 10]>(); + let value = U256::from(rand::random::()); + let gas_price = U256::from(10); + + let transaction_call_cost = + ::config().gas_transaction_call; + + let gas_limit = U256::from(transaction_call_cost + 10_000); + + assert_eq!(Nonce::::get(), U256::from(0)); + + let res = ::call( + sender_h160, + to, + data.as_slice(), + value, + gas_price, + gas_limit, + ); + assert_eq!(res.err().unwrap().error, BalanceLow::.into()); + + assert_eq!(Nonce::::get(), U256::from(1)); + }); + } + + #[test] + fn out_of_gas() { + new_test_ext().execute_with(|| { + let sender: AccountId32 = rand::random::<[u8; 32]>().into(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + let derived_sender = IdentityAddressMapping::into_account_id(sender_h160); + + Balances::mint_into(&derived_sender.into(), 1_000_000_000_000_000).unwrap(); + + let to = H160::from_low_u64_be(rand::random()); + let data = rand::random::<[u8; 10]>(); + let value = U256::from(rand::random::()); + let gas_price = U256::from(10); + + let transaction_call_cost = + ::config().gas_transaction_call; + + let gas_limit = U256::from(transaction_call_cost - 10_000); + + assert_eq!(Nonce::::get(), U256::from(0)); + + let res = ::call( + sender_h160, + to, + data.as_slice(), + value, + gas_price, + gas_limit, + ); + assert_eq!(res.err().unwrap().error, DispatchError::Other("out of gas")); + + assert_eq!(Nonce::::get(), U256::from(1)); + }); + } +} diff --git a/runtime/development/Cargo.toml b/runtime/development/Cargo.toml index ba8382baf6..8f30f918aa 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,15 +113,18 @@ 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/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 } pallet-claims = { path = "../../pallets/claims", default-features = false } pallet-collator-allowlist = { path = "../../pallets/collator-allowlist", default-features = false } pallet-connectors = { path = "../../pallets/connectors", default-features = false } +pallet-connectors-gateway = { path = "../../pallets/connectors-gateway", default-features = false } pallet-crowdloan-claim = { path = "../../pallets/crowdloan-claim", default-features = false } pallet-crowdloan-reward = { path = "../../pallets/crowdloan-reward", default-features = false } pallet-data-collector = { path = "../../pallets/data-collector", default-features = false } +pallet-ethereum-transaction = { path = "../../pallets/ethereum-transaction", default-features = false } pallet-fees = { path = "../../pallets/fees", default-features = false } pallet-interest-accrual = { path = "../../pallets/interest-accrual", default-features = false } pallet-investments = { path = "../../pallets/investments", default-features = false } @@ -148,11 +152,11 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", bran [features] default = ["std"] fast-runtime = [] +instant-voting = [] std = [ "cfg-primitives/std", "cfg-types/std", "chainbridge/std", - "chainbridge/std", "codec/std", "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", @@ -186,12 +190,15 @@ std = [ "pallet-collator-selection/std", "pallet-collective/std", "pallet-connectors/std", + "pallet-connectors-gateway/std", + "connectors-gateway-routers/std", "pallet-crowdloan-claim/std", "pallet-crowdloan-reward/std", "pallet-data-collector/std", "pallet-democracy/std", "pallet-elections-phragmen/std", "pallet-ethereum/std", + "pallet-ethereum-transaction/std", "pallet-evm/std", "pallet-evm-precompile-dispatch/std", "pallet-evm-chain-id/std", @@ -260,6 +267,7 @@ std = [ "fp-rpc/std", "fp-self-contained/std", "moonbeam-relay-encoder/std", + "getrandom/std", ] runtime-benchmarks = [ @@ -281,6 +289,7 @@ runtime-benchmarks = [ "pallet-crowdloan-reward/runtime-benchmarks", "pallet-data-collector/runtime-benchmarks", "pallet-ethereum/runtime-benchmarks", + "pallet-ethereum-transaction/runtime-benchmarks", "pallet-evm/runtime-benchmarks", "pallet-fees/runtime-benchmarks", "pallet-interest-accrual/runtime-benchmarks", @@ -308,6 +317,8 @@ runtime-benchmarks = [ "orml-xtokens/runtime-benchmarks", "pallet-bridge/runtime-benchmarks", "pallet-connectors/runtime-benchmarks", + "pallet-connectors-gateway/runtime-benchmarks", + "connectors-gateway-routers/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", "pallet-elections-phragmen/runtime-benchmarks", "pallet-identity/runtime-benchmarks", @@ -329,12 +340,14 @@ runtime-benchmarks = [ ] try-runtime = [ - "pallet-connectors/try-runtime", "cfg-primitives/try-runtime", "cfg-traits/try-runtime", "cfg-primitives/try-runtime", "cfg-traits/try-runtime", "cfg-types/try-runtime", + "pallet-connectors/try-runtime", + "pallet-connectors-gateway/try-runtime", + "connectors-gateway-routers/try-runtime", "pallet-interest-accrual/try-runtime", "pallet-investments/try-runtime", "pallet-keystore/try-runtime", @@ -404,6 +417,7 @@ try-runtime = [ "pallet-evm-chain-id/try-runtime", "pallet-base-fee/try-runtime", "pallet-ethereum/try-runtime", + "pallet-ethereum-transaction/try-runtime", "fp-self-contained/try-runtime", "runtime-common/try-runtime", "sp-runtime/try-runtime", diff --git a/runtime/development/src/connectors.rs b/runtime/development/src/connectors.rs new file mode 100644 index 0000000000..77cd46d57b --- /dev/null +++ b/runtime/development/src/connectors.rs @@ -0,0 +1,38 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; +use cfg_types::{domain_address::Domain, fixed_point::Rate}; +use frame_support::parameter_types; +use frame_system::EnsureRoot; + +use super::{Runtime, RuntimeEvent, RuntimeOrigin}; +use crate::Connectors; + +type ConnectorsMessage = pallet_connectors::Message; + +parameter_types! { + // TODO(cdamian): Double-check these. + pub const MaxIncomingMessageSize: u32 = 1024; +} + +impl pallet_connectors_gateway::Config for Runtime { + type AdminOrigin = EnsureRoot; + type InboundQueue = Connectors; + type LocalOrigin = pallet_connectors_gateway::EnsureLocal; + type MaxIncomingMessageSize = MaxIncomingMessageSize; + type Message = ConnectorsMessage; + type Router = connectors_gateway_routers::DomainRouter; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type WeightInfo = (); +} diff --git a/runtime/development/src/evm.rs b/runtime/development/src/evm.rs index b1c1fe2e11..5fd5af71c9 100644 --- a/runtime/development/src/evm.rs +++ b/runtime/development/src/evm.rs @@ -84,3 +84,7 @@ impl pallet_ethereum::Config for crate::Runtime { type RuntimeEvent = crate::RuntimeEvent; type StateRoot = pallet_ethereum::IntermediateStateRoot; } + +impl pallet_ethereum_transaction::Config for crate::Runtime { + type RuntimeEvent = crate::RuntimeEvent; +} diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 3dc53efd9e..e797ff0daf 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -30,7 +30,6 @@ use cfg_traits::{ }; use cfg_types::{ consts::pools::*, - domain_address::Domain, fee_keys::FeeKey, fixed_point::Rate, ids::PRICE_ORACLE_PALLET_ID, @@ -73,7 +72,7 @@ use orml_traits::{currency::MutationHooks, parameter_type_with_key}; use pallet_anchors::AnchorData; pub use pallet_balances::Call as BalancesCall; use pallet_collective::EnsureMember; -use pallet_ethereum::{Call::transact, Transaction as EthereumTransaction}; +use pallet_ethereum::{Call::transact, Transaction as EthTransaction}; use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use pallet_investments::OrderType; use pallet_pool_system::{ @@ -96,7 +95,7 @@ use runtime_common::{ }; use scale_info::TypeInfo; use sp_api::impl_runtime_apis; -use sp_core::{OpaqueMetadata, H160, H256, U256}; +use sp_core::{Get, OpaqueMetadata, H160, H256, U256}; use sp_inherents::{CheckInherentsResult, InherentData}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; @@ -117,10 +116,16 @@ use static_assertions::const_assert; use xcm_executor::XcmExecutor; use xcm_primitives::{UtilityAvailableCalls, UtilityEncodeCall}; +use crate::xcm::{ + BaseXcmWeight, FungiblesTransactor, SelfLocation, UniversalLocation, XcmConfig, + XcmOriginToTransactDispatchOrigin, XcmRouter, +}; + pub mod evm; mod weights; pub mod xcm; -pub use crate::xcm::*; + +pub mod connectors; // Make the WASM binary available. #[cfg(feature = "std")] @@ -704,11 +709,20 @@ impl pallet_elections_phragmen::Config for Runtime { type WeightInfo = pallet_elections_phragmen::weights::SubstrateWeight; } +#[cfg(feature = "instant-voting")] +parameter_types! { + pub const InstantAllowed: bool = true; +} + +#[cfg(not(feature = "instant-voting"))] +parameter_types! { + pub const InstantAllowed: bool = false; +} + parameter_types! { pub const LaunchPeriod: BlockNumber = 7 * DAYS; pub const VotingPeriod: BlockNumber = 7 * DAYS; pub const FastTrackVotingPeriod: BlockNumber = 3 * HOURS; - pub const InstantAllowed: bool = false; pub const MinimumDeposit: Balance = 10 * CFG; pub const EnactmentPeriod: BlockNumber = 8 * DAYS; pub const CooloffPeriod: BlockNumber = 7 * DAYS; @@ -1561,22 +1575,6 @@ impl orml_asset_registry::Config for Runtime { type WeightInfo = (); } -pub struct DummyOutboundQueue; - -impl cfg_traits::connectors::OutboundQueue for DummyOutboundQueue { - type Destination = Domain; - type Message = pallet_connectors::MessageOf; - type Sender = AccountId; - - fn submit( - _sender: AccountId, - _destination: Domain, - _msg: pallet_connectors::MessageOf, - ) -> DispatchResult { - Ok(()) - } -} - impl pallet_connectors::Config for Runtime { type AccountConverter = AccountConverter; type AdminOrigin = EnsureRoot; @@ -1585,7 +1583,7 @@ impl pallet_connectors::Config for Runtime { type CurrencyId = CurrencyId; type ForeignInvestment = Investments; type GeneralCurrencyPrefix = cfg_primitives::connectors::GeneralCurrencyPrefix; - type OutboundQueue = DummyOutboundQueue; + type OutboundQueue = ConnectorsGateway; type Permission = Permissions; type PoolId = PoolId; type PoolInspect = PoolSystem; @@ -1911,6 +1909,7 @@ construct_runtime!( TransferAllowList: pallet_transfer_allowlist::{Pallet, Call, Storage, Event} = 112, PriceCollector: pallet_data_collector::{Pallet, Storage} = 113, GapRewardMechanism: pallet_rewards::mechanism::gap = 114, + ConnectorsGateway: pallet_connectors_gateway::{Pallet, Call, Storage, Event, Origin } = 115, // XCM XcmpQueue: cumulus_pallet_xcmp_queue::{Pallet, Call, Storage, Event} = 120, @@ -1933,6 +1932,7 @@ construct_runtime!( EVMChainId: pallet_evm_chain_id::{Pallet, Config, Storage} = 161, BaseFee: pallet_base_fee::{Pallet, Call, Config, Storage, Event} = 162, Ethereum: pallet_ethereum::{Pallet, Config, Call, Storage, Event, Origin} = 163, + EthereumTransaction: pallet_ethereum_transaction::{Pallet, Storage, Event} = 164, // migration pallet Migration: pallet_migration_manager::{Pallet, Call, Storage, Event} = 199, @@ -2407,11 +2407,11 @@ impl_runtime_apis! { fn extrinsic_filter( xts: Vec<::Extrinsic>, - ) -> Vec { + ) -> Vec { xts.into_iter().filter_map(|xt| match xt.0.function { RuntimeCall::Ethereum(transact { transaction }) => Some(transaction), _ => None - }).collect::>() + }).collect::>() } fn elasticity() -> Option { @@ -2422,7 +2422,7 @@ impl_runtime_apis! { } impl fp_rpc::ConvertTransactionRuntimeApi for Runtime { - fn convert_transaction(transaction: EthereumTransaction) -> ::Extrinsic { + fn convert_transaction(transaction: EthTransaction) -> ::Extrinsic { UncheckedExtrinsic::new_unsigned( pallet_ethereum::Call::::transact { transaction }.into(), ) diff --git a/runtime/integration-tests/Cargo.toml b/runtime/integration-tests/Cargo.toml index bb3691a869..5a3829a21a 100644 --- a/runtime/integration-tests/Cargo.toml +++ b/runtime/integration-tests/Cargo.toml @@ -22,6 +22,9 @@ frame-support = { git = "https://github.com/paritytech/substrate", branch = "pol frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } pallet-aura = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +pallet-collective = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +pallet-democracy = { git = "https://github.com/paritytech//substrate", rev = "bcff60a227d455d95b4712b6cb356ce56b1ff672" } +pallet-preimage = { git = "https://github.com/paritytech//substrate", rev = "bcff60a227d455d95b4712b6cb356ce56b1ff672" } pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } pallet-uniques = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } @@ -78,13 +81,24 @@ development-runtime = { path = "../development" } runtime-common = { path = "../common" } [dev-dependencies] +getrandom = { version = "0.2", features = ["js"] } hex = { version = "0.4.3", default_features = false } +rand = { version = "0.8.5" } cfg-traits = { path = "../../libs/traits" } cfg-types = { path = "../../libs/types" } cfg-utils = { path = "../../libs/utils" } + +ethereum = { version = "0.14.0", default-features = false } + +pallet-evm = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } +pallet-evm-chain-id = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" } + +connectors-gateway-routers = { path = "../../pallets/connectors-gateway/connectors-gateway-routers" } pallet-block-rewards = { path = "../../pallets/block-rewards" } pallet-connectors = { path = "../../pallets/connectors" } +pallet-connectors-gateway = { path = "../../pallets/connectors-gateway" } +pallet-ethereum-transaction = { path = "../../pallets/ethereum-transaction" } pallet-investments = { path = "../../pallets/investments" } pallet-loans = { path = "../../pallets/loans" } pallet-permissions = { path = "../../pallets/permissions" } @@ -93,6 +107,7 @@ pallet-pool-system = { path = "../../pallets/pool-system" } pallet-rewards = { path = "../../pallets/rewards" } pallet-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +pallet-xcm-transactor = { git = "https://github.com/PureStake/moonbeam", default-features = false, rev = "00b3e3d97806e889b02e1bcb4b69e65433dd805d" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } sp-std = { git = "https://github.com/paritytech/substrate", default-features = true, branch = "polkadot-v0.9.38" } xcm-executor = { git = "https://github.com/paritytech/polkadot", default-features = true, branch = "release-v0.9.38" } @@ -100,7 +115,10 @@ xcm-executor = { git = "https://github.com/paritytech/polkadot", default-feature pallet-collator-selection = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.38" } [features] -default = ["runtime-development"] +default = [ + "runtime-development", + "development-runtime/instant-voting", +] fast-runtime = ["development-runtime/fast-runtime"] std = [ "altair-runtime/std", @@ -144,6 +162,7 @@ std = [ "sp-runtime/std", "sp-tracing/std", "xcm/std", + "getrandom/std", ] runtime-benchmarks = [ diff --git a/runtime/integration-tests/src/connectors_gateway/gateway.rs b/runtime/integration-tests/src/connectors_gateway/gateway.rs new file mode 100644 index 0000000000..eaeaead99a --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/gateway.rs @@ -0,0 +1,267 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{Balance, PoolId, TrancheId}; +use cfg_traits::connectors::Codec; +use cfg_types::{ + domain_address::{Domain, DomainAddress}, + fixed_point::Rate, + tokens::{CurrencyId, CustomMetadata}, +}; +use connectors_gateway_routers::{ + axelar_evm::AxelarEVMRouter, ethereum_xcm::EthereumXCMRouter, DomainRouter, EVMChain, + EVMDomain, FeeValues, XcmDomain, XcmTransactInfo, +}; +use development_runtime::xcm::CurrencyIdConvert; +use frame_support::{ + assert_noop, assert_ok, + dispatch::{GetDispatchInfo, Pays}, + weights::Weight, +}; +use fudge::primitives::Chain; +use orml_traits::asset_registry::AssetMetadata; +use pallet_connectors::Message; +use pallet_connectors_gateway::GatewayOrigin; +use pallet_democracy::{AccountVote, Conviction, ReferendumIndex, Vote, VoteThreshold}; +use sp_core::{bounded::BoundedVec, bounded_vec, H160, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, Convert, Hash}, + Storage, +}; +use tokio::runtime::Handle; +use xcm::{ + latest::{Junction, Junctions, MultiLocation}, + VersionedMultiLocation, +}; + +use crate::{ + chain::centrifuge::{ + AccountId, CouncilCollective, FastTrackVotingPeriod, MinimumDeposit, Runtime, RuntimeCall, + RuntimeEvent, PARA_ID, + }, + utils::{ + accounts::Keyring, + collective::{collective_close, collective_propose, collective_vote}, + connectors_gateway::{add_connector, remove_connector, set_domain_router}, + democracy::{democracy_vote, execute_via_democracy, external_propose_majority, fast_track}, + env::{ChainState, EventRange}, + preimage::note_preimage, + *, + }, +}; + +fn get_council_members() -> Vec { + vec![Keyring::Alice, Keyring::Bob, Keyring::Charlie] +} + +#[tokio::test] +async fn set_router() { + let mut env = { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + genesis::council_members::(get_council_members(), &mut genesis); + env::test_env_with_centrifuge_storage(Handle::current(), genesis) + }; + + let test_domain = Domain::EVM(1); + + let xcm_domain_location = MultiLocation { + parents: 0, + interior: Junctions::X1(Junction::Parachain(456)), + }; + + let currency_id = CurrencyId::ForeignAsset(1); + let currency_location = MultiLocation { + parents: 0, + interior: Junctions::X1(Junction::Parachain(123)), + }; + + let currency_meta = AssetMetadata:: { + decimals: 18, + name: "Test".into(), + symbol: "TST".into(), + existential_deposit: 1_000_000, + location: Some(VersionedMultiLocation::V3(currency_location)), + additional: Default::default(), + }; + + let xcm_domain = XcmDomain { + location: Box::new(xcm_domain_location.clone().into_versioned()), + ethereum_xcm_transact_call_index: bounded_vec![0], + contract_address: H160::from_slice(rand::random::<[u8; 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, + fee_per_second: 1u128, + fee_asset_location: Box::new(currency_location.clone().into_versioned()), + }; + + let ethereum_xcm_router = EthereumXCMRouter:: { + xcm_domain: xcm_domain, + _marker: Default::default(), + }; + + let test_router = DomainRouter::::EthereumXCM(ethereum_xcm_router); + + let set_domain_router_call = set_domain_router(test_domain.clone(), test_router.clone()); + + let council_threshold = 2; + let voting_period = 3; + + execute_via_democracy( + &mut env, + get_council_members(), + set_domain_router_call, + council_threshold, + voting_period, + 0, + 0, + ); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + voting_period + 1, + RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::DomainRouterSet { + domain, + router, + }) if [*domain == test_domain && *router == test_router], + ); +} + +#[tokio::test] +async fn add_remove_connectors() { + let mut env = { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + genesis::council_members::(get_council_members(), &mut genesis); + env::test_env_with_centrifuge_storage(Handle::current(), genesis) + }; + + let test_connector = DomainAddress::EVM { + 0: 1, + 1: H160::random().0, + }; + + let add_connector_call = add_connector(test_connector.clone()); + + let council_threshold = 2; + let voting_period = 3; + + let (prop_index, ref_index) = execute_via_democracy( + &mut env, + get_council_members(), + add_connector_call, + council_threshold, + voting_period, + 0, + 0, + ); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + voting_period + 1, + RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::ConnectorAdded { + connector, + }) if [*connector == test_connector], + ); + + let remove_connector_call = remove_connector(test_connector.clone()); + + execute_via_democracy( + &mut env, + get_council_members(), + remove_connector_call, + council_threshold, + voting_period, + prop_index, + ref_index, + ); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + voting_period + 1, + RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::ConnectorRemoved { + connector, + }) if [*connector == test_connector], + ); +} + +#[tokio::test] +async fn process_msg() { + let mut env = { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + genesis::council_members::(get_council_members(), &mut genesis); + env::test_env_with_centrifuge_storage(Handle::current(), genesis) + }; + + let test_connector = DomainAddress::EVM { + 0: 1, + 1: H160::random().0, + }; + + let add_connector_call = add_connector(test_connector.clone()); + + let council_threshold = 2; + let voting_period = 3; + + let (prop_index, ref_index) = execute_via_democracy( + &mut env, + get_council_members(), + add_connector_call, + council_threshold, + voting_period, + 0, + 0, + ); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + voting_period + 1, + RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::ConnectorAdded { + connector, + }) if [*connector == test_connector], + ); + + let msg = Message::::AddPool { pool_id: 123 }; + + let encoded_msg = msg.serialize(); + + let gateway_msg = BoundedVec::< + u8, + ::MaxIncomingMessageSize, + >::try_from(encoded_msg) + .unwrap(); + + env.with_state(Chain::Para(PARA_ID), || { + assert_noop!( + pallet_connectors_gateway::Pallet::::process_msg( + GatewayOrigin::Local(test_connector).into(), + gateway_msg, + ), + pallet_connectors::Error::::InvalidIncomingMessage, + ); + }) + .unwrap(); +} diff --git a/runtime/integration-tests/src/connectors_gateway/mod.rs b/runtime/integration-tests/src/connectors_gateway/mod.rs new file mode 100644 index 0000000000..8e284457eb --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +mod gateway; +mod routers; diff --git a/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/mod.rs b/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/mod.rs new file mode 100644 index 0000000000..96436bdd62 --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +mod router; diff --git a/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/router.rs b/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/router.rs new file mode 100644 index 0000000000..da947fef2a --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/axelar_evm/router.rs @@ -0,0 +1,138 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{Balance, PoolId, TrancheId, CFG}; +use cfg_traits::connectors::OutboundQueue; +use cfg_types::{domain_address::Domain, fixed_point::Rate}; +use connectors_gateway_routers::{ + axelar_evm::AxelarEVMRouter, DomainRouter, EVMChain, EVMDomain, FeeValues, +}; +use frame_support::{assert_ok, dispatch::RawOrigin, traits::fungible::Mutate}; +use fudge::primitives::Chain; +use pallet_connectors::Message; +use pallet_evm::FeeCalculator; +use runtime_common::account_conversion::AccountConverter; +use sp_core::{crypto::AccountId32, storage::Storage, Get, H160, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use tokio::runtime::Handle; + +use crate::{ + chain::centrifuge::{ + Balances, ConnectorsGateway, CouncilCollective, Runtime, RuntimeEvent, RuntimeOrigin, + PARA_ID, + }, + utils::{ + accounts::Keyring, + connectors_gateway::set_domain_router, + democracy::execute_via_democracy, + env, + env::{ChainState, EventRange, TestEnv}, + evm::mint_balance_into_derived_account, + genesis, + }, +}; + +fn get_council_members() -> Vec { + vec![Keyring::Alice, Keyring::Bob, Keyring::Charlie] +} + +#[tokio::test] +async fn call() { + let mut env = { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + genesis::council_members::(get_council_members(), &mut genesis); + env::test_env_with_centrifuge_storage(Handle::current(), genesis) + }; + + let test_domain = Domain::EVM(1); + + let axelar_contract_address = H160::from_low_u64_be(1); + let axelar_contract_code: Vec = vec![0, 0, 0]; + let axelar_contract_hash = BlakeTwo256::hash_of(&axelar_contract_code); + let connectors_contract_address = H160::from_low_u64_be(2); + + env.with_mut_state(Chain::Para(PARA_ID), || { + pallet_evm::AccountCodes::::insert(axelar_contract_address, axelar_contract_code) + }) + .unwrap(); + + let transaction_call_cost = env + .with_state(Chain::Para(PARA_ID), || { + ::config().gas_transaction_call + }) + .unwrap(); + + let evm_domain = EVMDomain { + chain: EVMChain::Ethereum, + axelar_contract_address, + axelar_contract_hash, + connectors_contract_address, + fee_values: FeeValues { + value: U256::from(10), + gas_limit: U256::from(transaction_call_cost + 10_000), + gas_price: U256::from(10), + }, + }; + + let axelar_evm_router = AxelarEVMRouter:: { + domain: evm_domain, + _marker: Default::default(), + }; + + let test_router = DomainRouter::::AxelarEVM(axelar_evm_router); + + let set_domain_router_call = set_domain_router(test_domain.clone(), test_router.clone()); + + let council_threshold = 2; + let voting_period = 3; + + execute_via_democracy( + &mut env, + get_council_members(), + set_domain_router_call, + council_threshold, + voting_period, + 0, + 0, + ); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + voting_period + 1, + RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::DomainRouterSet { + domain, + router, + }) if [*domain == test_domain && *router == test_router], + ); + + let sender = Keyring::Alice.to_account_id(); + let sender_h160: H160 = + H160::from_slice(&>::as_ref(&sender)[0..20]); + + // Note how both the target address and the sender need to have some balance. + mint_balance_into_derived_account(&mut env, axelar_contract_address, 1_000_000_000 * CFG); + mint_balance_into_derived_account(&mut env, sender_h160, 1_000_000 * CFG); + + let msg = Message::::Transfer { + currency: 0, + sender: Keyring::Alice.to_account_id().into(), + receiver: Keyring::Bob.to_account_id().into(), + amount: 1_000u128, + }; + + assert_ok!(env.with_state(Chain::Para(PARA_ID), || { + ::submit(test_domain, sender, msg).unwrap() + })); +} diff --git a/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/mod.rs b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/mod.rs new file mode 100644 index 0000000000..a283224f56 --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +mod router; +mod setup; +mod test_net; diff --git a/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/router.rs b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/router.rs new file mode 100644 index 0000000000..d57d621087 --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/router.rs @@ -0,0 +1,172 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use ::xcm::{ + latest::{Junction, Junction::*, Junctions::*, MultiLocation, NetworkId}, + prelude::{Parachain, X1, X2}, + VersionedMultiLocation, +}; +use cfg_primitives::{PoolId, TrancheId}; +use cfg_traits::connectors::OutboundQueue; +use cfg_types::{ + domain_address::Domain, + fixed_point::Rate, + tokens::{CrossChainTransferability, CurrencyId, CustomMetadata}, +}; +use connectors_gateway_routers::{ + ethereum_xcm::EthereumXCMRouter, DomainRouter, EVMChain, EVMDomain, FeeValues, XcmDomain, + XcmTransactInfo, +}; +use frame_support::{assert_noop, assert_ok}; +use hex::FromHex; +use orml_traits::{asset_registry::AssetMetadata, MultiCurrency}; +use pallet_connectors::Message; +use runtime_common::{xcm::general_key, xcm_fees::default_per_second}; +use sp_core::{bounded::BoundedVec, H160}; +use xcm_emulator::TestExt; + +use crate::{ + chain::centrifuge::{ + Balance, ConnectorsGateway, OrmlAssetRegistry, OrmlTokens, Runtime, RuntimeOrigin, + }, + connectors_gateway::routers::ethereum_xcm::{ + setup::{dollar, ALICE, BOB, CHARLIE, PARA_ID_MOONBEAM, TEST_DOMAIN}, + test_net::{Development, Moonbeam, RelayChain, TestNet}, + }, + utils::accounts::Keyring, +}; + +fn setup() { + let moonbeam_location = MultiLocation { + parents: 1, + interior: X1(Parachain(PARA_ID_MOONBEAM)), + }; + let moonbeam_native_token = MultiLocation { + parents: 1, + interior: X2(Parachain(PARA_ID_MOONBEAM), general_key(&[0, 1])), + }; + + /// Register Moonbeam's native token + let glmr_currency_id = CurrencyId::ForeignAsset(1); + let meta: AssetMetadata = AssetMetadata { + decimals: 18, + name: "Glimmer".into(), + symbol: "GLMR".into(), + existential_deposit: 1_000_000, + location: Some(VersionedMultiLocation::V3(moonbeam_native_token)), + additional: CustomMetadata { + transferability: CrossChainTransferability::Xcm(Default::default()), + ..CustomMetadata::default() + }, + }; + + let ethereum_xcm_router = EthereumXCMRouter:: { + xcm_domain: XcmDomain { + location: Box::new( + moonbeam_location + .try_into() + .expect("Bad xcm domain location"), + ), + ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), + contract_address: H160::from( + <[u8; 20]>::from_hex("cE0Cb9BB900dfD0D378393A041f3abAb6B182882") + .expect("Invalid address"), + ), + max_gas_limit: 700_000, + transact_info: XcmTransactInfo { + transact_extra_weight: 1.into(), + max_weight: 8_000_000_000_000_000.into(), + transact_extra_weight_signed: Some(3.into()), + }, + fee_currency: glmr_currency_id, + fee_per_second: default_per_second(18), + fee_asset_location: Box::new( + moonbeam_native_token + .try_into() + .expect("Bad xcm fee asset location"), + ), + }, + _marker: Default::default(), + }; + + let domain_router = DomainRouter::EthereumXCM(ethereum_xcm_router); + + assert_ok!(ConnectorsGateway::set_domain_router( + RuntimeOrigin::root(), + TEST_DOMAIN, + domain_router, + )); + + assert_ok!(OrmlAssetRegistry::register_asset( + RuntimeOrigin::root(), + meta, + Some(glmr_currency_id) + )); + + // Give Alice and BOB enough glimmer to pay for fees + OrmlTokens::deposit(glmr_currency_id, &ALICE.into(), 10 * dollar(18)); + OrmlTokens::deposit(glmr_currency_id, &BOB.into(), 10 * dollar(18)); + + // We first need to register AUSD in the asset registry + let ausd_meta: AssetMetadata = AssetMetadata { + decimals: 12, + name: "Acala Dollar".into(), + symbol: "AUSD".into(), + existential_deposit: 1_000, + location: None, + additional: CustomMetadata { + transferability: CrossChainTransferability::Xcm(Default::default()), + ..CustomMetadata::default() + }, + }; + assert_ok!(OrmlAssetRegistry::register_asset( + RuntimeOrigin::root(), + ausd_meta, + Some(CurrencyId::AUSD) + )); +} + +#[test] +fn call() { + TestNet::reset(); + + Development::execute_with(|| { + setup(); + + let msg = Message::::Transfer { + currency: 0, + sender: ALICE.into(), + receiver: BOB.into(), + amount: 1_000u128, + }; + + assert_ok!(::submit( + TEST_DOMAIN, + ALICE.into(), + msg.clone(), + )); + + assert_noop!( + ::submit( + Domain::EVM(1285), + ALICE.into(), + msg.clone(), + ), + pallet_connectors_gateway::Error::::RouterNotFound, + ); + + assert_noop!( + ::submit(TEST_DOMAIN, CHARLIE.into(), msg), + pallet_xcm_transactor::Error::::UnableToWithdrawAsset, + ); + }); +} diff --git a/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/setup.rs b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/setup.rs new file mode 100644 index 0000000000..89b67cf49f --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/setup.rs @@ -0,0 +1,113 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{currency_decimals, parachains, Balance}; +use cfg_types::{ + domain_address::Domain, + tokens::{CurrencyId, CustomMetadata}, +}; +use frame_support::traits::GenesisBuild; +use orml_traits::asset_registry::AssetMetadata; + +use crate::chain::centrifuge::{AccountId, Runtime, RuntimeOrigin, System}; + +/// Accounts +pub const ALICE: [u8; 32] = [4u8; 32]; +pub const BOB: [u8; 32] = [5u8; 32]; +pub const CHARLIE: [u8; 32] = [6u8; 32]; +pub const TEST_DOMAIN: Domain = Domain::EVM(1284); + +/// A PARA ID used for a sibling parachain emulating Moonbeam. +/// It must be one that doesn't collide with any other in use. +pub const PARA_ID_MOONBEAM: u32 = 2023; + +pub struct ExtBuilder { + balances: Vec<(AccountId, CurrencyId, Balance)>, + parachain_id: u32, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![], + parachain_id: parachains::polkadot::centrifuge::ID, + } + } +} + +impl ExtBuilder { + pub fn balances(mut self, balances: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub fn parachain_id(mut self, parachain_id: u32) -> Self { + self.parachain_id = parachain_id; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let native_currency_id = development_runtime::NativeToken::get(); + pallet_balances::GenesisConfig:: { + balances: self + .balances + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id == native_currency_id) + .map(|(account_id, _, initial_balance)| (account_id, initial_balance)) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .balances + .into_iter() + .filter(|(_, currency_id, _)| *currency_id != native_currency_id) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + >::assimilate_storage( + ¶chain_info::GenesisConfig { + parachain_id: self.parachain_id.into(), + }, + &mut t, + ) + .unwrap(); + + >::assimilate_storage( + &pallet_xcm::GenesisConfig { + safe_xcm_version: Some(2), + }, + &mut t, + ) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub fn cfg(amount: Balance) -> Balance { + amount * dollar(currency_decimals::NATIVE) +} + +pub fn dollar(decimals: u32) -> Balance { + 10u128.saturating_pow(decimals) +} diff --git a/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/test_net.rs b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/test_net.rs new file mode 100644 index 0000000000..2c19ceab74 --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/ethereum_xcm/test_net.rs @@ -0,0 +1,155 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{parachains, AccountId}; +use cfg_types::tokens::CurrencyId; +use cumulus_primitives_core::ParaId; +use frame_support::{traits::GenesisBuild, weights::Weight}; +use polkadot_primitives::v2::{BlockNumber, MAX_CODE_SIZE, MAX_POV_SIZE}; +use polkadot_runtime_parachains::configuration::HostConfiguration; +use sp_runtime::traits::AccountIdConversion; +use xcm_emulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain}; + +use super::setup::{cfg, ExtBuilder, ALICE, BOB, PARA_ID_MOONBEAM}; + +decl_test_relay_chain! { + pub struct RelayChain { + Runtime = polkadot_runtime::Runtime, + XcmConfig = polkadot_runtime::xcm_config::XcmConfig, + new_ext = relay_ext(), + } +} + +decl_test_parachain! { + pub struct Development { + Runtime = development_runtime::Runtime, + RuntimeOrigin = development_runtime::RuntimeOrigin, + XcmpMessageHandler = development_runtime::XcmpQueue, + DmpMessageHandler = development_runtime::DmpQueue, + new_ext = para_ext(parachains::polkadot::centrifuge::ID), + } +} + +decl_test_parachain! { + pub struct Moonbeam { + Runtime = development_runtime::Runtime, + RuntimeOrigin = development_runtime::RuntimeOrigin, + XcmpMessageHandler = development_runtime::XcmpQueue, + DmpMessageHandler = development_runtime::DmpQueue, + new_ext = para_ext(PARA_ID_MOONBEAM), + } +} + +decl_test_network! { + pub struct TestNet { + relay_chain = RelayChain, + parachains = vec![ + // N.B: Ideally, we could use the defined para id constants but doing so + // fails with: "error: arbitrary expressions aren't allowed in patterns" + + // Be sure to use `parachains::polkadot::centrifuge::ID` + (2031, Development), + // Be sure to use `PARA_ID_MOONBEAM` + (2023, Moonbeam), + ], + } +} + +pub fn relay_ext() -> sp_io::TestExternalities { + use polkadot_runtime::{Runtime, System}; + + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (AccountId::from(ALICE), cfg(2002)), + ( + ParaId::from(parachains::polkadot::centrifuge::ID).into_account_truncating(), + cfg(7), + ), + ( + ParaId::from(PARA_ID_MOONBEAM).into_account_truncating(), + cfg(7), + ), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + polkadot_runtime_parachains::configuration::GenesisConfig:: { + config: default_parachains_host_configuration(), + } + .assimilate_storage(&mut t) + .unwrap(); + + >::assimilate_storage( + &pallet_xcm::GenesisConfig { + safe_xcm_version: Some(2), + }, + &mut t, + ) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn para_ext(parachain_id: u32) -> sp_io::TestExternalities { + ExtBuilder::default() + .balances(vec![ + (AccountId::from(ALICE), CurrencyId::Native, cfg(10_000)), + (AccountId::from(BOB), CurrencyId::Native, cfg(10_000)), + ]) + .parachain_id(parachain_id) + .build() +} + +fn default_parachains_host_configuration() -> HostConfiguration { + HostConfiguration { + minimum_validation_upgrade_delay: 5, + validation_upgrade_cooldown: 5u32, + validation_upgrade_delay: 5, + code_retention_period: 1200, + max_code_size: MAX_CODE_SIZE, + max_pov_size: MAX_POV_SIZE, + max_head_data_size: 32 * 1024, + group_rotation_frequency: 20, + chain_availability_period: 4, + thread_availability_period: 4, + max_upward_queue_count: 8, + max_upward_queue_size: 1024 * 1024, + max_downward_message_size: 1024, + ump_service_total_weight: Weight::from_ref_time(4 * 1_000_000_000), + max_upward_message_size: 50 * 1024, + max_upward_message_num_per_candidate: 5, + hrmp_sender_deposit: 0, + hrmp_recipient_deposit: 0, + hrmp_channel_max_capacity: 8, + hrmp_channel_max_total_size: 8 * 1024, + hrmp_max_parachain_inbound_channels: 4, + hrmp_max_parathread_inbound_channels: 4, + hrmp_channel_max_message_size: 1024 * 1024, + hrmp_max_parachain_outbound_channels: 4, + hrmp_max_parathread_outbound_channels: 4, + hrmp_max_message_num_per_candidate: 5, + dispute_period: 6, + no_show_slots: 2, + n_delay_tranches: 25, + needed_approvals: 2, + relay_vrf_modulo_samples: 2, + zeroth_delay_tranche_width: 0, + ..Default::default() + } +} diff --git a/runtime/integration-tests/src/connectors_gateway/routers/mod.rs b/runtime/integration-tests/src/connectors_gateway/routers/mod.rs new file mode 100644 index 0000000000..a8be751d8f --- /dev/null +++ b/runtime/integration-tests/src/connectors_gateway/routers/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +mod axelar_evm; +mod ethereum_xcm; diff --git a/runtime/integration-tests/src/ethereum_transaction/mod.rs b/runtime/integration-tests/src/ethereum_transaction/mod.rs new file mode 100644 index 0000000000..0ca88fbd73 --- /dev/null +++ b/runtime/integration-tests/src/ethereum_transaction/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +mod pallet; diff --git a/runtime/integration-tests/src/ethereum_transaction/pallet.rs b/runtime/integration-tests/src/ethereum_transaction/pallet.rs new file mode 100644 index 0000000000..439414198e --- /dev/null +++ b/runtime/integration-tests/src/ethereum_transaction/pallet.rs @@ -0,0 +1,125 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::CFG; +use cfg_traits::ethereum::EthereumTransactor; +use ethereum::{LegacyTransaction, TransactionAction, TransactionSignature, TransactionV2}; +use frame_support::{assert_err, dispatch::RawOrigin}; +use fudge::primitives::Chain; +use pallet_evm::FeeCalculator; +use runtime_common::account_conversion::AccountConverter; +use sp_core::{Get, H160, U256}; +use tokio::runtime::Handle; + +use crate::{ + chain::centrifuge::{ + AccountId, CouncilCollective, FastTrackVotingPeriod, MinimumDeposit, Runtime, RuntimeCall, + RuntimeEvent, PARA_ID, + }, + ethereum_transaction::pallet, + utils::{ + env, + env::{ChainState, EventRange, TestEnv}, + evm::{deploy_contract, mint_balance_into_derived_account}, + }, +}; + +// From: +// https://github.com/moonbeam-foundation/frontier/blob/moonbeam-polkadot-v0.9.38/frame/ethereum/src/tests/legacy.rs#L279 +// +// pragma solidity ^0.6.6; +// contract Test { +// function foo() external pure returns (bool) { +// return true; +// } +// function bar() external pure { +// require(false, "error_msg"); +// } +// } +pub const TEST_CONTRACT_CODE: &str = "608060405234801561001057600080fd5b50610113806100206000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c8063c2985578146037578063febb0f7e146057575b600080fd5b603d605f565b604051808215151515815260200191505060405180910390f35b605d6068565b005b60006001905090565b600060db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260098152602001807f6572726f725f6d7367000000000000000000000000000000000000000000000081525060200191505060405180910390fd5b56fea2646970667358221220fde68a3968e0e99b16fabf9b2997a78218b32214031f8e07e2c502daf603a69e64736f6c63430006060033"; + +#[tokio::test] +async fn call() { + let mut env = env::test_env_default(Handle::current()); + + env.evolve().unwrap(); + + let contract_address = H160::from_low_u64_be(1); + + mint_balance_into_derived_account(&mut env, contract_address, 1_000_000 * CFG); + deploy_contract( + &mut env, + contract_address, + hex::decode(TEST_CONTRACT_CODE).unwrap(), + ); + + let sender_address = H160::from_low_u64_be(2); + mint_balance_into_derived_account(&mut env, sender_address, 1_000_000 * CFG); + + // From: + // https://github.com/moonbeam-foundation/frontier/blob/moonbeam-polkadot-v0.9.38/frame/ethereum/src/tests/legacy.rs#L297 + // let contract_address = + let foo = hex::decode("c2985578").unwrap(); + let bar = hex::decode("febb0f7e").unwrap(); + + let contract_address = env + .with_state(Chain::Para(PARA_ID), || { + for (address, code) in pallet_evm::AccountCodes::::iter() { + if code.len() > 0 { + return Ok(address); + } + } + + return Err(()); + }) + .unwrap() + .unwrap(); + + env.with_mut_state(Chain::Para(PARA_ID), || { + pallet_ethereum_transaction::Pallet::::call( + sender_address, + contract_address, + foo.as_slice(), + U256::zero(), + U256::from(1), + U256::from(0x100000), + ) + .unwrap(); + }) + .unwrap(); + + env::evolve_until_event_is_found!( + env, + Chain::Para(PARA_ID), + RuntimeEvent, + 5, + RuntimeEvent::EthereumTransaction(pallet_ethereum_transaction::Event::Executed { + value, + .. + }) if [ hex::encode(value) == "0000000000000000000000000000000000000000000000000000000000000001" ], + ); + + // This should be OK despite the error returned by the contract. + env.with_mut_state(Chain::Para(PARA_ID), || { + let res = pallet_ethereum_transaction::Pallet::::call( + sender_address, + contract_address, + bar.as_slice(), + U256::zero(), + U256::from(1), + U256::from(0x100000), + ); + + assert!(res.is_err()); + }) + .unwrap(); +} diff --git a/runtime/integration-tests/src/lib.rs b/runtime/integration-tests/src/lib.rs index ee666c27e9..5ce8dcb763 100644 --- a/runtime/integration-tests/src/lib.rs +++ b/runtime/integration-tests/src/lib.rs @@ -14,6 +14,8 @@ #![cfg(test)] #![allow(unused)] +mod connectors_gateway; +mod ethereum_transaction; mod pools; mod rewards; mod runtime_apis; diff --git a/runtime/integration-tests/src/utils/collective.rs b/runtime/integration-tests/src/utils/collective.rs new file mode 100644 index 0000000000..b10a86c334 --- /dev/null +++ b/runtime/integration-tests/src/utils/collective.rs @@ -0,0 +1,52 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use codec::Encode; +use frame_support::{traits::Len, weights::Weight}; +use pallet_collective::{Call as CouncilCall, MemberCount, ProposalIndex}; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +use crate::chain::centrifuge::{Runtime, RuntimeCall}; + +pub fn collective_propose(proposal: RuntimeCall, threshold: MemberCount) -> RuntimeCall { + let proposal_len = proposal.encode().len(); + let hash = BlakeTwo256::hash_of(&proposal); + + RuntimeCall::Council(CouncilCall::propose { + threshold, + proposal: Box::new(proposal), + length_bound: proposal_len as u32, + }) +} + +pub fn collective_vote(proposal: H256, index: ProposalIndex, approve: bool) -> RuntimeCall { + RuntimeCall::Council(CouncilCall::vote { + proposal, + index, + approve, + }) +} + +pub fn collective_close( + proposal_hash: H256, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, +) -> RuntimeCall { + RuntimeCall::Council(CouncilCall::close { + proposal_hash, + index, + proposal_weight_bound, + length_bound, + }) +} diff --git a/runtime/integration-tests/src/utils/connectors_gateway.rs b/runtime/integration-tests/src/utils/connectors_gateway.rs new file mode 100644 index 0000000000..8dd5b06e48 --- /dev/null +++ b/runtime/integration-tests/src/utils/connectors_gateway.rs @@ -0,0 +1,46 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; +use cfg_traits::connectors::Router; +use cfg_types::{ + domain_address::{Domain, DomainAddress}, + fixed_point::Rate, +}; +use connectors_gateway_routers::DomainRouter; +use development_runtime::connectors::MaxIncomingMessageSize; +use pallet_connectors::Message; +use pallet_connectors_gateway::Call as ConnectorsGatewayCall; +use sp_core::bounded::BoundedVec; + +use crate::chain::centrifuge::{Runtime, RuntimeCall}; + +pub fn set_domain_router(domain: Domain, router: DomainRouter) -> RuntimeCall { + RuntimeCall::ConnectorsGateway(ConnectorsGatewayCall::set_domain_router { domain, router }) +} + +pub fn add_connector(connector: DomainAddress) -> RuntimeCall { + RuntimeCall::ConnectorsGateway(ConnectorsGatewayCall::add_connector { connector }) +} + +pub fn remove_connector(connector: DomainAddress) -> RuntimeCall { + RuntimeCall::ConnectorsGateway(ConnectorsGatewayCall::remove_connector { connector }) +} + +pub fn process_msg(raw_msg: Vec) -> RuntimeCall { + let msg = BoundedVec::< + u8, + ::MaxIncomingMessageSize, + >::try_from(raw_msg) + .unwrap(); + RuntimeCall::ConnectorsGateway(ConnectorsGatewayCall::process_msg { msg }) +} diff --git a/runtime/integration-tests/src/utils/democracy.rs b/runtime/integration-tests/src/utils/democracy.rs new file mode 100644 index 0000000000..8720f40e23 --- /dev/null +++ b/runtime/integration-tests/src/utils/democracy.rs @@ -0,0 +1,287 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::Balance; +use chain::centrifuge::{ + BlockNumber, CouncilCollective, Runtime, RuntimeCall, RuntimeEvent, PARA_ID, +}; +use codec::Encode; +use frame_support::{dispatch::GetDispatchInfo, traits::Bounded}; +use fudge::primitives::Chain; +use pallet_collective::MemberCount; +use pallet_democracy::{ + AccountVote, Call as DemocracyCall, Conviction, PropIndex, ReferendumIndex, ReferendumInfo, + Vote, +}; +use sp_core::{blake2_256, H256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +use crate::{ + chain, + utils::{accounts::Keyring, collective::*, env, env::*, preimage::*}, +}; + +pub fn external_propose_majority(call: &RuntimeCall) -> RuntimeCall { + let hash = BlakeTwo256::hash_of(call); + + RuntimeCall::Democracy(DemocracyCall::external_propose_majority { + proposal: Bounded::Legacy { + hash, + dummy: Default::default(), + }, + }) +} + +pub fn fast_track( + proposal_hash: H256, + voting_period: BlockNumber, + delay: BlockNumber, +) -> RuntimeCall { + RuntimeCall::Democracy(DemocracyCall::fast_track { + proposal_hash, + voting_period, + delay, + }) +} + +pub fn democracy_vote(ref_index: ReferendumIndex, vote: AccountVote) -> RuntimeCall { + RuntimeCall::Democracy(DemocracyCall::vote { ref_index, vote }) +} + +pub fn execute_via_democracy( + test_env: &mut TestEnv, + council_members: Vec, + original_call: RuntimeCall, + council_threshold: MemberCount, + voting_period: BlockNumber, + starting_prop_index: PropIndex, + starting_ref_index: ReferendumIndex, +) -> (PropIndex, ReferendumIndex) { + let original_call_hash = BlakeTwo256::hash_of(&original_call); + + env::run!( + test_env, + Chain::Para(PARA_ID), + RuntimeCall, + ChainState::PoolEmpty, + council_members[0] => note_preimage(&original_call) + ); + + env::assert_events!( + test_env, + Chain::Para(PARA_ID), + RuntimeEvent, + EventRange::All, + RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], + RuntimeEvent::Preimage(pallet_preimage::Event::Noted{ hash }) if [*hash == original_call_hash], + ); + + let external_propose_majority_call = external_propose_majority(&original_call); + + execute_collective_proposal( + test_env, + &council_members, + external_propose_majority_call, + council_threshold, + starting_prop_index, + ); + + let fast_track_call = fast_track(original_call_hash, voting_period, 0); + + execute_collective_proposal( + test_env, + &council_members, + fast_track_call, + council_threshold, + starting_prop_index + 1, + ); + + let vote = AccountVote::::Standard { + vote: Vote { + aye: true, + conviction: Conviction::Locked2x, + }, + balance: 1_000_000u128, + }; + + execute_democracy_vote(test_env, &council_members, starting_ref_index, vote); + + (starting_prop_index + 2, starting_ref_index + 1) +} + +fn execute_democracy_vote( + test_env: &mut TestEnv, + voters: &Vec, + referendum_index: ReferendumIndex, + acc_vote: AccountVote, +) { + for acc in voters { + test_env.evolve().unwrap(); + + let ref_info = test_env + .with_state(Chain::Para(PARA_ID), || { + pallet_democracy::ReferendumInfoOf::::get(referendum_index).unwrap() + }) + .unwrap(); + + if let ReferendumInfo::Finished { .. } = ref_info { + // Referendum might be finished by the time all voters get to vote. + break; + } + + env::run!( + test_env, + Chain::Para(PARA_ID), + RuntimeCall, + ChainState::PoolEmpty, + *acc => democracy_vote(referendum_index, acc_vote) + ); + + env::assert_events!( + test_env, + Chain::Para(PARA_ID), + RuntimeEvent, + EventRange::All, + RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], + RuntimeEvent::Democracy(pallet_democracy::Event::Voted{ + voter, + ref_index, + vote, + }) if [ + *voter == acc.to_account_id() + && *ref_index == referendum_index + && *vote == acc_vote + ], + ) + } +} + +fn execute_collective_proposal( + test_env: &mut TestEnv, + council_members: &Vec, + proposal: RuntimeCall, + council_threshold: MemberCount, + prop_index: PropIndex, +) { + let prop_hash = BlakeTwo256::hash_of(&proposal); + + // Council proposal + + env::run!( + test_env, + Chain::Para(PARA_ID), + RuntimeCall, + ChainState::PoolEmpty, + council_members[0] => collective_propose(proposal.clone(), council_threshold) + ); + + env::assert_events!( + test_env, + Chain::Para(PARA_ID), + RuntimeEvent, + EventRange::All, + RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], + RuntimeEvent::Council(pallet_collective::Event::Proposed{ + account, + proposal_index, + proposal_hash, + threshold, + }) if [ + *account == council_members[0].to_account_id() + && *proposal_index == prop_index + && *proposal_hash == prop_hash + && *threshold == council_threshold + ], + ); + + // Council voting + + for (index, acc) in council_members.iter().enumerate() { + env::run!( + test_env, + Chain::Para(PARA_ID), + RuntimeCall, + ChainState::PoolEmpty, + *acc => collective_vote(prop_hash, prop_index, true) + ); + + env::assert_events!( + test_env, + Chain::Para(PARA_ID), + RuntimeEvent, + EventRange::All, + RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], + RuntimeEvent::Council(pallet_collective::Event::Voted{ + account, + proposal_hash, + voted, + yes, + no, + }) if [ + *account == acc.to_account_id() + && *proposal_hash == prop_hash + && *voted == true + && *yes == (index + 1) as u32 + && *no == 0 + ], + ) + } + + // Council closing + + let proposal_weight = test_env + .with_state(Chain::Para(PARA_ID), || { + let external_proposal = + pallet_collective::ProposalOf::::get(prop_hash) + .unwrap(); + + external_proposal.get_dispatch_info().weight + }) + .unwrap(); + + env::run!( + test_env, + Chain::Para(PARA_ID), + RuntimeCall, + ChainState::PoolEmpty, + council_members[0] => collective_close( + prop_hash, + prop_index, + proposal_weight.add(1), + (proposal.encoded_size() + 1) as u32, + ) + ); + + env::assert_events!( + test_env, + Chain::Para(PARA_ID), + RuntimeEvent, + EventRange::All, + RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], + RuntimeEvent::Council(pallet_collective::Event::Closed { + proposal_hash, + yes, + no, + }) if [ + *proposal_hash == prop_hash + && *yes == council_members.len() as u32 + && *no == 0 + ], + RuntimeEvent::Council(pallet_collective::Event::Approved{ + proposal_hash + }) if [ *proposal_hash == prop_hash], + RuntimeEvent::Council(pallet_collective::Event::Executed{ + proposal_hash, + result, + }) if [ *proposal_hash == prop_hash && result.is_ok()], + ); +} diff --git a/runtime/integration-tests/src/utils/env.rs b/runtime/integration-tests/src/utils/env.rs index 5df2c207fa..478a07bfe4 100644 --- a/runtime/integration-tests/src/utils/env.rs +++ b/runtime/integration-tests/src/utils/env.rs @@ -62,6 +62,94 @@ use crate::{ }; pub mod macros { + /// A macro that evolves the chain until the provided event and pattern are + /// encountered. + /// + /// Usage: + /// ```ignore + /// env::evolve_until_event!( + /// env, + /// Chain::Para(PARA_ID), + /// RuntimeEvent, + /// max_blocks, + /// RuntimeEvent::ConnectorsGateway(pallet_connectors_gateway::Event::DomainRouterSet { + /// domain, + /// router, + /// }) if [*domain == test_domain && *router == test_router], + /// ); + /// ``` + macro_rules! evolve_until_event_is_found { + ($env:expr, $chain:expr, $event:ty, $max_count:expr, $pattern:pat_param $(if $extra:tt)?, ) => {{ + use frame_support::assert_ok; + use frame_system::EventRecord as __hidden_EventRecord; + use sp_core::H256 as __hidden_H256; + use codec::Decode as _; + + use crate::utils::env::macros::{extra_counts, extra_guards}; + + let mut matched: Vec<$event> = Vec::new(); + + for _ in 0..$max_count { + let latest = $env + .centrifuge + .with_state(|| frame_system::Pallet::::block_number()) + .expect("Failed retrieving latest block"); + + if latest == 0 { + $env.evolve().unwrap(); + continue + } + + let scale_events = $env + .events($chain, EventRange::One(latest)) + .expect("Failed fetching events"); + + let events: Vec<$event> = scale_events + .into_iter() + .map(|scale_record| { + __hidden_EventRecord::<$event, __hidden_H256>::decode( + &mut scale_record.as_slice(), + ) + .expect("Decoding from chain data does not fail. qed") + }) + .map(|record| record.event) + .collect(); + + let matches = |event: &RuntimeEvent| { + match event { + $pattern $(if extra_guards!($extra))? => true, + _ => false + } + }; + + matched = events.clone(); + matched.retain(|event| matches(event)); + + if matched.len() > 0 { + break + } + + $env.evolve().unwrap(); + } + + let scale_events = $env.events($chain, EventRange::All).expect("Failed fetching events"); + let events: Vec<$event> = scale_events + .into_iter() + .map(|scale_record| __hidden_EventRecord::<$event, __hidden_H256>::decode(&mut scale_record.as_slice()) + .expect("Decoding from chain data does not fail. qed")) + .map(|record| record.event) + .collect(); + + assert!( + matched.len() == extra_counts!($pattern $(,$extra)?), + "events do not match the provided pattern - '{}'.\nMatched events: {:?}\nTotal events: {:?}\n", + stringify!($pattern $(,$extra)?), + matched, + events, + ); + }}; + } + /// A macro that helps checking whether a given list of events /// has been included in the given range of blocks. /// @@ -108,7 +196,13 @@ pub mod macros { let mut matched = events.clone(); matched.retain(|event| matches(event)); - assert!(matched.len() == extra_counts!($pattern $(,$extra)?)); + assert!( + matched.len() == extra_counts!($pattern $(,$extra)?), + "events do not match the provided pattern - '{}'.\nMatched events: {:?}\nTotal events: {:?}\n", + stringify!($pattern $(,$extra)?), + matched, + events, + ); )+ }}; @@ -184,13 +278,13 @@ pub mod macros { }; let mut searched_events = Vec::new(); - for record in event_records { + for record in event_records.clone() { if matches(&record.event) { searched_events.push(record.event); } } - searched_events + (searched_events, event_records) }}; } @@ -211,7 +305,8 @@ pub mod macros { /// ); /// ``` macro_rules! run { - ($env:expr, $chain:expr, $call:ty, $state:expr, $($sender:expr => $($calls:expr),+);*) => {{ + // ($env:expr, $chain:expr, $call:ty, $state:expr, $($sender:expr => $($calls:expr),+);*) => {{ + ($env:expr, $chain:expr, $call:ty, $state:expr, $($sender:expr => $($calls:expr$(,)?)+);*) => {{ use codec::Encode as _; trait CallAssimilator { @@ -248,6 +343,7 @@ pub mod macros { // Need to export after definition. pub(crate) use assert_events; pub(crate) use events; + pub(crate) use evolve_until_event_is_found; pub(crate) use extra_counts; pub(crate) use extra_guards; pub(crate) use run; diff --git a/runtime/integration-tests/src/utils/evm.rs b/runtime/integration-tests/src/utils/evm.rs new file mode 100644 index 0000000000..53520ce156 --- /dev/null +++ b/runtime/integration-tests/src/utils/evm.rs @@ -0,0 +1,78 @@ +// 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 frame_support::{dispatch::RawOrigin, traits::fungible::Mutate}; +use fudge::primitives::Chain; +use pallet_evm::FeeCalculator; +use runtime_common::account_conversion::AccountConverter; +use sp_core::{Get, H160, U256}; + +use crate::{ + chain::centrifuge::{Balances, Runtime, PARA_ID}, + utils::env::TestEnv, +}; + +pub fn mint_balance_into_derived_account(env: &mut TestEnv, address: H160, balance: u128) { + let chain_id = env + .with_state(Chain::Para(PARA_ID), || { + pallet_evm_chain_id::Pallet::::get() + }) + .unwrap(); + + let derived_account = + AccountConverter::::convert_evm_address(chain_id, address.to_fixed_bytes()); + + env.with_mut_state(Chain::Para(PARA_ID), || { + Balances::mint_into(&derived_account.into(), balance).unwrap() + }) + .unwrap(); +} + +pub fn deploy_contract(env: &mut TestEnv, address: H160, code: Vec) { + let chain_id = env + .with_state(Chain::Para(PARA_ID), || { + pallet_evm_chain_id::Pallet::::get() + }) + .unwrap(); + + let derived_address = + AccountConverter::::convert_evm_address(chain_id, address.to_fixed_bytes()); + + let transaction_create_cost = env + .with_state(Chain::Para(PARA_ID), || { + ::config().gas_transaction_create + }) + .unwrap(); + + let base_fee = env + .with_state(Chain::Para(PARA_ID), || { + let (base_fee, _) = ::FeeCalculator::min_gas_price(); + base_fee + }) + .unwrap(); + + env.with_mut_state(Chain::Para(PARA_ID), || { + pallet_evm::Pallet::::create( + RawOrigin::from(Some(derived_address)).into(), + address, + code, + U256::from(0), + transaction_create_cost * 10, + U256::from(base_fee + 10), + None, + None, + Vec::new(), + ) + .unwrap(); + }) + .unwrap(); +} diff --git a/runtime/integration-tests/src/utils/extrinsics.rs b/runtime/integration-tests/src/utils/extrinsics.rs index b479896e28..6d810cce4a 100644 --- a/runtime/integration-tests/src/utils/extrinsics.rs +++ b/runtime/integration-tests/src/utils/extrinsics.rs @@ -64,9 +64,11 @@ pub fn xt_centrifuge( (version.spec_version, version.transaction_version) }; - env.centrifuge - .with_state(|| sign_centrifuge(who, nonce, call, spec_version, tx_version, genesis_hash)) - .map_err(|_| ()) + let res = env + .centrifuge + .with_state(|| sign_centrifuge(who, nonce, call, spec_version, tx_version, genesis_hash)); + + res.map_err(|_| ()) } /// Generates an signed-extrinisc for relay-chain. diff --git a/runtime/integration-tests/src/utils/genesis.rs b/runtime/integration-tests/src/utils/genesis.rs index d5bdf2500a..529d998ad0 100644 --- a/runtime/integration-tests/src/utils/genesis.rs +++ b/runtime/integration-tests/src/utils/genesis.rs @@ -221,3 +221,31 @@ where .assimilate_storage(storage) .expect("ESSENTIAL: Genesisbuild is not allowed to fail."); } + +/// Sets the `default_accounts` as council members. +pub fn default_council_members(storage: &mut Storage) +where + Instance: 'static, + Runtime: pallet_collective::Config, + Runtime::AccountId: From, +{ + council_members::(default_accounts(), storage) +} + +/// Sets the provided account IDs as council members. +pub fn council_members(members: Vec, storage: &mut Storage) +where + Instance: 'static, + Runtime: pallet_collective::Config, + Runtime::AccountId: From, +{ + pallet_collective::GenesisConfig:: { + phantom: Default::default(), + members: members + .into_iter() + .map(|acc| acc.to_account_id().into()) + .collect(), + } + .assimilate_storage(storage) + .expect("ESSENTIAL: Pallet collective genesis build is not allowed to fail") +} diff --git a/runtime/integration-tests/src/utils/mod.rs b/runtime/integration-tests/src/utils/mod.rs index cc8d41751f..c1729932ba 100644 --- a/runtime/integration-tests/src/utils/mod.rs +++ b/runtime/integration-tests/src/utils/mod.rs @@ -13,12 +13,17 @@ use cfg_types::tokens::CurrencyId; pub mod accounts; +pub mod collective; +pub mod connectors_gateway; +pub mod democracy; pub mod env; +pub mod evm; pub mod extrinsics; pub mod genesis; pub mod loans; pub mod logs; pub mod pools; +pub mod preimage; pub mod time; pub mod tokens; @@ -28,5 +33,5 @@ pub const RELAY_ASSET_ID: CurrencyId = CurrencyId::ForeignAsset(1); pub const GLIMMER_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1000); /// The AUSD asset id pub const AUSD_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(2000); -/// The EVM Chain id of Moonbea +/// The EVM Chain id of Moonbeam pub const MOONBEAM_EVM_CHAIN_ID: u64 = 1284; diff --git a/runtime/integration-tests/src/utils/preimage.rs b/runtime/integration-tests/src/utils/preimage.rs new file mode 100644 index 0000000000..48e195f6a1 --- /dev/null +++ b/runtime/integration-tests/src/utils/preimage.rs @@ -0,0 +1,24 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use codec::Encode; +use pallet_preimage::Call as PreimageCall; + +use crate::chain::centrifuge::{Runtime, RuntimeCall}; + +pub fn note_preimage(call: &RuntimeCall) -> RuntimeCall { + let encoded_call = call.encode(); + + RuntimeCall::Preimage(PreimageCall::note_preimage { + bytes: encoded_call, + }) +} diff --git a/runtime/integration-tests/src/xcm/development/tests/connectors.rs b/runtime/integration-tests/src/xcm/development/tests/connectors.rs index 3ada22aed0..603424a8ea 100644 --- a/runtime/integration-tests/src/xcm/development/tests/connectors.rs +++ b/runtime/integration-tests/src/xcm/development/tests/connectors.rs @@ -27,11 +27,9 @@ use ::xcm::{ prelude::{Parachain, X1, X2}, VersionedMultiLocation, }; -use cfg_primitives::{ - currency_decimals, parachains, AccountId, Balance, Moment, PoolId, TrancheId, -}; +use cfg_primitives::{currency_decimals, parachains, AccountId, Balance, PoolId, TrancheId}; use cfg_traits::{ - connectors::{Codec as _, InboundQueue}, + connectors::{Codec, InboundQueue}, OrderManager, Permissions as _, PoolMutate, TrancheCurrency, }; use cfg_types::{ @@ -48,9 +46,11 @@ use cfg_types::{ xcm::XcmMetadata, }; use codec::Encode; +use connectors_gateway_routers::XcmDomain as GatewayXcmDomain; use development_runtime::{ - Balances, Connectors, Investments, Loans, OrmlAssetRegistry, OrmlTokens, Permissions, - PoolSystem, Runtime as DevelopmentRuntime, RuntimeOrigin, System, XTokens, XcmTransactor, + Balances, Connectors, ConnectorsGateway, Investments, Loans, OrmlAssetRegistry, OrmlTokens, + Permissions, PoolSystem, Runtime as DevelopmentRuntime, RuntimeOrigin, System, XTokens, + XcmTransactor, }; use frame_support::{ assert_noop, assert_ok, @@ -80,10 +80,13 @@ use utils::investments::{ use xcm_emulator::TestExt; use crate::{ - utils::{AUSD_CURRENCY_ID, MOONBEAM_EVM_CHAIN_ID}, + utils::{AUSD_CURRENCY_ID, GLIMMER_CURRENCY_ID, MOONBEAM_EVM_CHAIN_ID}, xcm::development::{ setup::{cfg, dollar, ALICE, BOB, PARA_ID_MOONBEAM}, test_net::{Development, Moonbeam, RelayChain, TestNet}, + tests::connectors::utils::{ + get_default_moonbeam_native_token_location, DEFAULT_MOONBEAM_LOCATION, + }, }, *, }; @@ -754,6 +757,7 @@ fn add_currency() { utils::setup_pre_requirements(); let currency_id = AUSD_CURRENCY_ID; + let location = utils::connector_transferable_multilocation( MOONBEAM_EVM_CHAIN_ID, utils::DEFAULT_EVM_ADDRESS_MOONBEAM, @@ -1612,7 +1616,11 @@ fn verify_tranche_fields_sizes() { } mod utils { + use cfg_primitives::Moment; use cfg_types::tokens::CrossChainTransferability; + use connectors_gateway_routers::{ + ethereum_xcm::EthereumXCMRouter, DomainRouter, XcmTransactInfo, + }; use super::*; use crate::{ @@ -1629,8 +1637,19 @@ mod utils { pub const DEFAULT_OTHER_DOMAIN_ADDRESS: DomainAddress = DomainAddress::EVM(MOONBEAM_EVM_CHAIN_ID, [0; 20]); + pub const DEFAULT_MOONBEAM_LOCATION: MultiLocation = MultiLocation { + parents: 1, + interior: X1(Parachain(PARA_ID_MOONBEAM)), + }; pub type ConnectorMessage = Message; + pub fn get_default_moonbeam_native_token_location() -> MultiLocation { + MultiLocation { + parents: 1, + interior: X2(Parachain(PARA_ID_MOONBEAM), general_key(&[0, 1])), + } + } + /// Returns a `VersionedMultiLocation` that can be converted into /// `ConnectorsWrappedToken` which is required for cross chain asset /// registration and transfer. @@ -1655,38 +1674,57 @@ mod utils { }) } + pub fn set_test_domain_router( + evm_chain_id: u64, + xcm_domain_location: VersionedMultiLocation, + currency_id: CurrencyId, + fee_location: VersionedMultiLocation, + ) { + let ethereum_xcm_router = EthereumXCMRouter:: { + xcm_domain: GatewayXcmDomain { + location: Box::new(xcm_domain_location), + ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), + contract_address: H160::from(utils::DEFAULT_EVM_ADDRESS_MOONBEAM), + max_gas_limit: 700_000, + transact_info: XcmTransactInfo { + transact_extra_weight: 1.into(), + max_weight: 8_000_000_000_000_000.into(), + transact_extra_weight_signed: Some(3.into()), + }, + fee_currency: currency_id, + fee_per_second: default_per_second(18), + fee_asset_location: Box::new(fee_location), + }, + _marker: Default::default(), + }; + + let domain_router = DomainRouter::EthereumXCM(ethereum_xcm_router); + let domain = Domain::EVM(evm_chain_id); + + assert_ok!(ConnectorsGateway::set_domain_router( + RuntimeOrigin::root(), + domain, + domain_router, + )); + } + /// Initializes universally required storage for connectors tests: - /// * Set transact info and domain router for Moonbeam `MultiLocation`, - /// * Set fee for GLMR (`GLIMMER_CURRENCY_ID`), + /// * Set the EthereumXCM router which in turn sets: + /// * transact info and domain router for Moonbeam `MultiLocation`, + /// * fee for GLMR (`GLIMMER_CURRENCY_ID`), /// * Register GLMR and AUSD in `OrmlAssetRegistry`, /// * Mint 10 GLMR (`DEFAULT_BALANCE_GLMR`) for Alice and Bob. /// /// NOTE: AUSD is the default pool currency in `create_pool`. /// Neither AUSD nor GLMR are registered as connector transferable currency! pub fn setup_pre_requirements() { - let moonbeam_location = MultiLocation { - parents: 1, - interior: X1(Parachain(PARA_ID_MOONBEAM)), - }; - let moonbeam_native_token = MultiLocation { - parents: 1, - interior: X2(Parachain(PARA_ID_MOONBEAM), general_key(&[0, 1])), - }; - - // We need to set the Transact info for Moonbeam in the XcmTransactor pallet - assert_ok!(XcmTransactor::set_transact_info( - RuntimeOrigin::root(), - Box::new(VersionedMultiLocation::V3(moonbeam_location)), - 1.into(), - 8_000_000_000_000_000.into(), - Some(3.into()) - )); - - assert_ok!(XcmTransactor::set_fee_per_second( - RuntimeOrigin::root(), - Box::new(VersionedMultiLocation::V3(moonbeam_native_token)), - default_per_second(18), // default fee_per_second for this token which has 18 decimals - )); + /// Set the EthereumXCM router necessary for Moonbeam. + set_test_domain_router( + MOONBEAM_EVM_CHAIN_ID, + DEFAULT_MOONBEAM_LOCATION.into(), + GLIMMER_CURRENCY_ID, + get_default_moonbeam_native_token_location().into(), + ); /// Register Moonbeam's native token assert_ok!(OrmlAssetRegistry::register_asset( @@ -1696,7 +1734,9 @@ mod utils { "GLMR".into(), 18, false, - Some(VersionedMultiLocation::V3(moonbeam_native_token)), + Some(VersionedMultiLocation::V3( + get_default_moonbeam_native_token_location() + )), CrossChainTransferability::Xcm(Default::default()), ), Some(GLIMMER_CURRENCY_ID) @@ -1706,21 +1746,6 @@ mod utils { OrmlTokens::deposit(GLIMMER_CURRENCY_ID, &ALICE.into(), DEFAULT_BALANCE_GLMR); OrmlTokens::deposit(GLIMMER_CURRENCY_ID, &BOB.into(), DEFAULT_BALANCE_GLMR); - assert_ok!(Connectors::set_domain_router( - RuntimeOrigin::root(), - DOMAIN_MOONBEAM, - Router::Xcm(XcmDomain { - location: Box::new(moonbeam_location.try_into().expect("Bad xcm version")), - ethereum_xcm_transact_call_index: BoundedVec::truncate_from(vec![38, 0]), - contract_address: H160::from( - <[u8; 20]>::from_hex("cE0Cb9BB900dfD0D378393A041f3abAb6B182882") - .expect("Invalid address"), - ), - fee_currency: GLIMMER_CURRENCY_ID, - max_gas_limit: 700_000, - }), - )); - // Register AUSD in the asset registry which is the default pool currency in // `create_pool` register_ausd();