diff --git a/Cargo.lock b/Cargo.lock index 8ce6162048..c099d27793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,6 +1516,7 @@ dependencies = [ "frame-system", "pallet-connectors-gateway", "pallet-ethereum", + "pallet-evm", "pallet-xcm-transactor", "parity-scale-codec 3.4.0", "scale-info", diff --git a/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml b/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml index 5227962fb3..de11cbbc05 100644 --- a/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml +++ b/pallets/connectors-gateway/connectors-gateway-routers/Cargo.toml @@ -27,6 +27,9 @@ xcm-primitives = { git = "https://github.com/PureStake/moonbeam", default-featur ethabi = { version = "16.0", default-features = false } pallet-ethereum = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.37" } +# EVM +pallet-evm = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.37" } + # Custom crates cfg-traits = { path = "../../../libs/traits", default-features = false } cfg-types = { path = "../../../libs/types", default-features = false } @@ -42,6 +45,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-ethereum/runtime-benchmarks", + "pallet-evm/std", "pallet-xcm-transactor/runtime-benchmarks", "xcm/runtime-benchmarks", "xcm-primitives/runtime-benchmarks", @@ -55,6 +59,7 @@ std = [ "xcm/std", "pallet-xcm-transactor/std", "pallet-ethereum/std", + "pallet-evm/std", "xcm-primitives/std", "ethabi/std", "scale-info/std", @@ -65,5 +70,6 @@ try-runtime = [ "cfg-traits/try-runtime", "cfg-types/try-runtime", "pallet-ethereum/try-runtime", + "pallet-evm/try-runtime", "pallet-xcm-transactor/try-runtime", ] \ No newline at end of file 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..27a2576205 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/axelar_evm.rs @@ -0,0 +1,211 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +use cfg_traits::connectors::Codec; +use codec::{Decode, Encode, MaxEncodedLen}; +use ethabi::{Contract, Function, Param, ParamType, Token}; +use frame_support::dispatch::{DispatchError, DispatchResult, RawOrigin}; +use scale_info::{ + prelude::string::{String, ToString}, + TypeInfo, +}; +use sp_core::{H160, U256}; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, vec, vec::Vec}; + +use crate::{AccountIdOf, MessageOf}; + +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct AxelarEVMRouter +where + T: frame_system::Config + pallet_connectors_gateway::Config + pallet_evm::Config, +{ + domain: EVMDomain, + _phantom: PhantomData, +} + +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct EVMDomain { + chain: EVMChain, + // TODO(cdamian): This should be registered on the EVM pallet, do we need it here or can we + // retrieve it somehow? + axelar_contract_address: H160, + connectors_contract_address: H160, + fee_values: FeeValues, +} + +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub struct FeeValues { + value: U256, + gas_limit: u64, + max_fee_per_gas: U256, + max_priority_fee_per_gas: Option, +} + +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum EVMChain { + Ethereum, +} + +/// Required due to the naming convention defined by Axelar here: +/// https://docs.axelar.dev/dev/reference/mainnet-chain-names +impl ToString for EVMChain { + fn to_string(&self) -> String { + match self { + EVMChain::Ethereum => "Ethereum".to_string(), + } + } +} + +impl AxelarEVMRouter +where + T: frame_system::Config + pallet_connectors_gateway::Config + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, +{ + pub fn do_send(&self, sender: AccountIdOf, msg: MessageOf) -> DispatchResult { + let eth_msg = self.get_eth_msg(msg).map_err(|e| DispatchError::Other(e))?; + + pallet_evm::Pallet::::call( + // The `call` method uses `EnsureAddressTruncated` to check this `origin`, this ensures + // that `origin` is the same as `source`. + RawOrigin::Signed(sender.clone()).into(), + // TODO(cdamian): Triple-check if truncating is OK: + // - who is this sender in a real use case and how is it handled in the Connectors + // contract? + // - do we need to map a substrate address to an ethereum one for extra validation? + // (Paranoid by Black Sabbath playing in the background) + H160::from_slice(&sender.as_ref()[0..20]), + self.domain.axelar_contract_address.clone(), + eth_msg, + self.domain.fee_values.value.clone(), + self.domain.fee_values.gas_limit.clone(), + self.domain.fee_values.max_fee_per_gas.clone(), + self.domain.fee_values.max_priority_fee_per_gas.clone(), + // TODO(cdamian): No nonce, is this OK? + None, + // TODO(cdamian): No access list, is this required? + Vec::new(), + ) + .map_err(|e| e.error)?; + + Ok(()) + } + + // - Connectors pallet should encode this to a single byte string, basically the + // Connectors encoded message. + // - The Axelar EVM router should ABI encode a call to call `callContract` on + // the Axelar router with as destination contract the `Router` of connectors + // on the target chain, and as payload the Connectors encoded message. + // - This ABI encoded call should be submitted into the Axelar Solidity router + // on our EVM. + // - Axelar then handles bridging & routing this, it eventually ends up as a + // transaction on the destination chain, calling `execute` of the Axelar + // router I pointed to above. This will then forward it to the gateway + // contract which will decode the Connectors encoded message. + fn get_eth_msg(&self, msg: MessageOf) -> Result, &'static str> { + // AxelarEVMRouter -> calls `callContract` on the Axelar Gateway contract + // deployed in the EVM pallet. + // + // Axelar Gateway contract -> calls `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([( + "handle".to_string(), + vec![Function { + name: "handle".into(), + inputs: vec![Param { + name: "message".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("handle") + .map_err(|_| "cannot retrieve handle function")? + .encode_input(&[Token::Bytes(msg.serialize())]) + .map_err(|_| "cannot encode input for handle 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([( + "callContract".into(), + vec![Function { + name: "callContract".into(), + inputs: vec![ + Param { + name: "destinationChain".into(), + kind: ParamType::String, + internal_type: None, + }, + Param { + name: "destinationContractAddress".into(), + kind: ParamType::String, + internal_type: None, + }, + Param { + name: "payload".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("callContract") + .map_err(|_| "cannot retrieve callContract 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 callContract 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 index 509f1b0e76..a2a3a9d1a1 100644 --- a/pallets/connectors-gateway/connectors-gateway-routers/src/ethereum_xcm.rs +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/ethereum_xcm.rs @@ -33,9 +33,12 @@ where _phantom: PhantomData, } -/// The ConnectorsXcmContract handle function name +/// The ConnectorsXcmContract handle function name. const HANDLE_FUNCTION: &str = "handle"; +/// The ConnectorsXcmContract message param name. +const MESSAGE_PARAM: &str = "message"; + impl EthereumXCMRouter where T: frame_system::Config + pallet_xcm_transactor::Config + pallet_connectors_gateway::Config, @@ -141,11 +144,11 @@ where let mut functions = BTreeMap::new(); #[allow(deprecated)] functions.insert( - "handle".into(), + HANDLE_FUNCTION.into(), vec![ethabi::Function { name: HANDLE_FUNCTION.into(), inputs: vec![ethabi::Param { - name: "message".into(), + name: MESSAGE_PARAM.into(), kind: ethabi::ParamType::Bytes, internal_type: None, }], diff --git a/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs b/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs index 9a21be5489..b260383b4b 100644 --- a/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs +++ b/pallets/connectors-gateway/connectors-gateway-routers/src/lib.rs @@ -16,8 +16,9 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::dispatch::DispatchResult; use scale_info::TypeInfo; -use crate::ethereum_xcm::EthereumXCMRouter; +use crate::{axelar_evm::AxelarEVMRouter, ethereum_xcm::EthereumXCMRouter}; +pub mod axelar_evm; pub mod ethereum_xcm; type CurrencyIdOf = ::CurrencyId; @@ -27,14 +28,23 @@ type AccountIdOf = ::AccountId; #[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, + T: frame_system::Config + + pallet_xcm_transactor::Config + + pallet_connectors_gateway::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, + T: frame_system::Config + + pallet_xcm_transactor::Config + + pallet_connectors_gateway::Config + + pallet_evm::Config, + T::AccountId: AsRef<[u8; 32]>, { type Message = MessageOf; type Sender = AccountIdOf; @@ -42,6 +52,7 @@ where 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), } } }